mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
Implement http handler for webhooks
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mxpv/patreon-go"
|
||||
revision = "181da1e272784f51dea234c0e58e595321abd1ed"
|
||||
version = "1.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/ventu-io/go-shortid"
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user