1
0
mirror of https://github.com/mxpv/podsync.git synced 2024-05-11 05:55:04 +00:00
mxpv-podsync/pkg/handler/handler.go

307 lines
7.3 KiB
Go
Raw Normal View History

2017-10-28 01:11:49 -07:00
package handler
import (
2017-10-30 17:26:46 -07:00
"encoding/json"
"io/ioutil"
2017-10-28 01:11:49 -07:00
"net/http"
"strings"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
2018-12-09 11:08:05 -08:00
"github.com/mxpv/patreon-go"
2017-10-28 01:11:49 -07:00
itunes "github.com/mxpv/podcast"
2018-12-02 13:58:41 -08:00
"golang.org/x/oauth2"
2017-10-28 01:11:49 -07:00
"github.com/mxpv/podsync/pkg/api"
"github.com/mxpv/podsync/pkg/config"
"github.com/mxpv/podsync/pkg/session"
2018-12-02 13:58:41 -08:00
log "github.com/sirupsen/logrus"
2017-10-28 01:11:49 -07:00
)
const (
maxHashIDLength = 16
)
2017-11-02 18:01:35 -07:00
type feedService interface {
2017-10-28 01:11:49 -07:00
CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) (string, error)
2017-11-03 19:16:15 -07:00
BuildFeed(hashID string) (*itunes.Podcast, error)
2017-11-03 16:04:33 -07:00
GetMetadata(hashId string) (*api.Metadata, error)
2017-11-03 20:55:58 -07:00
Downgrade(patronID string, featureLevel int) error
2017-10-28 01:11:49 -07:00
}
2017-11-02 18:01:35 -07:00
type patreonService interface {
2017-11-02 18:19:21 -07:00
Hook(pledge *patreon.Pledge, event string) error
2017-11-10 17:13:01 -08:00
GetFeatureLevelByID(patronID string) int
GetFeatureLevelFromAmount(amount int) int
2017-11-02 18:01:35 -07:00
}
2017-10-28 01:11:49 -07:00
type handler struct {
2017-11-02 18:01:35 -07:00
feed feedService
2017-11-02 17:43:08 -07:00
cfg *config.AppConfig
oauth2 oauth2.Config
2017-11-02 18:01:35 -07:00
patreon patreonService
2017-10-28 01:11:49 -07:00
}
2018-12-09 11:08:05 -08:00
func New(feed feedService, support patreonService, cfg *config.AppConfig) http.Handler {
r := gin.New()
r.Use(gin.Recovery())
store := sessions.NewCookieStore([]byte(cfg.CookieSecret))
r.Use(sessions.Sessions("podsync", store))
h := handler{
feed: feed,
patreon: support,
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("/login", h.login)
r.GET("/logout", h.logout)
r.GET("/patreon", h.patreonCallback)
r.GET("/api/ping", h.ping)
r.GET("/api/user", h.user)
r.POST("/api/create", h.create)
r.GET("/api/metadata/:hashId", h.metadata)
r.POST("/api/webhooks", h.webhook)
r.NoRoute(h.getFeed)
return r
}
2017-10-28 01:11:49 -07:00
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
2017-11-10 17:13:01 -08:00
level := h.patreon.GetFeatureLevelByID(user.Data.ID)
2017-10-28 01:11:49 -07:00
identity := &api.Identity{
2017-10-30 17:26:46 -07:00
UserId: user.Data.ID,
2017-10-28 01:11:49 -07:00
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) ping(c *gin.Context) {
c.String(http.StatusOK, "ok")
}
2018-12-09 11:08:05 -08:00
func (h handler) user(c *gin.Context) {
identity, err := session.GetIdentity(c)
if err != nil {
identity = &api.Identity{}
}
c.JSON(http.StatusOK, gin.H{
"user_id": identity.UserId,
"feature_level": identity.FeatureLevel,
})
}
2017-10-28 01:11:49 -07:00
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
}
// Check feature level again if user deleted pledge by still logged in
2017-11-10 17:13:01 -08:00
identity.FeatureLevel = h.patreon.GetFeatureLevelByID(identity.UserId)
2017-10-28 01:11:49 -07:00
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) {
2018-12-02 13:58:41 -08:00
hashID := c.Request.URL.Path[1:]
if hashID == "" || len(hashID) > maxHashIDLength {
2017-10-28 01:11:49 -07:00
c.String(http.StatusBadRequest, "invalid feed id")
return
}
2018-12-02 13:58:41 -08:00
if strings.HasSuffix(hashID, ".xml") {
hashID = strings.TrimSuffix(hashID, ".xml")
2017-10-28 01:11:49 -07:00
}
2018-12-02 13:58:41 -08:00
podcast, err := h.feed.BuildFeed(hashID)
2017-10-28 01:11:49 -07:00
if err != nil {
2018-12-02 13:58:41 -08:00
log.WithError(err).WithField("hash_id", hashID).Error("failed to build feed")
2017-10-28 01:11:49 -07:00
code := http.StatusInternalServerError
if err == api.ErrNotFound {
code = http.StatusNotFound
2017-11-10 17:13:01 -08:00
} else if err == api.ErrQuotaExceeded {
code = http.StatusTooManyRequests
2017-10-28 01:11:49 -07:00
}
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)
}
2017-10-30 17:26:46 -07:00
func (h handler) webhook(c *gin.Context) {
// Read body to byte array in order to verify signature first
body, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
2018-12-02 13:58:41 -08:00
log.WithError(err).Error("failed to read webhook request")
2017-10-30 17:26:46 -07:00
c.Status(http.StatusBadRequest)
return
}
// Verify signature
signature := c.GetHeader(patreon.HeaderSignature)
valid, err := patreon.VerifySignature(body, h.cfg.PatreonWebhooksSecret, signature)
if err != nil {
2018-12-02 13:58:41 -08:00
log.WithError(err).Error("failed to verify signature")
2017-10-30 17:26:46 -07:00
c.Status(http.StatusBadRequest)
return
}
if !valid {
2018-12-02 13:58:41 -08:00
log.Errorf("webhooks signatures are not equal (header: %s)", signature)
2017-10-30 17:26:46 -07:00
c.Status(http.StatusUnauthorized)
return
}
// Get event name
eventName := c.GetHeader(patreon.HeaderEventType)
if eventName == "" {
2018-12-02 13:58:41 -08:00
log.Error("event name header is empty")
2017-10-30 17:26:46 -07:00
c.Status(http.StatusBadRequest)
return
}
pledge := &patreon.WebhookPledge{}
if err := json.Unmarshal(body, pledge); err != nil {
2018-12-02 13:58:41 -08:00
log.WithError(err).Error("failed to unmarshal pledge")
2017-10-30 17:26:46 -07:00
c.JSON(badRequest(err))
return
}
2017-11-02 17:43:08 -07:00
if err := h.patreon.Hook(&pledge.Data, eventName); err != nil {
2018-12-02 13:58:41 -08:00
log.WithError(err).WithFields(log.Fields{
"user_id": pledge.Data.Relationships.Patron.Data.ID,
"pledge_id": pledge.Data.ID,
"pledge_event": eventName,
}).Error("failed to process patreon event")
2017-11-05 13:04:36 -08:00
2018-10-06 19:24:45 -07:00
// Don't return any errors to Patreon, otherwise subsequent notifications will be blocked.
2017-10-30 17:26:46 -07:00
return
}
2017-11-10 17:13:01 -08:00
patronID := pledge.Data.Relationships.Patron.Data.ID
if eventName == patreon.EventUpdatePledge {
newLevel := h.patreon.GetFeatureLevelFromAmount(pledge.Data.Attributes.AmountCents)
if err := h.feed.Downgrade(patronID, newLevel); err != nil {
return
}
} else if eventName == patreon.EventDeletePledge {
if err := h.feed.Downgrade(patronID, api.DefaultFeatures); err != nil {
2017-11-03 20:55:58 -07:00
return
}
}
2018-12-02 13:58:41 -08:00
log.Infof("sucessfully processed patreon event %s (%s)", pledge.Data.ID, eventName)
2017-10-30 17:26:46 -07:00
}
2017-10-28 01:11:49 -07:00
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()}
}