1
0
mirror of https://github.com/mxpv/podsync.git synced 2024-05-11 05:55:04 +00:00
mxpv-podsync/pkg/server/server.go
2017-10-24 21:26:41 -07:00

233 lines
5.1 KiB
Go

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()}
}