mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
Refactor server package
This commit is contained in:
@ -13,8 +13,8 @@ import (
|
||||
"github.com/mxpv/podsync/pkg/builders"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/feeds"
|
||||
"github.com/mxpv/podsync/pkg/handler"
|
||||
"github.com/mxpv/podsync/pkg/id"
|
||||
"github.com/mxpv/podsync/pkg/server"
|
||||
"github.com/mxpv/podsync/pkg/storage"
|
||||
)
|
||||
|
||||
@ -63,7 +63,7 @@ func main() {
|
||||
|
||||
srv := http.Server{
|
||||
Addr: fmt.Sprintf(":%d", 5001),
|
||||
Handler: server.MakeHandlers(feed, cfg),
|
||||
Handler: handler.New(feed, cfg),
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
256
pkg/handler/handler.go
Normal file
256
pkg/handler/handler.go
Normal file
@ -0,0 +1,256 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mxpv/patreon-go"
|
||||
itunes "github.com/mxpv/podcast"
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/session"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
creatorID = "2822191"
|
||||
maxHashIDLength = 16
|
||||
)
|
||||
|
||||
type feed interface {
|
||||
CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) (string, error)
|
||||
GetFeed(hashId string) (*itunes.Podcast, error)
|
||||
GetMetadata(hashId string) (*api.Feed, error)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
feed feed
|
||||
cfg *config.AppConfig
|
||||
oauth2 oauth2.Config
|
||||
}
|
||||
|
||||
func (h handler) index(c *gin.Context) {
|
||||
identity, err := session.GetIdentity(c)
|
||||
if err != nil {
|
||||
identity = &api.Identity{}
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", identity)
|
||||
}
|
||||
|
||||
func (h handler) login(c *gin.Context) {
|
||||
state, err := session.SetState(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
authURL := h.oauth2.AuthCodeURL(state)
|
||||
c.Redirect(http.StatusFound, authURL)
|
||||
}
|
||||
|
||||
func (h handler) logout(c *gin.Context) {
|
||||
session.Clear(c)
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (h handler) patreonCallback(c *gin.Context) {
|
||||
// Validate session state
|
||||
if session.GetSetate(c) != c.Query("state") {
|
||||
c.String(http.StatusUnauthorized, "invalid state")
|
||||
return
|
||||
}
|
||||
|
||||
// Exchange code with tokens
|
||||
token, err := h.oauth2.Exchange(c.Request.Context(), c.Query("code"))
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Create Patreon client
|
||||
tc := h.oauth2.Client(c.Request.Context(), token)
|
||||
client := patreon.NewClient(tc)
|
||||
|
||||
// Query user info from Patreon
|
||||
user, err := client.FetchUser()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Determine feature level
|
||||
level := api.DefaultFeatures
|
||||
|
||||
if user.Data.Id == creatorID {
|
||||
level = api.PodcasterFeature
|
||||
} else {
|
||||
amount := 0
|
||||
for _, item := range user.Included.Items {
|
||||
pledge, ok := item.(*patreon.Pledge)
|
||||
if ok {
|
||||
amount += pledge.Attributes.AmountCents
|
||||
}
|
||||
}
|
||||
|
||||
if amount >= 100 {
|
||||
level = api.ExtendedFeatures
|
||||
}
|
||||
}
|
||||
|
||||
identity := &api.Identity{
|
||||
UserId: user.Data.Id,
|
||||
FullName: user.Data.Attributes.FullName,
|
||||
Email: user.Data.Attributes.Email,
|
||||
ProfileURL: user.Data.Attributes.URL,
|
||||
FeatureLevel: level,
|
||||
}
|
||||
|
||||
session.SetIdentity(c, identity)
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (h handler) robots(c *gin.Context) {
|
||||
c.String(http.StatusOK, `User-agent: *
|
||||
Allow: /$
|
||||
Disallow: /
|
||||
Host: www.podsync.net`)
|
||||
}
|
||||
|
||||
func (h handler) ping(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
func (h handler) create(c *gin.Context) {
|
||||
req := &api.CreateFeedRequest{}
|
||||
|
||||
if err := c.BindJSON(req); err != nil {
|
||||
c.JSON(badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
identity, err := session.GetIdentity(c)
|
||||
if err != nil {
|
||||
c.JSON(internalError(err))
|
||||
return
|
||||
}
|
||||
|
||||
hashId, err := h.feed.CreateFeed(req, identity)
|
||||
if err != nil {
|
||||
c.JSON(internalError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"id": hashId})
|
||||
}
|
||||
|
||||
func (h handler) getFeed(c *gin.Context) {
|
||||
hashId := c.Request.URL.Path[1:]
|
||||
if hashId == "" || len(hashId) > maxHashIDLength {
|
||||
c.String(http.StatusBadRequest, "invalid feed id")
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(hashId, ".xml") {
|
||||
hashId = strings.TrimSuffix(hashId, ".xml")
|
||||
}
|
||||
|
||||
podcast, err := h.feed.GetFeed(hashId)
|
||||
if err != nil {
|
||||
code := http.StatusInternalServerError
|
||||
if err == api.ErrNotFound {
|
||||
code = http.StatusNotFound
|
||||
} else {
|
||||
log.Printf("server error (hash id: %s): %v", hashId, err)
|
||||
}
|
||||
|
||||
c.String(code, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "application/rss+xml; charset=UTF-8", podcast.Bytes())
|
||||
}
|
||||
|
||||
func (h handler) metadata(c *gin.Context) {
|
||||
hashId := c.Param("hashId")
|
||||
if hashId == "" || len(hashId) > maxHashIDLength {
|
||||
c.String(http.StatusBadRequest, "invalid feed id")
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := h.feed.GetMetadata(hashId)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, feed)
|
||||
}
|
||||
|
||||
func New(feed feed, cfg *config.AppConfig) http.Handler {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
store := sessions.NewCookieStore([]byte(cfg.CookieSecret))
|
||||
r.Use(sessions.Sessions("podsync", store))
|
||||
|
||||
// Static files + HTML
|
||||
|
||||
log.Printf("using assets path: %s", cfg.AssetsPath)
|
||||
if cfg.AssetsPath != "" {
|
||||
r.Static("/assets", cfg.AssetsPath)
|
||||
}
|
||||
|
||||
log.Printf("using templates path: %s", cfg.TemplatesPath)
|
||||
if cfg.TemplatesPath != "" {
|
||||
r.LoadHTMLGlob(path.Join(cfg.TemplatesPath, "*.html"))
|
||||
}
|
||||
|
||||
h := handler{
|
||||
feed: feed,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
// OAuth 2 configuration
|
||||
|
||||
h.oauth2 = oauth2.Config{
|
||||
ClientID: cfg.PatreonClientId,
|
||||
ClientSecret: cfg.PatreonSecret,
|
||||
RedirectURL: cfg.PatreonRedirectURL,
|
||||
Scopes: []string{"users", "pledges-to-me", "my-campaign"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: patreon.AuthorizationURL,
|
||||
TokenURL: patreon.AccessTokenURL,
|
||||
},
|
||||
}
|
||||
|
||||
// Handlers
|
||||
|
||||
r.GET("/", h.index)
|
||||
r.GET("/login", h.login)
|
||||
r.GET("/logout", h.logout)
|
||||
r.GET("/patreon", h.patreonCallback)
|
||||
r.GET("/robots.txt", h.robots)
|
||||
|
||||
r.GET("/api/ping", h.ping)
|
||||
r.POST("/api/create", h.create)
|
||||
r.GET("/api/metadata/:hashId", h.metadata)
|
||||
|
||||
r.NoRoute(h.getFeed)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func badRequest(err error) (int, interface{}) {
|
||||
return http.StatusBadRequest, gin.H{"error": err.Error()}
|
||||
}
|
||||
|
||||
func internalError(err error) (int, interface{}) {
|
||||
log.Printf("server error: %v", err)
|
||||
return http.StatusInternalServerError, gin.H{"error": err.Error()}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: server.go
|
||||
// Source: handler.go
|
||||
|
||||
package server
|
||||
package handler
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
@ -1,6 +1,6 @@
|
||||
//go:generate mockgen -source=server.go -destination=server_mock_test.go -package=server
|
||||
//go:generate mockgen -source=handler.go -destination=handler_mock_test.go -package=handler
|
||||
|
||||
package server
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@ -32,7 +32,7 @@ func TestCreateFeed(t *testing.T) {
|
||||
feed := NewMockfeed(ctrl)
|
||||
feed.EXPECT().CreateFeed(gomock.Eq(req), gomock.Any()).Times(1).Return("456", nil)
|
||||
|
||||
srv := httptest.NewServer(MakeHandlers(feed, cfg))
|
||||
srv := httptest.NewServer(New(feed, cfg))
|
||||
defer srv.Close()
|
||||
|
||||
query := `{"url": "https://youtube.com/channel/123", "page_size": 55, "quality": "low", "format": "audio"}`
|
||||
@ -47,7 +47,7 @@ func TestCreateInvalidFeed(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
srv := httptest.NewServer(MakeHandlers(NewMockfeed(ctrl), cfg))
|
||||
srv := httptest.NewServer(New(NewMockfeed(ctrl), cfg))
|
||||
defer srv.Close()
|
||||
|
||||
query := `{}`
|
||||
@ -100,7 +100,7 @@ func TestGetFeed(t *testing.T) {
|
||||
feed := NewMockfeed(ctrl)
|
||||
feed.EXPECT().GetFeed("123").Return(&podcast, nil)
|
||||
|
||||
srv := httptest.NewServer(MakeHandlers(feed, cfg))
|
||||
srv := httptest.NewServer(New(feed, cfg))
|
||||
defer srv.Close()
|
||||
|
||||
resp, err := http.Get(srv.URL + "/123")
|
||||
@ -115,7 +115,7 @@ func TestGetMetadata(t *testing.T) {
|
||||
feed := NewMockfeed(ctrl)
|
||||
feed.EXPECT().GetMetadata("123").Times(1).Return(&api.Feed{}, nil)
|
||||
|
||||
srv := httptest.NewServer(MakeHandlers(feed, cfg))
|
||||
srv := httptest.NewServer(New(feed, cfg))
|
||||
defer srv.Close()
|
||||
|
||||
resp, err := http.Get(srv.URL + "/api/metadata/123")
|
@ -1,232 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mxpv/patreon-go"
|
||||
itunes "github.com/mxpv/podcast"
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/session"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
creatorID = "2822191"
|
||||
maxHashIDLength = 16
|
||||
)
|
||||
|
||||
type feed interface {
|
||||
CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) (string, error)
|
||||
GetFeed(hashId string) (*itunes.Podcast, error)
|
||||
GetMetadata(hashId string) (*api.Feed, error)
|
||||
}
|
||||
|
||||
func MakeHandlers(feed feed, cfg *config.AppConfig) http.Handler {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
store := sessions.NewCookieStore([]byte(cfg.CookieSecret))
|
||||
r.Use(sessions.Sessions("podsync", store))
|
||||
|
||||
// Static files + HTML
|
||||
|
||||
log.Printf("using assets path: %s", cfg.AssetsPath)
|
||||
if cfg.AssetsPath != "" {
|
||||
r.Static("/assets", cfg.AssetsPath)
|
||||
}
|
||||
|
||||
log.Printf("using templates path: %s", cfg.TemplatesPath)
|
||||
if cfg.TemplatesPath != "" {
|
||||
r.LoadHTMLGlob(path.Join(cfg.TemplatesPath, "*.html"))
|
||||
}
|
||||
|
||||
conf := &oauth2.Config{
|
||||
ClientID: cfg.PatreonClientId,
|
||||
ClientSecret: cfg.PatreonSecret,
|
||||
RedirectURL: cfg.PatreonRedirectURL,
|
||||
Scopes: []string{"users", "pledges-to-me", "my-campaign"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: patreon.AuthorizationURL,
|
||||
TokenURL: patreon.AccessTokenURL,
|
||||
},
|
||||
}
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
identity, err := session.GetIdentity(c)
|
||||
if err != nil {
|
||||
identity = &api.Identity{}
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", identity)
|
||||
})
|
||||
|
||||
r.GET("/login", func(c *gin.Context) {
|
||||
state, err := session.SetState(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
authURL := conf.AuthCodeURL(state)
|
||||
c.Redirect(http.StatusFound, authURL)
|
||||
})
|
||||
|
||||
r.GET("/logout", func(c *gin.Context) {
|
||||
session.Clear(c)
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
})
|
||||
|
||||
r.GET("/patreon", func(c *gin.Context) {
|
||||
// Validate session state
|
||||
if session.GetSetate(c) != c.Query("state") {
|
||||
c.String(http.StatusUnauthorized, "invalid state")
|
||||
return
|
||||
}
|
||||
|
||||
// Exchange code with tokens
|
||||
token, err := conf.Exchange(c.Request.Context(), c.Query("code"))
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Create Patreon client
|
||||
tc := conf.Client(c.Request.Context(), token)
|
||||
client := patreon.NewClient(tc)
|
||||
|
||||
// Query user info from Patreon
|
||||
user, err := client.FetchUser()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Determine feature level
|
||||
level := api.DefaultFeatures
|
||||
|
||||
if user.Data.Id == creatorID {
|
||||
level = api.PodcasterFeature
|
||||
} else {
|
||||
amount := 0
|
||||
for _, item := range user.Included.Items {
|
||||
pledge, ok := item.(*patreon.Pledge)
|
||||
if ok {
|
||||
amount += pledge.Attributes.AmountCents
|
||||
}
|
||||
}
|
||||
|
||||
if amount >= 100 {
|
||||
level = api.ExtendedFeatures
|
||||
}
|
||||
}
|
||||
|
||||
identity := &api.Identity{
|
||||
UserId: user.Data.Id,
|
||||
FullName: user.Data.Attributes.FullName,
|
||||
Email: user.Data.Attributes.Email,
|
||||
ProfileURL: user.Data.Attributes.URL,
|
||||
FeatureLevel: level,
|
||||
}
|
||||
|
||||
session.SetIdentity(c, identity)
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
})
|
||||
|
||||
// GET /robots.txt
|
||||
r.GET("/robots.txt", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, `User-agent: *
|
||||
Allow: /$
|
||||
Disallow: /
|
||||
Host: www.podsync.net`)
|
||||
})
|
||||
|
||||
// REST API
|
||||
|
||||
r.GET("/api/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
r.POST("/api/create", func(c *gin.Context) {
|
||||
req := &api.CreateFeedRequest{}
|
||||
|
||||
if err := c.BindJSON(req); err != nil {
|
||||
c.JSON(badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
identity, err := session.GetIdentity(c)
|
||||
if err != nil {
|
||||
c.JSON(internalError(err))
|
||||
return
|
||||
}
|
||||
|
||||
hashId, err := feed.CreateFeed(req, identity)
|
||||
if err != nil {
|
||||
c.JSON(internalError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"id": hashId})
|
||||
})
|
||||
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
hashId := c.Request.URL.Path[1:]
|
||||
if hashId == "" || len(hashId) > maxHashIDLength {
|
||||
c.String(http.StatusBadRequest, "invalid feed id")
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(hashId, ".xml") {
|
||||
hashId = strings.TrimSuffix(hashId, ".xml")
|
||||
}
|
||||
|
||||
podcast, err := feed.GetFeed(hashId)
|
||||
if err != nil {
|
||||
code := http.StatusInternalServerError
|
||||
if err == api.ErrNotFound {
|
||||
code = http.StatusNotFound
|
||||
} else {
|
||||
log.Printf("server error (hash id: %s): %v", hashId, err)
|
||||
}
|
||||
|
||||
c.String(code, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "application/rss+xml; charset=UTF-8", podcast.Bytes())
|
||||
})
|
||||
|
||||
r.GET("/api/metadata/:hashId", func(c *gin.Context) {
|
||||
hashId := c.Param("hashId")
|
||||
if hashId == "" || len(hashId) > maxHashIDLength {
|
||||
c.String(http.StatusBadRequest, "invalid feed id")
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := feed.GetMetadata(hashId)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, feed)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func badRequest(err error) (int, interface{}) {
|
||||
return http.StatusBadRequest, gin.H{"error": err.Error()}
|
||||
}
|
||||
|
||||
func internalError(err error) (int, interface{}) {
|
||||
log.Printf("server error: %v", err)
|
||||
return http.StatusInternalServerError, gin.H{"error": err.Error()}
|
||||
}
|
Reference in New Issue
Block a user