diff --git a/Gopkg.toml b/Gopkg.toml index a52729e..5ceb3df 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -13,7 +13,7 @@ [[constraint]] name = "github.com/mxpv/patreon-go" - revision = "181da1e272784f51dea234c0e58e595321abd1ed" + version = "1.3" [[constraint]] name = "github.com/ventu-io/go-shortid" diff --git a/cmd/app/main.go b/cmd/app/main.go index f291c9a..0441eb1 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -4,11 +4,15 @@ import ( "context" "fmt" "log" + "net" "net/http" "os" "os/signal" + "strings" "syscall" + "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" + "github.com/go-pg/pg" "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/builders" "github.com/mxpv/podsync/pkg/config" @@ -16,6 +20,7 @@ import ( "github.com/mxpv/podsync/pkg/handler" "github.com/mxpv/podsync/pkg/id" "github.com/mxpv/podsync/pkg/storage" + "github.com/pkg/errors" ) func main() { @@ -42,6 +47,11 @@ func main() { panic(err) } + pg, err := createPg(cfg.PostgresConnectionURL) + if err != nil { + panic(err) + } + // Builders youtube, err := builders.NewYouTubeBuilder(cfg.YouTubeApiKey) @@ -63,7 +73,7 @@ func main() { srv := http.Server{ Addr: fmt.Sprintf(":%d", 5001), - Handler: handler.New(feed, cfg), + Handler: handler.New(feed, pg, cfg), } go func() { @@ -81,3 +91,29 @@ func main() { log.Printf("server gracefully stopped") } + +func createPg(connectionURL string) (*pg.DB, error) { + opts, err := pg.ParseURL(connectionURL) + if err != nil { + return nil, err + } + + // If host format is "projection:region:host", than use Google SQL Proxy + // See https://github.com/go-pg/pg/issues/576 + if strings.Count(opts.Addr, ":") == 2 { + log.Print("using GCP SQL proxy") + opts.Dialer = func(network, addr string) (net.Conn, error) { + return proxy.Dial(addr) + } + } + + db := pg.Connect(opts) + + // Check database connectivity + if _, err := db.ExecOne("SELECT 1"); err != nil { + db.Close() + return nil, errors.Wrap(err, "failed to check database connectivity") + } + + return db, nil +} diff --git a/docker-compose.yml b/docker-compose.yml index 6733308..cef05e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - 5001 environment: - REDIS_CONNECTION_URL=redis://redis + - POSTGRES_CONNECTION_URL={POSTGRES_CONNECTION_URL} - YOUTUBE_API_KEY={YOUTUBE_API_KEY} - VIMEO_API_KEY={VIMEO_API_KEY} - PATREON_CLIENT_ID={PATREON_CLIENT_ID} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 18266ea..3f360ef 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -1,6 +1,8 @@ package handler import ( + "encoding/json" + "io/ioutil" "log" "net/http" "path" @@ -8,11 +10,13 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/go-pg/pg" "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" + "github.com/mxpv/podsync/pkg/webhook" "golang.org/x/oauth2" ) @@ -31,6 +35,7 @@ type handler struct { feed feed cfg *config.AppConfig oauth2 oauth2.Config + hook *webhook.Handler } func (h handler) index(c *gin.Context) { @@ -87,7 +92,7 @@ func (h handler) patreonCallback(c *gin.Context) { // Determine feature level level := api.DefaultFeatures - if user.Data.Id == creatorID { + if user.Data.ID == creatorID { level = api.PodcasterFeature } else { amount := 0 @@ -104,7 +109,7 @@ func (h handler) patreonCallback(c *gin.Context) { } identity := &api.Identity{ - UserId: user.Data.Id, + UserId: user.Data.ID, FullName: user.Data.Attributes.FullName, Email: user.Data.Attributes.Email, ProfileURL: user.Data.Attributes.URL, @@ -192,7 +197,52 @@ func (h handler) metadata(c *gin.Context) { c.JSON(http.StatusOK, feed) } -func New(feed feed, cfg *config.AppConfig) http.Handler { +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 { + log.Printf("failed to read webhook body: %v", err) + c.Status(http.StatusBadRequest) + return + } + + // Verify signature + signature := c.GetHeader(patreon.HeaderSignature) + valid, err := patreon.VerifySignature(body, h.cfg.PatreonWebhooksSecret, signature) + if err != nil { + log.Printf("failed to verify signature: %v", err) + c.Status(http.StatusBadRequest) + return + } + + if !valid { + c.Status(http.StatusUnauthorized) + return + } + + // Get event name + eventName := c.GetHeader(patreon.HeaderEventType) + if eventName == "" { + log.Print("event name header is empty") + c.Status(http.StatusBadRequest) + return + } + + pledge := &patreon.WebhookPledge{} + if err := json.Unmarshal(body, pledge); err != nil { + c.JSON(badRequest(err)) + return + } + + if err := h.hook.Handle(&pledge.Data, eventName); err != nil { + c.JSON(internalError(err)) + return + } + + log.Printf("sucessfully processed patreon event %s (%s)", pledge.Data.ID, eventName) +} + +func New(feed feed, db *pg.DB, cfg *config.AppConfig) http.Handler { r := gin.New() r.Use(gin.Recovery()) @@ -214,6 +264,7 @@ func New(feed feed, cfg *config.AppConfig) http.Handler { h := handler{ feed: feed, cfg: cfg, + hook: webhook.NewHookHandler(db), } // OAuth 2 configuration @@ -240,6 +291,7 @@ func New(feed feed, cfg *config.AppConfig) http.Handler { r.GET("/api/ping", h.ping) r.POST("/api/create", h.create) r.GET("/api/metadata/:hashId", h.metadata) + r.POST("/api/webhooks", h.webhook) r.NoRoute(h.getFeed) diff --git a/pkg/webhook/hook.go b/pkg/webhook/hook.go index 217970e..d500d46 100644 --- a/pkg/webhook/hook.go +++ b/pkg/webhook/hook.go @@ -10,28 +10,19 @@ import ( "github.com/pkg/errors" ) -const ( - EventHeader = "X-Patreon-Event" - SignatureHeader = "X-Patreon-Signature" - - EventNameCreatePledge = "pledges:create" - EventNameUpdatePledge = "pledges:update" - EventNameDeletePledge = "pledges:delete" -) - type Handler struct { db *pg.DB } func (h Handler) toModel(pledge *patreon.Pledge) (*models.Pledge, error) { - pledgeID, err := strconv.ParseInt(pledge.Id, 10, 64) + pledgeID, err := strconv.ParseInt(pledge.ID, 10, 64) if err != nil { - return nil, errors.Wrapf(err, "failed to parse pledge id: %s", pledge.Id) + return nil, errors.Wrapf(err, "failed to parse pledge id: %s", pledge.ID) } - patronID, err := strconv.ParseInt(pledge.Relationships.Patron.Data.Id, 10, 64) + patronID, err := strconv.ParseInt(pledge.Relationships.Patron.Data.ID, 10, 64) if err != nil { - return nil, errors.Wrapf(err, "failed to parse patron id: %s", pledge.Relationships.Patron.Data.Id) + return nil, errors.Wrapf(err, "failed to parse patron id: %s", pledge.Relationships.Patron.Data.ID) } model := &models.Pledge{ @@ -72,11 +63,11 @@ func (h Handler) Handle(pledge *patreon.Pledge, event string) error { } switch event { - case EventNameCreatePledge: + case patreon.EventCreatePledge: return h.db.Insert(model) - case EventNameUpdatePledge: + case patreon.EventUpdatePledge: return h.db.Update(model) - case EventNameDeletePledge: + case patreon.EventDeletePledge: return h.db.Delete(model) default: return fmt.Errorf("unknown event: %s", event) diff --git a/pkg/webhook/hook_test.go b/pkg/webhook/hook_test.go index 61fd40f..cb05e79 100644 --- a/pkg/webhook/hook_test.go +++ b/pkg/webhook/hook_test.go @@ -14,7 +14,7 @@ func TestCreate(t *testing.T) { pledge := createPledge() hook := createHandler(t) - err := hook.Handle(pledge, EventNameCreatePledge) + err := hook.Handle(pledge, patreon.EventCreatePledge) require.NoError(t, err) model := &models.Pledge{PledgeID: 12345} @@ -27,12 +27,12 @@ func TestUpdate(t *testing.T) { pledge := createPledge() hook := createHandler(t) - err := hook.Handle(pledge, EventNameCreatePledge) + err := hook.Handle(pledge, patreon.EventCreatePledge) require.NoError(t, err) pledge.Attributes.AmountCents = 999 - err = hook.Handle(pledge, EventNameUpdatePledge) + err = hook.Handle(pledge, patreon.EventUpdatePledge) require.NoError(t, err) model := &models.Pledge{PledgeID: 12345} @@ -45,10 +45,10 @@ func TestDelete(t *testing.T) { pledge := createPledge() hook := createHandler(t) - err := hook.Handle(pledge, EventNameCreatePledge) + err := hook.Handle(pledge, patreon.EventCreatePledge) require.NoError(t, err) - err = hook.Handle(pledge, EventNameDeletePledge) + err = hook.Handle(pledge, patreon.EventDeletePledge) require.NoError(t, err) } @@ -68,7 +68,7 @@ func createHandler(t *testing.T) *Handler { func createPledge() *patreon.Pledge { pledge := &patreon.Pledge{ - Id: "12345", + ID: "12345", Type: "pledge", } @@ -76,7 +76,7 @@ func createPledge() *patreon.Pledge { pledge.Attributes.CreatedAt = patreon.NullTime{Valid: true, Time: time.Now().UTC()} pledge.Relationships.Patron = &patreon.PatronRelationship{} - pledge.Relationships.Patron.Data.Id = "67890" + pledge.Relationships.Patron.Data.ID = "67890" return pledge }