Initial commit

This commit is contained in:
r 2019-12-13 18:08:26 +00:00
commit 5e4da01c3a
43 changed files with 3429 additions and 0 deletions

151
service/auth.go Normal file
View file

@ -0,0 +1,151 @@
package service
import (
"context"
"errors"
"io"
"mastodon"
"web/model"
)
var (
ErrInvalidSession = errors.New("invalid session")
)
type authService struct {
sessionRepo model.SessionRepository
appRepo model.AppRepository
Service
}
func NewAuthService(sessionRepo model.SessionRepository, appRepo model.AppRepository, s Service) Service {
return &authService{sessionRepo, appRepo, s}
}
func getSessionID(ctx context.Context) (sessionID string, err error) {
sessionID, ok := ctx.Value("session_id").(string)
if !ok || len(sessionID) < 1 {
return "", ErrInvalidSession
}
return sessionID, nil
}
func (s *authService) getClient(ctx context.Context) (c *mastodon.Client, err error) {
sessionID, err := getSessionID(ctx)
if err != nil {
return nil, ErrInvalidSession
}
session, err := s.sessionRepo.Get(sessionID)
if err != nil {
return nil, ErrInvalidSession
}
client, err := s.appRepo.Get(session.InstanceURL)
if err != nil {
return
}
c = mastodon.NewClient(&mastodon.Config{
Server: session.InstanceURL,
ClientID: client.ClientID,
ClientSecret: client.ClientSecret,
AccessToken: session.AccessToken,
})
return c, nil
}
func (s *authService) GetAuthUrl(ctx context.Context, instance string) (
redirectUrl string, sessionID string, err error) {
return s.Service.GetAuthUrl(ctx, instance)
}
func (s *authService) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
code string) (token string, err error) {
sessionID, err = getSessionID(ctx)
if err != nil {
return
}
c, err = s.getClient(ctx)
if err != nil {
return
}
token, err = s.Service.GetUserToken(ctx, sessionID, c, code)
if err != nil {
return
}
err = s.sessionRepo.Update(sessionID, token)
if err != nil {
return
}
return
}
func (s *authService) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
return s.Service.ServeHomePage(ctx, client)
}
func (s *authService) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
s.Service.ServeErrorPage(ctx, client, err)
}
func (s *authService) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
return s.Service.ServeSigninPage(ctx, client)
}
func (s *authService) ServeTimelinePage(ctx context.Context, client io.Writer,
c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
c, err = s.getClient(ctx)
if err != nil {
return
}
return s.Service.ServeTimelinePage(ctx, client, c, maxID, sinceID, minID)
}
func (s *authService) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
c, err = s.getClient(ctx)
if err != nil {
return
}
return s.Service.ServeThreadPage(ctx, client, c, id, reply)
}
func (s *authService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
c, err = s.getClient(ctx)
if err != nil {
return
}
return s.Service.Like(ctx, client, c, id)
}
func (s *authService) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
c, err = s.getClient(ctx)
if err != nil {
return
}
return s.Service.UnLike(ctx, client, c, id)
}
func (s *authService) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
c, err = s.getClient(ctx)
if err != nil {
return
}
return s.Service.Retweet(ctx, client, c, id)
}
func (s *authService) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
c, err = s.getClient(ctx)
if err != nil {
return
}
return s.Service.UnRetweet(ctx, client, c, id)
}
func (s *authService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) {
c, err = s.getClient(ctx)
if err != nil {
return
}
return s.Service.PostTweet(ctx, client, c, content, replyToID)
}

117
service/logging.go Normal file
View file

@ -0,0 +1,117 @@
package service
import (
"context"
"io"
"log"
"mastodon"
"time"
)
type loggingService struct {
logger *log.Logger
Service
}
func NewLoggingService(logger *log.Logger, s Service) Service {
return &loggingService{logger, s}
}
func (s *loggingService) GetAuthUrl(ctx context.Context, instance string) (
redirectUrl string, sessionID string, err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, instance=%v, took=%v, err=%v\n",
"GetAuthUrl", instance, time.Since(begin), err)
}(time.Now())
return s.Service.GetAuthUrl(ctx, instance)
}
func (s *loggingService) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
code string) (token string, err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, session_id=%v, code=%v, took=%v, err=%v\n",
"GetUserToken", sessionID, code, time.Since(begin), err)
}(time.Now())
return s.Service.GetUserToken(ctx, sessionID, c, code)
}
func (s *loggingService) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, took=%v, err=%v\n",
"ServeHomePage", time.Since(begin), err)
}(time.Now())
return s.Service.ServeHomePage(ctx, client)
}
func (s *loggingService) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, err=%v, took=%v\n",
"ServeErrorPage", err, time.Since(begin))
}(time.Now())
s.Service.ServeErrorPage(ctx, client, err)
}
func (s *loggingService) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, took=%v, err=%v\n",
"ServeSigninPage", time.Since(begin), err)
}(time.Now())
return s.Service.ServeSigninPage(ctx, client)
}
func (s *loggingService) ServeTimelinePage(ctx context.Context, client io.Writer,
c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, max_id=%v, since_id=%v, min_id=%v, took=%v, err=%v\n",
"ServeTimelinePage", maxID, sinceID, minID, time.Since(begin), err)
}(time.Now())
return s.Service.ServeTimelinePage(ctx, client, c, maxID, sinceID, minID)
}
func (s *loggingService) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, id=%v, reply=%v, took=%v, err=%v\n",
"ServeThreadPage", id, reply, time.Since(begin), err)
}(time.Now())
return s.Service.ServeThreadPage(ctx, client, c, id, reply)
}
func (s *loggingService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
"Like", id, time.Since(begin), err)
}(time.Now())
return s.Service.Like(ctx, client, c, id)
}
func (s *loggingService) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
"UnLike", id, time.Since(begin), err)
}(time.Now())
return s.Service.UnLike(ctx, client, c, id)
}
func (s *loggingService) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
"Retweet", id, time.Since(begin), err)
}(time.Now())
return s.Service.Retweet(ctx, client, c, id)
}
func (s *loggingService) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
"UnRetweet", id, time.Since(begin), err)
}(time.Now())
return s.Service.UnRetweet(ctx, client, c, id)
}
func (s *loggingService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) {
defer func(begin time.Time) {
s.logger.Printf("method=%v, content=%v, reply_to_id=%v, took=%v, err=%v\n",
"PostTweet", content, replyToID, time.Since(begin), err)
}(time.Now())
return s.Service.PostTweet(ctx, client, c, content, replyToID)
}

285
service/service.go Normal file
View file

@ -0,0 +1,285 @@
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"mastodon"
"web/model"
"web/renderer"
"web/util"
)
var (
ErrInvalidArgument = errors.New("invalid argument")
ErrInvalidToken = errors.New("invalid token")
ErrInvalidClient = errors.New("invalid client")
)
type Service interface {
ServeHomePage(ctx context.Context, client io.Writer) (err error)
GetAuthUrl(ctx context.Context, instance string) (url string, sessionID string, err error)
GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, token string) (accessToken string, err error)
ServeErrorPage(ctx context.Context, client io.Writer, err error)
ServeSigninPage(ctx context.Context, client io.Writer) (err error)
ServeTimelinePage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, sinceID string, minID string) (err error)
ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error)
Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error)
}
type service struct {
clientName string
clientScope string
clientWebsite string
renderer renderer.Renderer
sessionRepo model.SessionRepository
appRepo model.AppRepository
}
func NewService(clientName string, clientScope string, clientWebsite string,
renderer renderer.Renderer, sessionRepo model.SessionRepository,
appRepo model.AppRepository) Service {
return &service{
clientName: clientName,
clientScope: clientScope,
clientWebsite: clientWebsite,
renderer: renderer,
sessionRepo: sessionRepo,
appRepo: appRepo,
}
}
func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
redirectUrl string, sessionID string, err error) {
if !strings.HasPrefix(instance, "https://") {
instance = "https://" + instance
}
sessionID = util.NewSessionId()
err = svc.sessionRepo.Add(model.Session{
ID: sessionID,
InstanceURL: instance,
})
if err != nil {
return
}
app, err := svc.appRepo.Get(instance)
if err != nil {
if err != model.ErrAppNotFound {
return
}
var mastoApp *mastodon.Application
mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
Server: instance,
ClientName: svc.clientName,
Scopes: svc.clientScope,
Website: svc.clientWebsite,
RedirectURIs: svc.clientWebsite + "/oauth_callback",
})
if err != nil {
return
}
app = model.App{
InstanceURL: instance,
ClientID: mastoApp.ClientID,
ClientSecret: mastoApp.ClientSecret,
}
err = svc.appRepo.Add(app)
if err != nil {
return
}
}
u, err := url.Parse(path.Join(instance, "/oauth/authorize"))
if err != nil {
return
}
q := make(url.Values)
q.Set("scope", "read write follow")
q.Set("client_id", app.ClientID)
q.Set("response_type", "code")
q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
u.RawQuery = q.Encode()
redirectUrl = u.String()
return
}
func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
code string) (token string, err error) {
if len(code) < 1 {
err = ErrInvalidArgument
return
}
session, err := svc.sessionRepo.Get(sessionID)
if err != nil {
return
}
app, err := svc.appRepo.Get(session.InstanceURL)
if err != nil {
return
}
data := &bytes.Buffer{}
err = json.NewEncoder(data).Encode(map[string]string{
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": svc.clientWebsite + "/oauth_callback",
})
if err != nil {
return
}
resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
if err != nil {
return
}
defer resp.Body.Close()
var res struct {
AccessToken string `json:"access_token"`
}
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return
}
/*
err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
if err != nil {
return
}
err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
*/
return res.AccessToken, nil
}
func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
err = svc.renderer.RenderHomePage(ctx, client)
if err != nil {
return
}
return
}
func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
svc.renderer.RenderErrorPage(ctx, client, err)
}
func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
err = svc.renderer.RenderSigninPage(ctx, client)
if err != nil {
return
}
return
}
func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
var hasNext, hasPrev bool
var nextLink, prevLink string
var pg = mastodon.Pagination{
MaxID: maxID,
SinceID: sinceID,
MinID: minID,
Limit: 20,
}
statuses, err := c.GetTimelineHome(ctx, &pg)
if err != nil {
return err
}
if len(pg.MaxID) > 0 {
hasNext = true
nextLink = fmt.Sprintf("/timeline?max_id=%s", pg.MaxID)
}
if len(pg.SinceID) > 0 {
hasPrev = true
prevLink = fmt.Sprintf("/timeline?since_id=%s", pg.SinceID)
}
data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink)
err = svc.renderer.RenderTimelinePage(ctx, client, data)
if err != nil {
return
}
return
}
func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
status, err := c.GetStatus(ctx, id)
if err != nil {
return
}
context, err := c.GetStatusContext(ctx, id)
if err != nil {
return
}
data := renderer.NewThreadPageTemplateData(status, context, reply, id)
err = svc.renderer.RenderThreadPage(ctx, client, data)
if err != nil {
return
}
return
}
func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
_, err = c.Favourite(ctx, id)
return
}
func (svc *service) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
_, err = c.Unfavourite(ctx, id)
return
}
func (svc *service) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
_, err = c.Reblog(ctx, id)
return
}
func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
_, err = c.Unreblog(ctx, id)
return
}
func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) {
tweet := &mastodon.Toot{
Status: content,
InReplyToID: replyToID,
}
_, err = c.PostStatus(ctx, tweet)
return
}

165
service/transport.go Normal file
View file

@ -0,0 +1,165 @@
package service
import (
"context"
"fmt"
"net/http"
"path"
"github.com/gorilla/mux"
)
var (
ctx = context.Background()
cookieAge = "31536000"
)
func getContextWithSession(ctx context.Context, req *http.Request) context.Context {
sessionID, err := req.Cookie("session_id")
if err != nil {
return ctx
}
return context.WithValue(ctx, "session_id", sessionID.Value)
}
func NewHandler(s Service, staticDir string) http.Handler {
r := mux.NewRouter()
r.PathPrefix("/static").Handler(http.StripPrefix("/static",
http.FileServer(http.Dir(path.Join(".", staticDir)))))
r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
err := s.ServeHomePage(ctx, w)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
}).Methods(http.MethodGet)
r.HandleFunc("/signin", func(w http.ResponseWriter, req *http.Request) {
err := s.ServeSigninPage(ctx, w)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
}).Methods(http.MethodGet)
r.HandleFunc("/signin", func(w http.ResponseWriter, req *http.Request) {
instance := req.FormValue("instance")
url, sessionId, err := s.GetAuthUrl(ctx, instance)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
w.Header().Add("Set-Cookie", fmt.Sprintf("session_id=%s;max-age=%s", sessionId, cookieAge))
w.Header().Add("Location", url)
w.WriteHeader(http.StatusSeeOther)
}).Methods(http.MethodPost)
r.HandleFunc("/oauth_callback", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
token := req.URL.Query().Get("code")
_, err := s.GetUserToken(ctx, "", nil, token)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
w.Header().Add("Location", "/timeline")
w.WriteHeader(http.StatusSeeOther)
}).Methods(http.MethodGet)
r.HandleFunc("/timeline", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
maxID := req.URL.Query().Get("max_id")
sinceID := req.URL.Query().Get("since_id")
minID := req.URL.Query().Get("min_id")
err := s.ServeTimelinePage(ctx, w, nil, maxID, sinceID, minID)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
}).Methods(http.MethodGet)
r.HandleFunc("/thread/{id}", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
id, _ := mux.Vars(req)["id"]
reply := req.URL.Query().Get("reply")
err := s.ServeThreadPage(ctx, w, nil, id, len(reply) > 1)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
}).Methods(http.MethodGet)
r.HandleFunc("/like/{id}", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
id, _ := mux.Vars(req)["id"]
err := s.Like(ctx, w, nil, id)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
w.Header().Add("Location", req.Header.Get("Referer"))
w.WriteHeader(http.StatusSeeOther)
}).Methods(http.MethodGet)
r.HandleFunc("/unlike/{id}", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
id, _ := mux.Vars(req)["id"]
err := s.UnLike(ctx, w, nil, id)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
w.Header().Add("Location", req.Header.Get("Referer"))
w.WriteHeader(http.StatusSeeOther)
}).Methods(http.MethodGet)
r.HandleFunc("/retweet/{id}", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
id, _ := mux.Vars(req)["id"]
err := s.Retweet(ctx, w, nil, id)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
w.Header().Add("Location", req.Header.Get("Referer"))
w.WriteHeader(http.StatusSeeOther)
}).Methods(http.MethodGet)
r.HandleFunc("/unretweet/{id}", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
id, _ := mux.Vars(req)["id"]
err := s.UnRetweet(ctx, w, nil, id)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
w.Header().Add("Location", req.Header.Get("Referer"))
w.WriteHeader(http.StatusSeeOther)
}).Methods(http.MethodGet)
r.HandleFunc("/post", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req)
content := req.FormValue("content")
replyToID := req.FormValue("reply_to_id")
err := s.PostTweet(ctx, w, nil, content, replyToID)
if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
w.Header().Add("Location", req.Header.Get("Referer"))
w.WriteHeader(http.StatusSeeOther)
}).Methods(http.MethodPost)
return r
}