Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
cfg config.Config
db *gorm.DB
store *storage.SqlStorage
stores storage.Stores

log = logger.GetWithPackage("cmd")

Expand Down Expand Up @@ -43,6 +44,7 @@ func initConfig() {
log.WithError(err).Fatal("could not connect to database")
}
store = storage.NewSqlStorage(db, cfg.Upload.Path)
stores = storage.NewStores(store)
if err := storage.MigrateDB(db); err != nil {
log.WithError(err).Fatal("could not migrate database")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var (
log.Fatal(http.ListenAndServe(cfg.MetricsListen, nil))
}()

apiServer := api.API(cfg, store)
apiServer := api.API(cfg, stores)
server := &http.Server{
Addr: cfg.Listen,
Handler: apiServer.Router,
Expand Down
72 changes: 36 additions & 36 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,22 @@ type Server struct {

type handler struct {
config config.Config
storage storage.Storage
stores storage.Stores
bridges bridge.Bridges
cache *cache.Cache
realtime *realtime.Engine
}

func API(config config.Config, store storage.Storage) *Server {
func API(config config.Config, stores storage.Stores) *Server {
inMemoryCache := cache.NewCache(5 * time.Minute)

ws := realtime.New()
go ws.Run()

handler := handler{
config: config,
storage: store,
bridges: bridge.RegisterBridges(config, store),
stores: stores,
bridges: bridge.RegisterBridges(config, stores),
cache: inMemoryCache,
realtime: ws,
}
Expand All @@ -63,11 +63,11 @@ func API(config config.Config, store storage.Storage) *Server {
r.Use(limits.RequestSizeLimiter(1024 * 1024 * 10))

// the jwt middleware
authMiddleware := auth.AuthMiddleware(store, config.Secret)
authMiddleware := auth.AuthMiddleware(stores.Users, config.Secret)

admin := r.Group("/v1/admin")
{
meMiddleware := me.MeMiddleware(store)
meMiddleware := me.MeMiddleware(stores.Users)
admin.Use(authMiddleware.MiddlewareFunc())
admin.Use(meMiddleware)

Expand All @@ -76,39 +76,39 @@ func API(config config.Config, store storage.Storage) *Server {
admin.GET("/features", handler.GetFeatures)

admin.GET(`/tickers`, handler.GetTickers)
admin.GET(`/tickers/:tickerID`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.GetTicker)
admin.GET(`/tickers/:tickerID`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.GetTicker)
admin.POST(`/tickers`, user.NeedAdmin(), handler.PostTicker)
admin.PUT(`/tickers/:tickerID`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTicker)
admin.DELETE(`/tickers/:tickerID/websites`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerWebsites)
admin.PUT(`/tickers/:tickerID/websites`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerWebsites)
admin.PUT(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerTelegram)
admin.DELETE(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerTelegram)
admin.PUT(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerMastodon)
admin.DELETE(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerMastodon)
admin.PUT(`/tickers/:tickerID/bluesky`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerBluesky)
admin.DELETE(`/tickers/:tickerID/bluesky`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerBluesky)
admin.PUT(`/tickers/:tickerID/signal_group`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerSignalGroup)
admin.DELETE(`/tickers/:tickerID/signal_group`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerSignalGroup)
admin.PUT(`/tickers/:tickerID/signal_group/admin`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerSignalGroupAdmin)
admin.DELETE(`/tickers/:tickerID`, user.NeedAdmin(), ticker.PrefetchTicker(store), handler.DeleteTicker)
admin.PUT(`/tickers/:tickerID/reset`, ticker.PrefetchTicker(store, storage.WithPreload()), ticker.PrefetchTicker(store), handler.ResetTicker)
admin.GET(`/tickers/:tickerID/users`, ticker.PrefetchTicker(store), handler.GetTickerUsers)
admin.PUT(`/tickers/:tickerID/users`, user.NeedAdmin(), ticker.PrefetchTicker(store), handler.PutTickerUsers)
admin.DELETE(`/tickers/:tickerID/users/:userID`, user.NeedAdmin(), ticker.PrefetchTicker(store), handler.DeleteTickerUser)

admin.GET(`/tickers/:tickerID/messages`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.GetMessages)
admin.GET(`/tickers/:tickerID/messages/:messageID`, ticker.PrefetchTicker(store, storage.WithPreload()), message.PrefetchMessage(store), handler.GetMessage)
admin.POST(`/tickers/:tickerID/messages`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PostMessage)
admin.DELETE(`/tickers/:tickerID/messages/:messageID`, ticker.PrefetchTicker(store, storage.WithPreload()), message.PrefetchMessage(store), handler.DeleteMessage)
admin.PUT(`/tickers/:tickerID`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PutTicker)
admin.DELETE(`/tickers/:tickerID/websites`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.DeleteTickerWebsites)
admin.PUT(`/tickers/:tickerID/websites`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PutTickerWebsites)
admin.PUT(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PutTickerTelegram)
admin.DELETE(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.DeleteTickerTelegram)
admin.PUT(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PutTickerMastodon)
admin.DELETE(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.DeleteTickerMastodon)
admin.PUT(`/tickers/:tickerID/bluesky`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PutTickerBluesky)
admin.DELETE(`/tickers/:tickerID/bluesky`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.DeleteTickerBluesky)
admin.PUT(`/tickers/:tickerID/signal_group`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PutTickerSignalGroup)
admin.DELETE(`/tickers/:tickerID/signal_group`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.DeleteTickerSignalGroup)
admin.PUT(`/tickers/:tickerID/signal_group/admin`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PutTickerSignalGroupAdmin)
admin.DELETE(`/tickers/:tickerID`, user.NeedAdmin(), ticker.PrefetchTicker(stores.Tickers), handler.DeleteTicker)
admin.PUT(`/tickers/:tickerID/reset`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), ticker.PrefetchTicker(stores.Tickers), handler.ResetTicker)
admin.GET(`/tickers/:tickerID/users`, ticker.PrefetchTicker(stores.Tickers), handler.GetTickerUsers)
admin.PUT(`/tickers/:tickerID/users`, user.NeedAdmin(), ticker.PrefetchTicker(stores.Tickers), handler.PutTickerUsers)
admin.DELETE(`/tickers/:tickerID/users/:userID`, user.NeedAdmin(), ticker.PrefetchTicker(stores.Tickers), handler.DeleteTickerUser)

admin.GET(`/tickers/:tickerID/messages`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.GetMessages)
admin.GET(`/tickers/:tickerID/messages/:messageID`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), message.PrefetchMessage(stores.Messages), handler.GetMessage)
admin.POST(`/tickers/:tickerID/messages`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), handler.PostMessage)
admin.DELETE(`/tickers/:tickerID/messages/:messageID`, ticker.PrefetchTicker(stores.Tickers, storage.WithPreload()), message.PrefetchMessage(stores.Messages), handler.DeleteMessage)

admin.POST(`/upload`, handler.PostUpload)

admin.GET(`/users`, user.NeedAdmin(), handler.GetUsers)
admin.GET(`/users/:userID`, user.PrefetchUser(store), handler.GetUser)
admin.GET(`/users/:userID`, user.PrefetchUser(stores.Users), handler.GetUser)
admin.POST(`/users`, user.NeedAdmin(), handler.PostUser)
admin.PUT(`/users/me`, handler.PutMe)
admin.PUT(`/users/:userID`, user.NeedAdmin(), user.PrefetchUser(store), handler.PutUser)
admin.DELETE(`/users/:userID`, user.NeedAdmin(), user.PrefetchUser(store), handler.DeleteUser)
admin.PUT(`/users/:userID`, user.NeedAdmin(), user.PrefetchUser(stores.Users), handler.PutUser)
admin.DELETE(`/users/:userID`, user.NeedAdmin(), user.PrefetchUser(stores.Users), handler.DeleteUser)

admin.GET(`/settings/:name`, user.NeedAdmin(), handler.GetSetting)
admin.PUT(`/settings/inactive_settings`, user.NeedAdmin(), handler.PutInactiveSettings)
Expand All @@ -121,10 +121,10 @@ func API(config config.Config, store storage.Storage) *Server {
public.POST(`/admin/login`, authMiddleware.LoginHandler)

public.GET(`/init`, response_cache.CachePage(inMemoryCache, 5*time.Minute, handler.GetInit))
public.GET(`/manifest.json`, ticker.PrefetchTickerFromRequest(store), handler.HandleManifest)
public.GET(`/timeline`, ticker.PrefetchTickerFromRequest(store), response_cache.CachePage(inMemoryCache, 10*time.Second, handler.GetTimeline))
public.GET(`/feed`, ticker.PrefetchTickerFromRequest(store), response_cache.CachePage(inMemoryCache, 5*time.Minute, handler.GetFeed))
public.GET(`/ws`, ticker.PrefetchTickerFromRequest(store), handler.HandleWebSocket)
public.GET(`/manifest.json`, ticker.PrefetchTickerFromRequest(stores.Tickers), handler.HandleManifest)
public.GET(`/timeline`, ticker.PrefetchTickerFromRequest(stores.Tickers), response_cache.CachePage(inMemoryCache, 10*time.Second, handler.GetTimeline))
public.GET(`/feed`, ticker.PrefetchTickerFromRequest(stores.Tickers), response_cache.CachePage(inMemoryCache, 5*time.Minute, handler.GetFeed))
public.GET(`/ws`, ticker.PrefetchTickerFromRequest(stores.Tickers), handler.HandleWebSocket)
}

r.GET(`/media/:fileName`, handler.GetMedia)
Expand Down
20 changes: 10 additions & 10 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ func (s *APITestSuite) SetupTest() {
logrus.SetOutput(io.Discard)

s.cfg = config.LoadConfig("")
s.store = &storage.MockStorage{}
s.store = storage.NewMockStorage()
}

func (s *APITestSuite) TestHealthz() {
server := API(s.cfg, s.store)
server := API(s.cfg, s.store.Stores())
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
w := httptest.NewRecorder()
server.Router.ServeHTTP(w, req)
Expand All @@ -47,8 +47,8 @@ func (s *APITestSuite) TestLogin() {
s.Run("when password is wrong", func() {
user, err := storage.NewUser("user@systemli.org", "password")
s.NoError(err)
s.store.On("FindUserByEmail", mock.Anything, mock.Anything).Return(user, nil)
server := API(s.cfg, s.store)
s.store.Users.On("FindUserByEmail", mock.Anything, mock.Anything).Return(user, nil)
server := API(s.cfg, s.store.Stores())

body := `{"username":"louis@systemli.org","password":"WRONG"}`
req := httptest.NewRequest(http.MethodPost, "/v1/admin/login", strings.NewReader(body))
Expand All @@ -69,9 +69,9 @@ func (s *APITestSuite) TestLogin() {
s.Run("when login is successful", func() {
user, err := storage.NewUser("user@systemli.org", "password")
s.NoError(err)
s.store.On("FindUserByEmail", mock.Anything, mock.Anything).Return(user, nil)
s.store.On("SaveUser", mock.Anything).Return(nil)
server := API(s.cfg, s.store)
s.store.Users.On("FindUserByEmail", mock.Anything, mock.Anything).Return(user, nil)
s.store.Users.On("SaveUser", mock.Anything).Return(nil)
server := API(s.cfg, s.store.Stores())

body := `{"username":"louis@systemli.org","password":"password"}`
req := httptest.NewRequest(http.MethodPost, "/v1/admin/login", strings.NewReader(body))
Expand All @@ -96,10 +96,10 @@ func (s *APITestSuite) TestLogin() {
s.Run("when save user fails", func() {
user, err := storage.NewUser("user@systemli.org", "password")
s.NoError(err)
s.store.On("FindUserByEmail", mock.Anything, mock.Anything).Return(user, nil)
s.store.On("SaveUser", mock.Anything).Return(errors.New("failed to save user"))
s.store.Users.On("FindUserByEmail", mock.Anything, mock.Anything).Return(user, nil)
s.store.Users.On("SaveUser", mock.Anything).Return(errors.New("failed to save user"))

server := API(s.cfg, s.store)
server := API(s.cfg, s.store.Stores())

body := `{"username":"louis@systemli.org","password":"password"}`
req := httptest.NewRequest(http.MethodPost, "/v1/admin/login", strings.NewReader(body))
Expand Down
8 changes: 4 additions & 4 deletions internal/api/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import (

type FeaturesResponse map[string]bool

func NewFeaturesResponse(storage storage.Storage) FeaturesResponse {
telegramSettings := storage.GetTelegramSettings()
signalGroupSettings := storage.GetSignalGroupSettings()
func NewFeaturesResponse(settings storage.SettingsStore) FeaturesResponse {
telegramSettings := storage.GetSettings(settings, storage.TelegramSetting)
signalGroupSettings := storage.GetSettings(settings, storage.SignalGroupSetting)
return FeaturesResponse{
"telegramEnabled": telegramSettings.Token != "",
"signalGroupEnabled": signalGroupSettings.Enabled(),
}
}

func (h *handler) GetFeatures(c *gin.Context) {
features := NewFeaturesResponse(h.storage)
features := NewFeaturesResponse(h.stores.Settings)
c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"features": features}))
}
10 changes: 5 additions & 5 deletions internal/api/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ func (s *FeaturesTestSuite) SetupTest() {
func (s *FeaturesTestSuite) TestGetFeatures() {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
store := &storage.MockStorage{}
store := storage.NewMockStorage()

// Mock GetTelegramSettings to return empty token (disabled)
store.On("GetTelegramSettings").Return(storage.TelegramSettings{Token: ""})
store.Settings.MockGetTelegram(storage.TelegramSettings{Token: ""})
// Mock GetSignalGroupSettings to return empty settings (disabled)
store.On("GetSignalGroupSettings").Return(storage.DefaultSignalGroupSettings())
store.Settings.MockGetSignalGroup(storage.DefaultSignalGroupSettings())

h := handler{
storage: store,
config: config.LoadConfig(""),
stores: store.Stores(),
config: config.LoadConfig(""),
}

h.GetFeatures(c)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (h *handler) GetFeed(c *gin.Context) {
}

pagination := pagination.NewPagination(c)
messages, err := h.storage.FindMessagesByTickerAndPagination(ticker, *pagination)
messages, err := h.stores.Messages.FindMessagesByTickerAndPagination(ticker, *pagination)
if err != nil {
c.JSON(http.StatusOK, response.ErrorResponse(response.CodeDefault, response.MessageFetchError))
return
Expand Down
10 changes: 5 additions & 5 deletions internal/api/feed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (s *FeedTestSuite) SetupTest() {

s.w = httptest.NewRecorder()
s.ctx, _ = gin.CreateTestContext(s.w)
s.store = &storage.MockStorage{}
s.store = storage.NewMockStorage()
s.cfg = config.LoadConfig("")
}

Expand All @@ -43,7 +43,7 @@ func (s *FeedTestSuite) TestGetFeed() {

s.Run("when fetching messages fails", func() {
s.ctx.Set("ticker", storage.Ticker{})
s.store.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, errors.New("storage error")).Once()
s.store.Messages.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, errors.New("storage error")).Once()

h := s.handler()
h.GetFeed(s.ctx)
Expand All @@ -69,7 +69,7 @@ func (s *FeedTestSuite) TestGetFeed() {
TickerID: ticker.ID,
Text: "Text",
}
s.store.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{message}, nil).Once()
s.store.Messages.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{message}, nil).Once()

h := s.handler()
h.GetFeed(s.ctx)
Expand All @@ -81,8 +81,8 @@ func (s *FeedTestSuite) TestGetFeed() {

func (s *FeedTestSuite) handler() handler {
return handler{
storage: s.store,
config: s.cfg,
stores: s.store.Stores(),
config: s.cfg,
}
}

Expand Down
5 changes: 3 additions & 2 deletions internal/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/systemli/ticker/internal/api/helper"
"github.com/systemli/ticker/internal/api/response"
"github.com/systemli/ticker/internal/storage"
)

func (h *handler) GetInit(c *gin.Context) {
Expand All @@ -16,9 +17,9 @@ func (h *handler) GetInit(c *gin.Context) {
return
}

ticker, err := h.storage.FindTickerByOrigin(origin)
ticker, err := h.stores.Tickers.FindTickerByOrigin(origin)
if err != nil || !ticker.Active {
settings.InactiveSettings = h.storage.GetInactiveSettings()
settings.InactiveSettings = storage.GetSettings(h.stores.Settings, storage.InactiveSetting)
c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": nil, "settings": settings}))
return
}
Expand Down
Loading
Loading