mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
261 lines
5.6 KiB
Go
261 lines
5.6 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"path"
|
|
|
|
"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"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
const (
|
|
campaignId = "278915"
|
|
identitySessionKey = "identity"
|
|
)
|
|
|
|
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) {
|
|
s := sessions.Default(c)
|
|
|
|
identity := &api.Identity{
|
|
FeatureLevel: api.DefaultFeatures,
|
|
}
|
|
|
|
buf, ok := s.Get(identitySessionKey).(string)
|
|
if ok {
|
|
// We are failed to deserialize Identity structure, do cleanup, force user to login again
|
|
if err := json.Unmarshal([]byte(buf), identity); err != nil {
|
|
s.Clear()
|
|
s.Save()
|
|
}
|
|
}
|
|
|
|
c.HTML(http.StatusOK, "index.html", identity)
|
|
})
|
|
|
|
r.GET("/login", func(c *gin.Context) {
|
|
state := randToken()
|
|
|
|
s := sessions.Default(c)
|
|
s.Set("state", state)
|
|
s.Save()
|
|
|
|
authURL := conf.AuthCodeURL(state)
|
|
c.Redirect(http.StatusFound, authURL)
|
|
})
|
|
|
|
r.GET("/logout", func(c *gin.Context) {
|
|
s := sessions.Default(c)
|
|
s.Clear()
|
|
s.Save()
|
|
|
|
c.Redirect(http.StatusFound, "/")
|
|
})
|
|
|
|
r.GET("/patreon", func(c *gin.Context) {
|
|
// Validate session state
|
|
s := sessions.Default(c)
|
|
state := s.Get("state")
|
|
if state != 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
|
|
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,
|
|
}
|
|
|
|
// Serialize identity and return cookies
|
|
buf, err := json.Marshal(identity)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
s.Clear()
|
|
s.Set(identitySessionKey, string(buf))
|
|
s.Save()
|
|
|
|
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
|
|
}
|
|
|
|
s := sessions.Default(c)
|
|
|
|
identity := &api.Identity{
|
|
FeatureLevel: api.DefaultFeatures,
|
|
}
|
|
|
|
buf, ok := s.Get(identitySessionKey).(string)
|
|
if ok {
|
|
// We are failed to deserialize Identity structure, do cleanup, force user to login again
|
|
if err := json.Unmarshal([]byte(buf), identity); err != nil {
|
|
s.Clear()
|
|
s.Save()
|
|
}
|
|
}
|
|
|
|
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) > 12 {
|
|
c.String(http.StatusBadRequest, "invalid feed id")
|
|
return
|
|
}
|
|
|
|
podcast, err := feed.GetFeed(hashId)
|
|
if err != nil {
|
|
code := http.StatusInternalServerError
|
|
if err == api.ErrNotFound {
|
|
code = http.StatusNotFound
|
|
}
|
|
|
|
c.String(code, err.Error())
|
|
return
|
|
}
|
|
|
|
c.Data(http.StatusOK, "application/rss+xml", podcast.Bytes())
|
|
})
|
|
|
|
r.GET("/api/metadata/:hashId", func(c *gin.Context) {
|
|
hashId := c.Param("hashId")
|
|
if hashId == "" || len(hashId) > 12 {
|
|
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{}) {
|
|
return http.StatusInternalServerError, gin.H{"error": err.Error()}
|
|
}
|
|
|
|
func randToken() string {
|
|
b := make([]byte, 32)
|
|
rand.Read(b)
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|