diff --git a/cmd/app/main.go b/cmd/app/main.go index 0441eb1..0f19e8a 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -20,6 +20,7 @@ import ( "github.com/mxpv/podsync/pkg/handler" "github.com/mxpv/podsync/pkg/id" "github.com/mxpv/podsync/pkg/storage" + "github.com/mxpv/podsync/pkg/support" "github.com/pkg/errors" ) @@ -47,11 +48,13 @@ func main() { panic(err) } - pg, err := createPg(cfg.PostgresConnectionURL) + database, err := createPg(cfg.PostgresConnectionURL) if err != nil { panic(err) } + patreon := support.NewPatreon(database) + // Builders youtube, err := builders.NewYouTubeBuilder(cfg.YouTubeApiKey) @@ -73,7 +76,7 @@ func main() { srv := http.Server{ Addr: fmt.Sprintf(":%d", 5001), - Handler: handler.New(feed, pg, cfg), + Handler: handler.New(feed, patreon, cfg), } go func() { @@ -88,6 +91,7 @@ func main() { log.Printf("shutting down server") srv.Shutdown(ctx) + database.Close() log.Printf("server gracefully stopped") } diff --git a/pkg/feeds/feeds.go b/pkg/feeds/feeds.go index da070df..f564a29 100644 --- a/pkg/feeds/feeds.go +++ b/pkg/feeds/feeds.go @@ -13,9 +13,22 @@ const ( maxPageSize = 150 ) +type idService interface { + Generate(feed *api.Feed) (string, error) +} + +type storageService interface { + CreateFeed(feed *api.Feed) error + GetFeed(hashId string) (*api.Feed, error) +} + +type builder interface { + Build(feed *api.Feed) (podcast *itunes.Podcast, err error) +} + type service struct { - id id - storage storage + id idService + storage storageService builders map[api.Provider]builder } @@ -85,13 +98,13 @@ func (s *service) GetMetadata(hashId string) (*api.Feed, error) { type feedOption func(*service) -func WithStorage(storage storage) feedOption { +func WithStorage(storage storageService) feedOption { return func(service *service) { service.storage = storage } } -func WithIdGen(id id) feedOption { +func WithIdGen(id idService) feedOption { return func(service *service) { service.id = id } diff --git a/pkg/feeds/interfaces_mock_test.go b/pkg/feeds/feeds_mock_test.go similarity index 57% rename from pkg/feeds/interfaces_mock_test.go rename to pkg/feeds/feeds_mock_test.go index a76822c..cc1cdcc 100644 --- a/pkg/feeds/interfaces_mock_test.go +++ b/pkg/feeds/feeds_mock_test.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: interfaces.go +// Source: feeds.go package feeds @@ -10,31 +10,31 @@ import ( reflect "reflect" ) -// Mockid is a mock of id interface -type Mockid struct { +// MockidService is a mock of idService interface +type MockidService struct { ctrl *gomock.Controller - recorder *MockidMockRecorder + recorder *MockidServiceMockRecorder } -// MockidMockRecorder is the mock recorder for Mockid -type MockidMockRecorder struct { - mock *Mockid +// MockidServiceMockRecorder is the mock recorder for MockidService +type MockidServiceMockRecorder struct { + mock *MockidService } -// NewMockid creates a new mock instance -func NewMockid(ctrl *gomock.Controller) *Mockid { - mock := &Mockid{ctrl: ctrl} - mock.recorder = &MockidMockRecorder{mock} +// NewMockidService creates a new mock instance +func NewMockidService(ctrl *gomock.Controller) *MockidService { + mock := &MockidService{ctrl: ctrl} + mock.recorder = &MockidServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (_m *Mockid) EXPECT() *MockidMockRecorder { +func (_m *MockidService) EXPECT() *MockidServiceMockRecorder { return _m.recorder } // Generate mocks base method -func (_m *Mockid) Generate(feed *api.Feed) (string, error) { +func (_m *MockidService) Generate(feed *api.Feed) (string, error) { ret := _m.ctrl.Call(_m, "Generate", feed) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) @@ -42,47 +42,47 @@ func (_m *Mockid) Generate(feed *api.Feed) (string, error) { } // Generate indicates an expected call of Generate -func (_mr *MockidMockRecorder) Generate(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Generate", reflect.TypeOf((*Mockid)(nil).Generate), arg0) +func (_mr *MockidServiceMockRecorder) Generate(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Generate", reflect.TypeOf((*MockidService)(nil).Generate), arg0) } -// Mockstorage is a mock of storage interface -type Mockstorage struct { +// MockstorageService is a mock of storageService interface +type MockstorageService struct { ctrl *gomock.Controller - recorder *MockstorageMockRecorder + recorder *MockstorageServiceMockRecorder } -// MockstorageMockRecorder is the mock recorder for Mockstorage -type MockstorageMockRecorder struct { - mock *Mockstorage +// MockstorageServiceMockRecorder is the mock recorder for MockstorageService +type MockstorageServiceMockRecorder struct { + mock *MockstorageService } -// NewMockstorage creates a new mock instance -func NewMockstorage(ctrl *gomock.Controller) *Mockstorage { - mock := &Mockstorage{ctrl: ctrl} - mock.recorder = &MockstorageMockRecorder{mock} +// NewMockstorageService creates a new mock instance +func NewMockstorageService(ctrl *gomock.Controller) *MockstorageService { + mock := &MockstorageService{ctrl: ctrl} + mock.recorder = &MockstorageServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (_m *Mockstorage) EXPECT() *MockstorageMockRecorder { +func (_m *MockstorageService) EXPECT() *MockstorageServiceMockRecorder { return _m.recorder } // CreateFeed mocks base method -func (_m *Mockstorage) CreateFeed(feed *api.Feed) error { +func (_m *MockstorageService) CreateFeed(feed *api.Feed) error { ret := _m.ctrl.Call(_m, "CreateFeed", feed) ret0, _ := ret[0].(error) return ret0 } // CreateFeed indicates an expected call of CreateFeed -func (_mr *MockstorageMockRecorder) CreateFeed(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "CreateFeed", reflect.TypeOf((*Mockstorage)(nil).CreateFeed), arg0) +func (_mr *MockstorageServiceMockRecorder) CreateFeed(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "CreateFeed", reflect.TypeOf((*MockstorageService)(nil).CreateFeed), arg0) } // GetFeed mocks base method -func (_m *Mockstorage) GetFeed(hashId string) (*api.Feed, error) { +func (_m *MockstorageService) GetFeed(hashId string) (*api.Feed, error) { ret := _m.ctrl.Call(_m, "GetFeed", hashId) ret0, _ := ret[0].(*api.Feed) ret1, _ := ret[1].(error) @@ -90,8 +90,8 @@ func (_m *Mockstorage) GetFeed(hashId string) (*api.Feed, error) { } // GetFeed indicates an expected call of GetFeed -func (_mr *MockstorageMockRecorder) GetFeed(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "GetFeed", reflect.TypeOf((*Mockstorage)(nil).GetFeed), arg0) +func (_mr *MockstorageServiceMockRecorder) GetFeed(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "GetFeed", reflect.TypeOf((*MockstorageService)(nil).GetFeed), arg0) } // Mockbuilder is a mock of builder interface diff --git a/pkg/feeds/feeds_test.go b/pkg/feeds/feeds_test.go index 7d81baa..b585847 100644 --- a/pkg/feeds/feeds_test.go +++ b/pkg/feeds/feeds_test.go @@ -1,4 +1,4 @@ -//go:generate mockgen -source=interfaces.go -destination=interfaces_mock_test.go -package=feeds +//go:generate mockgen -source=feeds.go -destination=feeds_mock_test.go -package=feeds package feeds @@ -14,10 +14,10 @@ func TestService_CreateFeed(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - id := NewMockid(ctrl) + id := NewMockidService(ctrl) id.EXPECT().Generate(gomock.Any()).Times(1).Return("123", nil) - storage := NewMockstorage(ctrl) + storage := NewMockstorageService(ctrl) storage.EXPECT().CreateFeed(gomock.Any()).Times(1).Return(nil) s := service{ @@ -44,7 +44,7 @@ func TestService_GetFeed(t *testing.T) { feed := &api.Feed{Provider: api.Youtube} - storage := NewMockstorage(ctrl) + storage := NewMockstorageService(ctrl) storage.EXPECT().GetFeed("123").Times(1).Return(feed, nil) bld := NewMockbuilder(ctrl) @@ -63,7 +63,7 @@ func TestService_GetMetadata(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - storage := NewMockstorage(ctrl) + storage := NewMockstorageService(ctrl) storage.EXPECT().GetFeed("123").Times(1).Return(&api.Feed{}, nil) s := service{storage: storage} diff --git a/pkg/feeds/interfaces.go b/pkg/feeds/interfaces.go deleted file mode 100644 index f85355e..0000000 --- a/pkg/feeds/interfaces.go +++ /dev/null @@ -1,19 +0,0 @@ -package feeds - -import ( - itunes "github.com/mxpv/podcast" - "github.com/mxpv/podsync/pkg/api" -) - -type id interface { - Generate(feed *api.Feed) (string, error) -} - -type storage interface { - CreateFeed(feed *api.Feed) error - GetFeed(hashId string) (*api.Feed, error) -} - -type builder interface { - Build(feed *api.Feed) (podcast *itunes.Podcast, err error) -} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index cd61c8c..806857c 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -10,32 +10,34 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" - "github.com/go-pg/pg" - "github.com/mxpv/patreon-go" + patreon "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" ) const ( - creatorID = "2822191" maxHashIDLength = 16 ) -type feed interface { +type feedService interface { CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) (string, error) GetFeed(hashId string) (*itunes.Podcast, error) GetMetadata(hashId string) (*api.Feed, error) } +type patreonService interface { + Hook(pledge *patreon.Pledge, event string) error + GetFeatureLevel(patronID string) int +} + type handler struct { - feed feed - cfg *config.AppConfig - oauth2 oauth2.Config - hook *webhook.Handler + feed feedService + cfg *config.AppConfig + oauth2 oauth2.Config + patreon patreonService } func (h handler) index(c *gin.Context) { @@ -90,23 +92,7 @@ func (h handler) patreonCallback(c *gin.Context) { } // 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 - } - } + level := h.patreon.GetFeatureLevel(user.Data.ID) identity := &api.Identity{ UserId: user.Data.ID, @@ -145,6 +131,9 @@ func (h handler) create(c *gin.Context) { return } + // Check feature level again if user deleted pledge by still logged in + identity.FeatureLevel = h.patreon.GetFeatureLevel(identity.UserId) + hashId, err := h.feed.CreateFeed(req, identity) if err != nil { c.JSON(internalError(err)) @@ -235,7 +224,7 @@ func (h handler) webhook(c *gin.Context) { return } - if err := h.hook.Handle(&pledge.Data, eventName); err != nil { + if err := h.patreon.Hook(&pledge.Data, eventName); err != nil { log.Printf("failed to process patreon event %s (%s): %v", pledge.Data.ID, eventName, err) c.JSON(internalError(err)) return @@ -244,7 +233,7 @@ func (h handler) webhook(c *gin.Context) { log.Printf("sucessfully processed patreon event %s (%s)", pledge.Data.ID, eventName) } -func New(feed feed, db *pg.DB, cfg *config.AppConfig) http.Handler { +func New(feed feedService, support patreonService, cfg *config.AppConfig) http.Handler { r := gin.New() r.Use(gin.Recovery()) @@ -264,9 +253,9 @@ func New(feed feed, db *pg.DB, cfg *config.AppConfig) http.Handler { } h := handler{ - feed: feed, - cfg: cfg, - hook: webhook.NewHookHandler(db), + feed: feed, + patreon: support, + cfg: cfg, } // OAuth 2 configuration diff --git a/pkg/handler/handler_mock_test.go b/pkg/handler/handler_mock_test.go index 9c88046..050eb6a 100644 --- a/pkg/handler/handler_mock_test.go +++ b/pkg/handler/handler_mock_test.go @@ -5,36 +5,37 @@ package handler import ( gomock "github.com/golang/mock/gomock" + patreon_go "github.com/mxpv/patreon-go" podcast "github.com/mxpv/podcast" api "github.com/mxpv/podsync/pkg/api" reflect "reflect" ) -// Mockfeed is a mock of feed interface -type Mockfeed struct { +// MockfeedService is a mock of feedService interface +type MockfeedService struct { ctrl *gomock.Controller - recorder *MockfeedMockRecorder + recorder *MockfeedServiceMockRecorder } -// MockfeedMockRecorder is the mock recorder for Mockfeed -type MockfeedMockRecorder struct { - mock *Mockfeed +// MockfeedServiceMockRecorder is the mock recorder for MockfeedService +type MockfeedServiceMockRecorder struct { + mock *MockfeedService } -// NewMockfeed creates a new mock instance -func NewMockfeed(ctrl *gomock.Controller) *Mockfeed { - mock := &Mockfeed{ctrl: ctrl} - mock.recorder = &MockfeedMockRecorder{mock} +// NewMockfeedService creates a new mock instance +func NewMockfeedService(ctrl *gomock.Controller) *MockfeedService { + mock := &MockfeedService{ctrl: ctrl} + mock.recorder = &MockfeedServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (_m *Mockfeed) EXPECT() *MockfeedMockRecorder { +func (_m *MockfeedService) EXPECT() *MockfeedServiceMockRecorder { return _m.recorder } // CreateFeed mocks base method -func (_m *Mockfeed) CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) (string, error) { +func (_m *MockfeedService) CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) (string, error) { ret := _m.ctrl.Call(_m, "CreateFeed", req, identity) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) @@ -42,12 +43,12 @@ func (_m *Mockfeed) CreateFeed(req *api.CreateFeedRequest, identity *api.Identit } // CreateFeed indicates an expected call of CreateFeed -func (_mr *MockfeedMockRecorder) CreateFeed(arg0, arg1 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "CreateFeed", reflect.TypeOf((*Mockfeed)(nil).CreateFeed), arg0, arg1) +func (_mr *MockfeedServiceMockRecorder) CreateFeed(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "CreateFeed", reflect.TypeOf((*MockfeedService)(nil).CreateFeed), arg0, arg1) } // GetFeed mocks base method -func (_m *Mockfeed) GetFeed(hashId string) (*podcast.Podcast, error) { +func (_m *MockfeedService) GetFeed(hashId string) (*podcast.Podcast, error) { ret := _m.ctrl.Call(_m, "GetFeed", hashId) ret0, _ := ret[0].(*podcast.Podcast) ret1, _ := ret[1].(error) @@ -55,12 +56,12 @@ func (_m *Mockfeed) GetFeed(hashId string) (*podcast.Podcast, error) { } // GetFeed indicates an expected call of GetFeed -func (_mr *MockfeedMockRecorder) GetFeed(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "GetFeed", reflect.TypeOf((*Mockfeed)(nil).GetFeed), arg0) +func (_mr *MockfeedServiceMockRecorder) GetFeed(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "GetFeed", reflect.TypeOf((*MockfeedService)(nil).GetFeed), arg0) } // GetMetadata mocks base method -func (_m *Mockfeed) GetMetadata(hashId string) (*api.Feed, error) { +func (_m *MockfeedService) GetMetadata(hashId string) (*api.Feed, error) { ret := _m.ctrl.Call(_m, "GetMetadata", hashId) ret0, _ := ret[0].(*api.Feed) ret1, _ := ret[1].(error) @@ -68,6 +69,53 @@ func (_m *Mockfeed) GetMetadata(hashId string) (*api.Feed, error) { } // GetMetadata indicates an expected call of GetMetadata -func (_mr *MockfeedMockRecorder) GetMetadata(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "GetMetadata", reflect.TypeOf((*Mockfeed)(nil).GetMetadata), arg0) +func (_mr *MockfeedServiceMockRecorder) GetMetadata(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "GetMetadata", reflect.TypeOf((*MockfeedService)(nil).GetMetadata), arg0) +} + +// MockpatreonService is a mock of patreonService interface +type MockpatreonService struct { + ctrl *gomock.Controller + recorder *MockpatreonServiceMockRecorder +} + +// MockpatreonServiceMockRecorder is the mock recorder for MockpatreonService +type MockpatreonServiceMockRecorder struct { + mock *MockpatreonService +} + +// NewMockpatreonService creates a new mock instance +func NewMockpatreonService(ctrl *gomock.Controller) *MockpatreonService { + mock := &MockpatreonService{ctrl: ctrl} + mock.recorder = &MockpatreonServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (_m *MockpatreonService) EXPECT() *MockpatreonServiceMockRecorder { + return _m.recorder +} + +// Hook mocks base method +func (_m *MockpatreonService) Hook(pledge *patreon_go.Pledge, event string) error { + ret := _m.ctrl.Call(_m, "Hook", pledge, event) + ret0, _ := ret[0].(error) + return ret0 +} + +// Hook indicates an expected call of Hook +func (_mr *MockpatreonServiceMockRecorder) Hook(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Hook", reflect.TypeOf((*MockpatreonService)(nil).Hook), arg0, arg1) +} + +// GetFeatureLevel mocks base method +func (_m *MockpatreonService) GetFeatureLevel(patronID string) int { + ret := _m.ctrl.Call(_m, "GetFeatureLevel", patronID) + ret0, _ := ret[0].(int) + return ret0 +} + +// GetFeatureLevel indicates an expected call of GetFeatureLevel +func (_mr *MockpatreonServiceMockRecorder) GetFeatureLevel(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "GetFeatureLevel", reflect.TypeOf((*MockpatreonService)(nil).GetFeatureLevel), arg0) } diff --git a/pkg/handler/handler_test.go b/pkg/handler/handler_test.go index b0e04a3..0a1f516 100644 --- a/pkg/handler/handler_test.go +++ b/pkg/handler/handler_test.go @@ -29,10 +29,13 @@ func TestCreateFeed(t *testing.T) { Format: api.AudioFormat, } - feed := NewMockfeed(ctrl) + feed := NewMockfeedService(ctrl) feed.EXPECT().CreateFeed(gomock.Eq(req), gomock.Any()).Times(1).Return("456", nil) - srv := httptest.NewServer(New(feed, nil, cfg)) + patreon := NewMockpatreonService(ctrl) + patreon.EXPECT().GetFeatureLevel(gomock.Any()).Return(api.DefaultFeatures) + + srv := httptest.NewServer(New(feed, patreon, cfg)) defer srv.Close() query := `{"url": "https://youtube.com/channel/123", "page_size": 55, "quality": "low", "format": "audio"}` @@ -47,7 +50,7 @@ func TestCreateInvalidFeed(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - srv := httptest.NewServer(New(NewMockfeed(ctrl), nil, cfg)) + srv := httptest.NewServer(New(NewMockfeedService(ctrl), nil, cfg)) defer srv.Close() query := `{}` @@ -97,7 +100,7 @@ func TestGetFeed(t *testing.T) { podcast := itunes.New("", "", "", nil, nil) - feed := NewMockfeed(ctrl) + feed := NewMockfeedService(ctrl) feed.EXPECT().GetFeed("123").Return(&podcast, nil) srv := httptest.NewServer(New(feed, nil, cfg)) @@ -112,7 +115,7 @@ func TestGetMetadata(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - feed := NewMockfeed(ctrl) + feed := NewMockfeedService(ctrl) feed.EXPECT().GetMetadata("123").Times(1).Return(&api.Feed{}, nil) srv := httptest.NewServer(New(feed, nil, cfg)) diff --git a/pkg/models/pg.sql b/pkg/models/pg.sql index f05d0cd..f6f67b0 100644 --- a/pkg/models/pg.sql +++ b/pkg/models/pg.sql @@ -2,7 +2,7 @@ BEGIN; CREATE TABLE IF NOT EXISTS pledges ( pledge_id BIGSERIAL PRIMARY KEY, - patron_id BIGINT NOT NULL , + patron_id BIGINT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL, declined_since TIMESTAMPTZ NULL, amount_cents INT NOT NULL, diff --git a/pkg/webhook/hook.go b/pkg/support/patreon.go similarity index 59% rename from pkg/webhook/hook.go rename to pkg/support/patreon.go index d500d46..75e155a 100644 --- a/pkg/webhook/hook.go +++ b/pkg/support/patreon.go @@ -1,20 +1,26 @@ -package webhook +package support import ( "fmt" + "log" "strconv" "github.com/go-pg/pg" "github.com/mxpv/patreon-go" + "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/models" "github.com/pkg/errors" ) -type Handler struct { +const ( + creatorID = "2822191" +) + +type Patreon struct { db *pg.DB } -func (h Handler) toModel(pledge *patreon.Pledge) (*models.Pledge, error) { +func (h Patreon) toModel(pledge *patreon.Pledge) (*models.Pledge, error) { pledgeID, err := strconv.ParseInt(pledge.ID, 10, 64) if err != nil { return nil, errors.Wrapf(err, "failed to parse pledge id: %s", pledge.ID) @@ -56,7 +62,7 @@ func (h Handler) toModel(pledge *patreon.Pledge) (*models.Pledge, error) { return model, nil } -func (h Handler) Handle(pledge *patreon.Pledge, event string) error { +func (h Patreon) Hook(pledge *patreon.Pledge, event string) error { model, err := h.toModel(pledge) if err != nil { return err @@ -68,12 +74,48 @@ func (h Handler) Handle(pledge *patreon.Pledge, event string) error { case patreon.EventUpdatePledge: return h.db.Update(model) case patreon.EventDeletePledge: - return h.db.Delete(model) + err := h.db.Delete(model) + if err == pg.ErrNoRows { + return nil + } + + return err default: return fmt.Errorf("unknown event: %s", event) } } -func NewHookHandler(db *pg.DB) *Handler { - return &Handler{db: db} +func (h Patreon) FindPledge(patronID string) (*models.Pledge, error) { + p := &models.Pledge{} + return p, h.db.Model(p).Where("patron_id = ?", patronID).Limit(1).Select() +} + +func (h Patreon) GetFeatureLevel(patronID string) (level int) { + level = api.DefaultFeatures + + if patronID == creatorID { + level = api.PodcasterFeature + return + } + + pledge, err := h.FindPledge(patronID) + if err != nil { + log.Printf("! can't find pledge for user %s: %v", patronID, err) + return + } + + // Check pledge is valid + if pledge.DeclinedSince.IsZero() && !pledge.IsPaused { + // Check the amount of pledge + if pledge.AmountCents >= 100 { + level = api.ExtendedFeatures + return + } + } + + return +} + +func NewPatreon(db *pg.DB) *Patreon { + return &Patreon{db: db} } diff --git a/pkg/webhook/hook_test.go b/pkg/support/patreon_test.go similarity index 57% rename from pkg/webhook/hook_test.go rename to pkg/support/patreon_test.go index cb05e79..bd48233 100644 --- a/pkg/webhook/hook_test.go +++ b/pkg/support/patreon_test.go @@ -1,4 +1,4 @@ -package webhook +package support import ( "testing" @@ -6,6 +6,7 @@ import ( "github.com/go-pg/pg" "github.com/mxpv/patreon-go" + "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/models" "github.com/stretchr/testify/require" ) @@ -14,7 +15,7 @@ func TestCreate(t *testing.T) { pledge := createPledge() hook := createHandler(t) - err := hook.Handle(pledge, patreon.EventCreatePledge) + err := hook.Hook(pledge, patreon.EventCreatePledge) require.NoError(t, err) model := &models.Pledge{PledgeID: 12345} @@ -27,12 +28,12 @@ func TestUpdate(t *testing.T) { pledge := createPledge() hook := createHandler(t) - err := hook.Handle(pledge, patreon.EventCreatePledge) + err := hook.Hook(pledge, patreon.EventCreatePledge) require.NoError(t, err) pledge.Attributes.AmountCents = 999 - err = hook.Handle(pledge, patreon.EventUpdatePledge) + err = hook.Hook(pledge, patreon.EventUpdatePledge) require.NoError(t, err) model := &models.Pledge{PledgeID: 12345} @@ -45,14 +46,38 @@ func TestDelete(t *testing.T) { pledge := createPledge() hook := createHandler(t) - err := hook.Handle(pledge, patreon.EventCreatePledge) + err := hook.Hook(pledge, patreon.EventCreatePledge) require.NoError(t, err) - err = hook.Handle(pledge, patreon.EventDeletePledge) + err = hook.Hook(pledge, patreon.EventDeletePledge) require.NoError(t, err) } -func createHandler(t *testing.T) *Handler { +func TestFindPledge(t *testing.T) { + pledge := createPledge() + hook := createHandler(t) + + err := hook.Hook(pledge, patreon.EventCreatePledge) + require.NoError(t, err) + + res, err := hook.FindPledge("67890") + require.NoError(t, err) + require.Equal(t, res.AmountCents, pledge.Attributes.AmountCents) +} + +func TestGetFeatureLevel(t *testing.T) { + pledge := createPledge() + hook := createHandler(t) + + err := hook.Hook(pledge, patreon.EventCreatePledge) + require.NoError(t, err) + + require.Equal(t, api.PodcasterFeature, hook.GetFeatureLevel(creatorID)) + require.Equal(t, api.DefaultFeatures, hook.GetFeatureLevel("xyz")) + require.Equal(t, api.ExtendedFeatures, hook.GetFeatureLevel(pledge.Relationships.Patron.Data.ID)) +} + +func createHandler(t *testing.T) *Patreon { opts, err := pg.ParseURL("postgres://postgres:@localhost/podsync?sslmode=disable") if err != nil { require.NoError(t, err) @@ -63,7 +88,7 @@ func createHandler(t *testing.T) *Handler { _, err = db.Model(&models.Pledge{}).Where("1=1").Delete() require.NoError(t, err) - return NewHookHandler(db) + return NewPatreon(db) } func createPledge() *patreon.Pledge {