diff --git a/README.md b/README.md index 390a31d..810b20a 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ All parameters can be overwritten by ENV variable. "path": "host=myhost port=myport user=gorm dbname=gorm password=mypassword" }, "loglevel": "trace", - "metrics": false -} + "metrics": false, ``` - `API_ENDPOINT` marudor endpoint diff --git a/cmd/bot.go b/cmd/bot.go index f828070..f4ecf30 100644 --- a/cmd/bot.go +++ b/cmd/bot.go @@ -11,14 +11,16 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/pkuebler/bahn-bot/pkg/application" "github.com/pkuebler/bahn-bot/pkg/config" "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" - "github.com/pkuebler/bahn-bot/pkg/infrastructure/repository" "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" "github.com/pkuebler/bahn-bot/pkg/interface/cron" "github.com/pkuebler/bahn-bot/pkg/interface/telegram" "github.com/pkuebler/bahn-bot/pkg/metrics" + trainalarmApplication "github.com/pkuebler/bahn-bot/pkg/trainalarms/application" + trainalarmRepository "github.com/pkuebler/bahn-bot/pkg/trainalarms/repository" + webhookApplication "github.com/pkuebler/bahn-bot/pkg/webhooks/application" + webhookRepository "github.com/pkuebler/bahn-bot/pkg/webhooks/repository" ) // NewBotCmd create a command to start the bot @@ -66,16 +68,19 @@ func BotCommand(ctx context.Context, cmd *cobra.Command, args []string) { hafas := api.HafasService // storage - // repo := repository.NewMemoryDatabase() - repo := repository.NewSQLDatabase(cfg.Database.Dialect, cfg.Database.Path) - defer repo.Close() + // repo := trainalarmRepository.NewMemoryDatabase() + trainalarmRepo := trainalarmRepository.NewSQLDatabase(cfg.Database.Dialect, cfg.Database.Path) + defer trainalarmRepo.Close() + webhookRepo := webhookRepository.NewSQLDatabase(cfg.Database.Dialect, cfg.Database.Path) + defer webhookRepo.Close() // application - app := application.NewApplication(hafas, repo, log) + trainalarmApp := trainalarmApplication.NewApplication(hafas, trainalarmRepo, log) + webhookApp := webhookApplication.NewApplication(trainalarmRepo, webhookRepo, log) // interfaces - service := telegram.NewTelegramService(log, repo, app, hafas) - cronService := cron.NewCronJob(log, app, cfg.EnableMetrics) + service := telegram.NewTelegramService(log, cfg, trainalarmRepo, webhookRepo, webhookApp, trainalarmApp, hafas) + cronService := cron.NewCronJob(log, trainalarmApp, cfg.EnableMetrics) // conversationengine router := telegramconversation.NewConversationRouter("start") @@ -89,6 +94,14 @@ func BotCommand(ctx context.Context, cmd *cobra.Command, args []string) { router.OnState("start", service.Start) router.OnState("cancel", service.Cancel) + router.OnCommand("webhooks", "listwebhooks") + router.OnState("listwebhooks", service.Webhooks) + router.OnState("webhook", service.WebhookMenu) + router.OnState("deletewebhook", service.DeleteWebhook) + + router.OnCommand("newwebhook", "newwebhook") + router.OnState("newwebhook", service.NewWebhook) + router.OnCommand("myalarms", "listalarms") router.OnState("listalarms", service.ListTrainAlarms) router.OnState("alarm", service.AlarmMenu) @@ -149,7 +162,7 @@ func BotCommand(ctx context.Context, cmd *cobra.Command, args []string) { tctx.SetMessage(update.Message.Text) - if state, payload, err := repo.GetState(ctx, tctx.ChatID(), "telegram"); err == nil { + if state, payload, err := trainalarmRepo.GetState(ctx, tctx.ChatID(), "telegram"); err == nil { log.Tracef("[%d] Load State %s (Payload: %s)", tctx.MessageID(), state, payload) tctx.SetStatePayload(payload) tctx.ChangeState(state) @@ -212,7 +225,7 @@ func BotCommand(ctx context.Context, cmd *cobra.Command, args []string) { bot.DeleteMessage(tgbotapi.NewDeleteMessage(tctx.ChatID64(), tctx.DeletedMessageID())) } - repo.UpdateState(ctx, tctx.ChatID(), "telegram", func(state string, payload string) (string, string, error) { + trainalarmRepo.UpdateState(ctx, tctx.ChatID(), "telegram", func(state string, payload string) (string, string, error) { msgLog.Tracef("Save State %s with payload `%s` (old: %s / `%s`)", tctx.State(), tctx.StatePayload(), state, payload) return tctx.State(), tctx.StatePayload(), nil }) diff --git a/docker-compose.yml b/docker-compose.yml index d234b10..5682d35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,8 +15,8 @@ services: - POSTGRES_USER=bahn-bot - POSTGRES_PASSWORD=supersecretpassword - POSTGRES_DB=bahn-bot - volumes: - - ./data:/var/lib/postgresql/data +# volumes: +# - ./data:/var/lib/postgresql/data # mysql: # image: mysql:5.7 diff --git a/pkg/config/config.go b/pkg/config/config.go index 67cab07..0c1cc9e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -24,6 +24,12 @@ type DatabaseConfig struct { Path string `env:"DB_PATH" json:"path"` } +// WebhookConfig connection data +type WebhookConfig struct { + Endpoint string `env:"WEBHOOK_ENDPOINT" json:"endpoint"` + Port string `env:"WEBHOOK_PORT" json:"port"` +} + // Config contains the complete service configuration type Config struct { APIConfig APIConfig `json:"api"` @@ -31,6 +37,7 @@ type Config struct { Telegram TelegramConfig `json:"telegram"` LogLevel string `env:"LOG_LEVEL" json:"loglevel"` EnableMetrics bool `json:"enable_metrics"` + Webhook WebhookConfig `json:"webhook"` } // NewTestConfig return a config object with test settings @@ -40,6 +47,9 @@ func NewTestConfig() *Config { APIEndpoint: "https://marudor.de/api", }, LogLevel: "trace", + Webhook: WebhookConfig{ + Endpoint: "http://localhost/hook", + }, } } @@ -73,6 +83,14 @@ func ReadConfig(file string, log *logrus.Entry) *Config { config.APIConfig.APIEndpoint = "https://marudor.de/api" } + if config.Webhook.Endpoint == "" { + panic("Need WEBHOOK_ENDPOINT") + } + + if config.Webhook.Port == "" { + config.Webhook.Port = ":8080" + } + return &config } diff --git a/pkg/interface/cron/cron.go b/pkg/interface/cron/cron.go index b656c86..03ce720 100644 --- a/pkg/interface/cron/cron.go +++ b/pkg/interface/cron/cron.go @@ -5,11 +5,12 @@ import ( "fmt" "time" - "github.com/pkuebler/bahn-bot/pkg/application" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" + "github.com/sirupsen/logrus" + "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" - "github.com/sirupsen/logrus" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/application" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // CronJob triggers applications @@ -67,7 +68,7 @@ func (c *CronJob) ClearDatabase(ctx context.Context) { // NotifyUsers about train delays func (c *CronJob) NotifyUsers(ctx context.Context) { - c.application.NotifyUsers(ctx, func(ctx context.Context, alarm *trainalarm.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error { + c.application.NotifyUsers(ctx, func(ctx context.Context, alarm *domain.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error { tctx := telegramconversation.NewTContext(alarm.GetIdentifyer()) if c.metrics != nil { diff --git a/pkg/interface/telegram/deletealarm.go b/pkg/interface/telegram/deletealarm.go index 3007041..b7bd9bb 100644 --- a/pkg/interface/telegram/deletealarm.go +++ b/pkg/interface/telegram/deletealarm.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/pkuebler/bahn-bot/pkg/application" "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/application" ) // DeleteAlarm from database @@ -22,7 +22,7 @@ func (t *TelegramService) DeleteAlarm(ctx telegramconversation.TContext) telegra cmd := application.DeleteTrainAlarmCmd{ AlarmID: ctx.ButtonData(), } - alarm, _ := t.application.DeleteTrainAlarm(context.Background(), cmd) + alarm, _ := t.trainalarmApp.DeleteTrainAlarm(context.Background(), cmd) return ctx.SendWithState(fmt.Sprintf("Alarm `%s > %s` gelöscht.", alarm.GetTrainName(), alarm.GetFinalDestinationName()), "start") } diff --git a/pkg/interface/telegram/deletewebhook.go b/pkg/interface/telegram/deletewebhook.go new file mode 100644 index 0000000..b58cfd7 --- /dev/null +++ b/pkg/interface/telegram/deletewebhook.go @@ -0,0 +1,28 @@ +package telegram + +import ( + "context" + "fmt" + + "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" + "github.com/pkuebler/bahn-bot/pkg/webhooks/application" +) + +// DeleteWebhook from database +func (t *TelegramService) DeleteWebhook(ctx telegramconversation.TContext) telegramconversation.TContext { + log := ctx.LogFields(t.log) + log.Trace("DeleteWebhook()") + + if !ctx.IsButtonPressed() { + return ctx.SendWithState("Irgendetwas ist schief gelaufen. :/", "start") + } + + ctx.DeleteMessage(ctx.MessageID()) + + cmd := application.DeleteWebhookCmd{ + WebhookID: ctx.ButtonData(), + } + hook, _ := t.webhookApp.DeleteWebhook(context.Background(), cmd) + + return ctx.SendWithState(fmt.Sprintf("Webhook `%s: %s` gelöscht.", hook.GetProtocol(), hook.GetURLHash()), "start") +} diff --git a/pkg/interface/telegram/listtrainalarms.go b/pkg/interface/telegram/listtrainalarms.go index 878f035..560accf 100644 --- a/pkg/interface/telegram/listtrainalarms.go +++ b/pkg/interface/telegram/listtrainalarms.go @@ -24,9 +24,6 @@ func (t *TelegramService) ListTrainAlarms(ctx telegramconversation.TContext) tel } if len(alarms) == 0 { - if ctx.IsButtonPressed() { - ctx.DeleteMessage(ctx.MessageID()) - } return ctx.SendWithState("Du beobachtest noch keine Züge. /help", "start") } diff --git a/pkg/interface/telegram/newwebhook.go b/pkg/interface/telegram/newwebhook.go new file mode 100644 index 0000000..702e196 --- /dev/null +++ b/pkg/interface/telegram/newwebhook.go @@ -0,0 +1,39 @@ +package telegram + +import ( + "context" + "fmt" + "net/url" + "path" + + "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" + "github.com/pkuebler/bahn-bot/pkg/webhooks/application" + "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +// NewWebhook generates a webhook that Travelynx uses to automatically set alarms for current trains. +func (t *TelegramService) NewWebhook(ctx telegramconversation.TContext) telegramconversation.TContext { + log := ctx.LogFields(t.log) + log.Trace("NewWebhook()") + + cmd := application.AddWebhookCmd{ + Identifyer: ctx.ChatID(), + Plattform: "telegram", + Protocol: string(domain.TravelynxProtocol), + } + + hook, err := t.webhookApp.AddWebhook(context.Background(), cmd) + if err != nil { + log.Error(err) + return ctx.SendWithState("Irgendetwas ist schief gelaufen. :/", "start") + } + + u, _ := url.Parse(t.config.Webhook.Endpoint) + u.Path = path.Join(u.Path, hook.GetURLHash()) + txt := fmt.Sprintf(`Neuer Webhook angelegt. + + Um automatisch Alarme angelegt zu bekommen, müssen unter https://travelynx.de/account/hooks folgende Daten hinterlegt werden: + + URL: `+"`%s`", u.String()) + return ctx.SendWithState(txt, "start") +} diff --git a/pkg/interface/telegram/savealarm.go b/pkg/interface/telegram/savealarm.go index 4d18446..1e265e0 100644 --- a/pkg/interface/telegram/savealarm.go +++ b/pkg/interface/telegram/savealarm.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/pkuebler/bahn-bot/pkg/application" "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/application" ) // SaveAlarm to database @@ -36,7 +36,7 @@ func (t *TelegramService) SaveAlarm(ctx telegramconversation.TContext) telegramc StationDate: stationDate, } - err = t.application.AddTrainAlarm(context.Background(), cmd) + err = t.trainalarmApp.AddTrainAlarm(context.Background(), cmd) if err != nil { log.Error(err) return ctx.SendWithState("Irgendetwas ist schief gelaufen. :/", "start") diff --git a/pkg/interface/telegram/savedelay.go b/pkg/interface/telegram/savedelay.go index 70b8d1d..834bf42 100644 --- a/pkg/interface/telegram/savedelay.go +++ b/pkg/interface/telegram/savedelay.go @@ -7,8 +7,8 @@ import ( "strconv" "time" - "github.com/pkuebler/bahn-bot/pkg/application" "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/application" ) var ( @@ -52,7 +52,7 @@ func (t *TelegramService) SaveDelay(ctx telegramconversation.TContext) telegramc ThresholdMinutes: int(thresholdMinutes.Minutes()), } - err = t.application.UpdateTrainAlarmThreshold(context.Background(), cmd) + err = t.trainalarmApp.UpdateTrainAlarmThreshold(context.Background(), cmd) if err != nil { log.Error(err) return ctx.SendWithState("Irgendetwas ist schief gelaufen. :/", "start") diff --git a/pkg/interface/telegram/start.go b/pkg/interface/telegram/start.go index fb7fcf7..f2516d7 100644 --- a/pkg/interface/telegram/start.go +++ b/pkg/interface/telegram/start.go @@ -24,6 +24,9 @@ func (t *TelegramService) Start(ctx telegramconversation.TContext) telegramconve /myalarms Bearbeite deine Alarme /newalarm Erzeuge neuen Alarm +/webhooks Bearbeite deine Webhooks +/newwebhook Verknüpfe travelynx.de + /cancel Breche aktuelle Option ab `) ctx.Send(txt) diff --git a/pkg/interface/telegram/telegram.go b/pkg/interface/telegram/telegram.go index a97c782..1d0733f 100644 --- a/pkg/interface/telegram/telegram.go +++ b/pkg/interface/telegram/telegram.go @@ -7,17 +7,23 @@ import ( "strings" "time" - "github.com/pkuebler/bahn-bot/pkg/application" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" - "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" "github.com/sirupsen/logrus" + + "github.com/pkuebler/bahn-bot/pkg/config" + "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" + trainalarmApplication "github.com/pkuebler/bahn-bot/pkg/trainalarms/application" + trainalarmDomain "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" + webhookApplication "github.com/pkuebler/bahn-bot/pkg/webhooks/application" + webhookDomain "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" ) // Application with business logic type Application interface { - DeleteTrainAlarm(ctx context.Context, cmd application.DeleteTrainAlarmCmd) (*trainalarm.TrainAlarm, error) - AddTrainAlarm(ctx context.Context, cmd application.AddTrainAlarmCmd) error - UpdateTrainAlarmThreshold(ctx context.Context, cmd application.UpdateTrainAlarmThresholdCmd) error + DeleteTrainAlarm(ctx context.Context, cmd trainalarmApplication.DeleteTrainAlarmCmd) (*trainalarmDomain.TrainAlarm, error) + AddTrainAlarm(ctx context.Context, cmd trainalarmApplication.AddTrainAlarmCmd) error + UpdateTrainAlarmThreshold(ctx context.Context, cmd trainalarmApplication.UpdateTrainAlarmThresholdCmd) error + AddWebhook(ctx context.Context, cmd webhookApplication.AddWebhookCmd) (*webhookDomain.Webhook, error) + DeleteWebhook(ctx context.Context, cmd webhookApplication.DeleteWebhookCmd) (*webhookDomain.Webhook, error) } // HafasService to request train informations @@ -28,17 +34,31 @@ type HafasService interface { // TelegramService to handle requests type TelegramService struct { log *logrus.Entry - trainAlarmRepository trainalarm.Repository - application Application + config *config.Config + trainAlarmRepository trainalarmDomain.Repository + trainalarmApp *trainalarmApplication.Application + webhookRepository webhookDomain.Repository + webhookApp *webhookApplication.Application hafas HafasService } // NewTelegramService to create a new service -func NewTelegramService(log *logrus.Entry, repository trainalarm.Repository, application Application, hafas HafasService) *TelegramService { +func NewTelegramService( + log *logrus.Entry, + cfg *config.Config, + trainAlarmRepository trainalarmDomain.Repository, + webhookRepository webhookDomain.Repository, + webhookApp *webhookApplication.Application, + trainalarmApp *trainalarmApplication.Application, + hafas HafasService, +) *TelegramService { return &TelegramService{ log: log.WithField("service", "telegram"), - trainAlarmRepository: repository, - application: application, + config: cfg, + trainAlarmRepository: trainAlarmRepository, + trainalarmApp: trainalarmApp, + webhookRepository: webhookRepository, + webhookApp: webhookApp, hafas: hafas, } } diff --git a/pkg/interface/telegram/webhookmenu.go b/pkg/interface/telegram/webhookmenu.go new file mode 100644 index 0000000..afec63e --- /dev/null +++ b/pkg/interface/telegram/webhookmenu.go @@ -0,0 +1,35 @@ +package telegram + +import ( + "context" + "fmt" + + "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" +) + +// WebhookMenu to select webhook options +func (t *TelegramService) WebhookMenu(ctx telegramconversation.TContext) telegramconversation.TContext { + log := ctx.LogFields(t.log) + log.Trace("WebhookMenu()") + + if !ctx.IsButtonPressed() { + ctx.ChangeState("start") + return ctx + } + + ctx.DeleteMessage(ctx.MessageID()) + + hook, err := t.webhookRepository.GetWebhook(context.Background(), ctx.ButtonData()) + if err != nil { + log.Error(err) + return ctx.SendWithState("Webhook nicht gefunden.", "start") + } + + txt := fmt.Sprintf("Was möchtest du für `%s: %s` ändern?", hook.GetProtocol(), hook.GetURLHash()) + buttons := []telegramconversation.TButton{ + telegramconversation.NewTButton("Löschen", fmt.Sprintf("deletewebhook|%s", hook.GetID())), + telegramconversation.NewTButton("Zurück zur Liste", "listwebhooks"), + } + + return ctx.SendWithKeyboard(txt, buttons, 2) +} diff --git a/pkg/interface/telegram/webhooks.go b/pkg/interface/telegram/webhooks.go new file mode 100644 index 0000000..d7cba56 --- /dev/null +++ b/pkg/interface/telegram/webhooks.go @@ -0,0 +1,43 @@ +package telegram + +import ( + "context" + "fmt" + + "github.com/pkuebler/bahn-bot/pkg/infrastructure/telegramconversation" +) + +// Webhooks managed webhook +func (t *TelegramService) Webhooks(ctx telegramconversation.TContext) telegramconversation.TContext { + log := ctx.LogFields(t.log) + log.Trace("Webhooks()") + + if ctx.IsButtonPressed() { + ctx.DeleteMessage(ctx.MessageID()) + } + + webhooks, err := t.webhookRepository.GetWebhooksByIdentifyer(context.Background(), ctx.ChatID(), "telegram") + if err != nil { + log.Error(err) + return ctx.SendWithState("Irgendetwas ist schief gelaufen. :/", "start") + } + + if len(webhooks) == 0 { + return ctx.SendWithState(`Du hast noch keine Webhooks angelegt. + +Über den Webhook können automatisiert Alarme verwaltet werden, wenn z.B. über die Plattform travelynx.de eine aktive Zugfahrt getrackt wird. + +/help`, "start") + } + + txt := "Welcher Webhook soll bearbeitet werden?" + buttons := []telegramconversation.TButton{} + for _, hook := range webhooks { + hookName := fmt.Sprintf("%s: %s", hook.GetProtocol(), hook.GetURLHash()) + button := telegramconversation.NewTButton(hookName, fmt.Sprintf("webhook|%s", hook.GetID())) + buttons = append(buttons, button) + } + buttons = append(buttons, telegramconversation.NewTButton("Abbruch", "cancel")) + + return ctx.SendWithKeyboard(txt, buttons, 2) +} diff --git a/pkg/interface/webhook/travelynx.go b/pkg/interface/webhook/travelynx.go new file mode 100644 index 0000000..3a722ca --- /dev/null +++ b/pkg/interface/webhook/travelynx.go @@ -0,0 +1,46 @@ +package webhook + +// TravelynxWebhook message container +type TravelynxWebhook struct { + Reason string `json:"reason"` + Status TravelynxStatus `json:"status"` +} + +// TravelynxStatus of the application +type TravelynxStatus struct { + ActionTime int64 `json:"actionTime"` + CheckedIn bool `json:"checkedIn"` + Deprecated bool `json:"deprecated"` + FromStation TravelynxStation `json:"fromStation"` + IntermediateStops []TravelynxStop `json:"intermediateStops"` + ToStation TravelynxStation `json:"toStation"` + Train TravelynxTrain `json:"train"` +} + +// TravelynxStation on start or end +type TravelynxStation struct { + DS100 *string `json:"ds100"` + Latitude *float32 `json:"latitude"` + Longitude *float32 `json:"longitude"` + Name *string `json:"name"` + RealTime int64 `json:"realTime"` + ScheduledTime int64 `json:"scheduledTime"` + UIC *int64 `json:"uic"` +} + +// TravelynxStop on journey +type TravelynxStop struct { + Name string `json:"name"` + RealArrival *int64 `json:"realArrival"` + RealDeparture *int64 `json:"realDeparture"` + ScheduledArrival *int64 `json:"scheduledArrival"` + ScheduledDeparture *int64 `json:"scheduledDeparture"` +} + +// TravelynxTrain info +type TravelynxTrain struct { + ID string `json:"id"` + Line *string `json:"line"` + No string `json:"no"` + Type string `json:"type"` +} diff --git a/pkg/interface/webhook/webhook.go b/pkg/interface/webhook/webhook.go new file mode 100644 index 0000000..c1b7ac3 --- /dev/null +++ b/pkg/interface/webhook/webhook.go @@ -0,0 +1,118 @@ +package webhook + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/sirupsen/logrus" + + "github.com/pkuebler/bahn-bot/pkg/config" + trainalarmDomain "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" + "github.com/pkuebler/bahn-bot/pkg/webhooks/application" + webhookDomain "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +// Webhook endpoint +type Webhook struct { + application *application.Application + webhookRepo webhookDomain.Repository + trainalarmRepo trainalarmDomain.Repository + log *logrus.Entry + config *config.Config +} + +// NewWebhook endpoint to recive updates +func NewWebhook( + application *application.Application, + webhookRepo webhookDomain.Repository, + trainalarmRepo trainalarmDomain.Repository, + log *logrus.Entry, + cfg *config.Config +) *Webhook { + return &Webhook{ + application: application, + webhookRepo: webhookRepo, + trainalarmRepo: trainalarmRepo, + log: log, + config: cfg, + } +} + +// Start listener +func (w *Webhook) Start(ctx context.Context) { + w.log.Info("start webhook listener") + + server := &http.Server{Addr: w.config.Webhook.Port} + + u, _ := url.Parse(w.config.Webhook.Endpoint) + + w.log.Info("listen %d/%s", w.config.Webhook.Port, u.Path) + http.HandleFunc(u.Path, func(resp http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + defer func() { + r := recover() + if r != nil { + var err error + switch t := r.(type) { + case string: + err = errors.New(t) + case error: + err = t + default: + err = errors.New("Unknown error") + } + fmt.Println(err.Error()) + http.Error(resp, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }() + + parts := strings.Split(r.URL.Path, "/") + + // find hash at database + hook, err := w.webhookRepo.GetWebhookByURLHash(r.Context(), parts[len(parts)-1]) + if err != nil { + fmt.Println(err.Error()) + http.Error(resp, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + if hook == nil { + http.Error(resp, http.StatusText(http.StatusNotFound), http.StatusNotFound) + } + + // json unmarschal body + msg := TravelynxWebhook{} + if err := json.NewDecoder(r.Body).Decode(&msg); err != nil { + fmt.Println(err.Error()) + http.Error(resp, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + // get current alarms + alarms, err := w.trainalarmRepo.GetTrainAlarms(ctx, hook.GetIdentifyer(), hook.GetPlattform()) + if err != nil { + fmt.Println(err.Error()) + http.Error(resp, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + // train.Train.Type, train.Train.Number + // cancel webhook alarm or search train and cancel? + + // create cmd + + // application.AddAlarm + // application.RemoveAlarm + }) + + go server.ListenAndServe() + + <-ctx.Done() + + if err := server.Shutdown(context.Background()); err != nil { + panic(err) + } +} diff --git a/pkg/application/addtrainalarm.go b/pkg/trainalarms/application/addtrainalarm.go similarity index 93% rename from pkg/application/addtrainalarm.go rename to pkg/trainalarms/application/addtrainalarm.go index 64a8c75..00a7065 100644 --- a/pkg/application/addtrainalarm.go +++ b/pkg/trainalarms/application/addtrainalarm.go @@ -5,8 +5,9 @@ import ( "errors" "time" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" "github.com/sirupsen/logrus" + + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // AddTrainAlarmCmd for AddTrainAlarm @@ -47,7 +48,7 @@ func (a *Application) AddTrainAlarm(ctx context.Context, cmd AddTrainAlarmCmd) e } } - alarm, err := trainalarm.NewTrainAlarm( + alarm, err := domain.NewTrainAlarm( cmd.Identifyer, cmd.Plattform, cmd.TrainName, diff --git a/pkg/application/addtrainalarm_test.go b/pkg/trainalarms/application/addtrainalarm_test.go similarity index 100% rename from pkg/application/addtrainalarm_test.go rename to pkg/trainalarms/application/addtrainalarm_test.go diff --git a/pkg/application/application.go b/pkg/trainalarms/application/application.go similarity index 73% rename from pkg/application/application.go rename to pkg/trainalarms/application/application.go index 3f72308..b3572ea 100644 --- a/pkg/application/application.go +++ b/pkg/trainalarms/application/application.go @@ -3,9 +3,10 @@ package application import ( "context" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" - "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" "github.com/sirupsen/logrus" + + "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" + trainalarmDomain "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // Train by hafas @@ -20,12 +21,12 @@ type HafasService interface { // Application represents all usecases type Application struct { hafas HafasService - repo trainalarm.Repository + repo trainalarmDomain.Repository log *logrus.Entry } // NewApplication returns a application service object -func NewApplication(hafas HafasService, repo trainalarm.Repository, log *logrus.Entry) *Application { +func NewApplication(hafas HafasService, repo trainalarmDomain.Repository, log *logrus.Entry) *Application { return &Application{ hafas: hafas, repo: repo, diff --git a/pkg/application/deleteoldstates.go b/pkg/trainalarms/application/deleteoldstates.go similarity index 100% rename from pkg/application/deleteoldstates.go rename to pkg/trainalarms/application/deleteoldstates.go diff --git a/pkg/application/deleteoldstates_test.go b/pkg/trainalarms/application/deleteoldstates_test.go similarity index 100% rename from pkg/application/deleteoldstates_test.go rename to pkg/trainalarms/application/deleteoldstates_test.go diff --git a/pkg/application/deleteoldtrainalarms.go b/pkg/trainalarms/application/deleteoldtrainalarms.go similarity index 100% rename from pkg/application/deleteoldtrainalarms.go rename to pkg/trainalarms/application/deleteoldtrainalarms.go diff --git a/pkg/application/deleteoldtrainalarms_test.go b/pkg/trainalarms/application/deleteoldtrainalarms_test.go similarity index 100% rename from pkg/application/deleteoldtrainalarms_test.go rename to pkg/trainalarms/application/deleteoldtrainalarms_test.go diff --git a/pkg/application/deletetrainalarm.go b/pkg/trainalarms/application/deletetrainalarm.go similarity index 88% rename from pkg/application/deletetrainalarm.go rename to pkg/trainalarms/application/deletetrainalarm.go index 622d9e6..87c518d 100644 --- a/pkg/application/deletetrainalarm.go +++ b/pkg/trainalarms/application/deletetrainalarm.go @@ -4,8 +4,9 @@ import ( "context" "errors" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" "github.com/sirupsen/logrus" + + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // DeleteTrainAlarmCmd for DeleteTrainAlarm @@ -14,7 +15,7 @@ type DeleteTrainAlarmCmd struct { } // DeleteTrainAlarm at database -func (a *Application) DeleteTrainAlarm(ctx context.Context, cmd DeleteTrainAlarmCmd) (*trainalarm.TrainAlarm, error) { +func (a *Application) DeleteTrainAlarm(ctx context.Context, cmd DeleteTrainAlarmCmd) (*domain.TrainAlarm, error) { log := a.log.WithFields(logrus.Fields{ "alarmID": cmd.AlarmID, }) diff --git a/pkg/application/deletetrainalarm_test.go b/pkg/trainalarms/application/deletetrainalarm_test.go similarity index 76% rename from pkg/application/deletetrainalarm_test.go rename to pkg/trainalarms/application/deletetrainalarm_test.go index 6795b2d..5d66fdd 100644 --- a/pkg/application/deletetrainalarm_test.go +++ b/pkg/trainalarms/application/deletetrainalarm_test.go @@ -6,15 +6,16 @@ import ( "time" "github.com/google/uuid" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" "github.com/stretchr/testify/assert" + + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) func TestDeleteTrainAlarm(t *testing.T) { app, repo := createTestCase(true) ctx := context.Background() - alarm, err := trainalarm.NewTrainAlarm(uuid.New().String(), "telegram", "ice 4", 8503000, int64(1595797980000), time.Now(), "Berlin Ostbahnhof") + alarm, err := domain.NewTrainAlarm(uuid.New().String(), "telegram", "ice 4", 8503000, int64(1595797980000), time.Now(), "Berlin Ostbahnhof") assert.Nil(t, err) assert.NotNil(t, alarm) diff --git a/pkg/application/notifyuser.go b/pkg/trainalarms/application/notifyuser.go similarity index 84% rename from pkg/application/notifyuser.go rename to pkg/trainalarms/application/notifyuser.go index d6543e0..9accf0b 100644 --- a/pkg/application/notifyuser.go +++ b/pkg/trainalarms/application/notifyuser.go @@ -5,13 +5,14 @@ import ( "errors" "time" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" - "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" "github.com/sirupsen/logrus" + + "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // NotifyUsers check train delay threshold and call the notifyFn -func (a *Application) NotifyUsers(ctx context.Context, notifyFn func(ctx context.Context, alarm *trainalarm.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error) error { +func (a *Application) NotifyUsers(ctx context.Context, notifyFn func(ctx context.Context, alarm *domain.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error) error { // sort alarms by lastNotificationAt alarms, err := a.repo.GetTrainAlarmsSortByLastNotificationAt(ctx, 20) if err != nil { @@ -26,7 +27,7 @@ func (a *Application) NotifyUsers(ctx context.Context, notifyFn func(ctx context return nil } -func (a *Application) notifyUser(ctx context.Context, notifyFn func(ctx context.Context, alarm *trainalarm.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error, alarm trainalarm.TrainAlarm) error { +func (a *Application) notifyUser(ctx context.Context, notifyFn func(ctx context.Context, alarm *domain.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error, alarm domain.TrainAlarm) error { log := a.log.WithFields(logrus.Fields{ "alarmID": alarm.GetID(), "identifyer": alarm.GetIdentifyer(), @@ -35,7 +36,7 @@ func (a *Application) notifyUser(ctx context.Context, notifyFn func(ctx context. }) // UpdateTrainAlarm use a transaction - err := a.repo.UpdateTrainAlarm(ctx, alarm.GetID(), func(t *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err := a.repo.UpdateTrainAlarm(ctx, alarm.GetID(), func(t *domain.TrainAlarm) (*domain.TrainAlarm, error) { // search train // todo: use cache train, err := a.hafas.GetTrainByStation(ctx, t.GetTrainName(), t.GetStationEVA(), t.GetStationDate()) diff --git a/pkg/application/notifyuser_test.go b/pkg/trainalarms/application/notifyuser_test.go similarity index 83% rename from pkg/application/notifyuser_test.go rename to pkg/trainalarms/application/notifyuser_test.go index 842852b..219f4fe 100644 --- a/pkg/application/notifyuser_test.go +++ b/pkg/trainalarms/application/notifyuser_test.go @@ -9,8 +9,8 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) func TestNotifyUser(t *testing.T) { @@ -52,7 +52,7 @@ func TestNotifyUser(t *testing.T) { for _, testCase := range testCases { app, repo := createTestCase(testCase.HafasFound) - alarm, err := trainalarm.NewTrainAlarm( + alarm, err := domain.NewTrainAlarm( uuid.New().String(), "telegram", "ice 4", @@ -70,7 +70,7 @@ func TestNotifyUser(t *testing.T) { } isRunning := false - err = app.notifyUser(ctx, func(ctx context.Context, alarm *trainalarm.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error { + err = app.notifyUser(ctx, func(ctx context.Context, alarm *domain.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error { isRunning = true if !testCase.NotifySuccess { @@ -97,7 +97,7 @@ func TestNotifyUsers(t *testing.T) { ctx := context.Background() for i := 0; i < 4; i++ { - alarm, err := trainalarm.NewTrainAlarm( + alarm, err := domain.NewTrainAlarm( uuid.New().String(), "telegram", "ice 4", @@ -111,7 +111,7 @@ func TestNotifyUsers(t *testing.T) { repo.TrainAlarms[alarm.GetID()] = alarm } - err := app.NotifyUsers(ctx, func(ctx context.Context, alarm *trainalarm.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error { + err := app.NotifyUsers(ctx, func(ctx context.Context, alarm *domain.TrainAlarm, train marudor.HafasTrain, diff time.Duration) error { return nil }) assert.Nil(t, err) diff --git a/pkg/application/updatetrainalarmthreshold.go b/pkg/trainalarms/application/updatetrainalarmthreshold.go similarity index 81% rename from pkg/application/updatetrainalarmthreshold.go rename to pkg/trainalarms/application/updatetrainalarmthreshold.go index 2bcc296..9c8f0e7 100644 --- a/pkg/application/updatetrainalarmthreshold.go +++ b/pkg/trainalarms/application/updatetrainalarmthreshold.go @@ -4,8 +4,9 @@ import ( "context" "errors" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" "github.com/sirupsen/logrus" + + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // UpdateTrainAlarmThresholdCmd for UpdateTrainAlarmThreshold @@ -22,7 +23,7 @@ func (a *Application) UpdateTrainAlarmThreshold(ctx context.Context, cmd UpdateT }) // search alarm at repository - err := a.repo.UpdateTrainAlarm(ctx, cmd.AlarmID, func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err := a.repo.UpdateTrainAlarm(ctx, cmd.AlarmID, func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { alarm.SetDelayThresholdMinutes(cmd.ThresholdMinutes) return alarm, nil }) diff --git a/pkg/application/updatetrainalarmthreshold_test.go b/pkg/trainalarms/application/updatetrainalarmthreshold_test.go similarity index 90% rename from pkg/application/updatetrainalarmthreshold_test.go rename to pkg/trainalarms/application/updatetrainalarmthreshold_test.go index 2d9ec66..99c156b 100644 --- a/pkg/application/updatetrainalarmthreshold_test.go +++ b/pkg/trainalarms/application/updatetrainalarmthreshold_test.go @@ -8,14 +8,14 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) func TestUpdateTrainAlarmThreshold(t *testing.T) { app, repo := createTestCase(true) ctx := context.Background() - alarm, err := trainalarm.NewTrainAlarm( + alarm, err := domain.NewTrainAlarm( uuid.New().String(), "telegram", "ice 4", diff --git a/pkg/application/utils_test.go b/pkg/trainalarms/application/utils_test.go similarity index 77% rename from pkg/application/utils_test.go rename to pkg/trainalarms/application/utils_test.go index e024a2e..4549871 100644 --- a/pkg/application/utils_test.go +++ b/pkg/trainalarms/application/utils_test.go @@ -4,7 +4,7 @@ import ( "github.com/sirupsen/logrus" "github.com/pkuebler/bahn-bot/pkg/infrastructure/marudor" - "github.com/pkuebler/bahn-bot/pkg/infrastructure/repository" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/repository" ) func createTestCase(trainExists bool) (*Application, *repository.MemoryDatabase) { @@ -12,7 +12,7 @@ func createTestCase(trainExists bool) (*Application, *repository.MemoryDatabase) hafas := &marudor.HafasMock{TrainExists: trainExists} repo := repository.NewMemoryDatabase() - app := NewApplication(hafas, repo, log) + app := NewApplication(hafas, repo, repo, log) return app, repo } diff --git a/pkg/domain/trainalarm/repository.go b/pkg/trainalarms/domain/repository.go similarity index 97% rename from pkg/domain/trainalarm/repository.go rename to pkg/trainalarms/domain/repository.go index fe8e999..1cbdd53 100644 --- a/pkg/domain/trainalarm/repository.go +++ b/pkg/trainalarms/domain/repository.go @@ -1,4 +1,4 @@ -package trainalarm +package domain import ( "context" diff --git a/pkg/domain/trainalarm/trainalarm.go b/pkg/trainalarms/domain/trainalarm.go similarity index 99% rename from pkg/domain/trainalarm/trainalarm.go rename to pkg/trainalarms/domain/trainalarm.go index 424f844..72bad66 100644 --- a/pkg/domain/trainalarm/trainalarm.go +++ b/pkg/trainalarms/domain/trainalarm.go @@ -1,4 +1,4 @@ -package trainalarm +package domain import ( "time" diff --git a/pkg/domain/trainalarm/trainalarm_test.go b/pkg/trainalarms/domain/trainalarm_test.go similarity index 98% rename from pkg/domain/trainalarm/trainalarm_test.go rename to pkg/trainalarms/domain/trainalarm_test.go index d9ea8da..56fed99 100644 --- a/pkg/domain/trainalarm/trainalarm_test.go +++ b/pkg/trainalarms/domain/trainalarm_test.go @@ -1,4 +1,4 @@ -package trainalarm +package domain import ( "testing" diff --git a/pkg/infrastructure/repository/gorm.go b/pkg/trainalarms/repository/gorm.go similarity index 91% rename from pkg/infrastructure/repository/gorm.go rename to pkg/trainalarms/repository/gorm.go index 97aac96..27aceec 100644 --- a/pkg/infrastructure/repository/gorm.go +++ b/pkg/trainalarms/repository/gorm.go @@ -6,7 +6,7 @@ import ( "github.com/jinzhu/gorm" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // SQLDatabase to persistence @@ -46,7 +46,7 @@ type SQLTrainAlarmModel struct { } // NewSQLTrainAlarmModel convert domain model to repository model -func NewSQLTrainAlarmModel(alarm *trainalarm.TrainAlarm) *SQLTrainAlarmModel { +func NewSQLTrainAlarmModel(alarm *domain.TrainAlarm) *SQLTrainAlarmModel { return &SQLTrainAlarmModel{ ID: alarm.GetID(), Identifyer: alarm.GetIdentifyer(), @@ -63,8 +63,8 @@ func NewSQLTrainAlarmModel(alarm *trainalarm.TrainAlarm) *SQLTrainAlarmModel { } // TrainAlarm convert to TrainAlarm domain model -func (s *SQLTrainAlarmModel) TrainAlarm() *trainalarm.TrainAlarm { - alarm, _ := trainalarm.NewTrainAlarmFromRepository( +func (s *SQLTrainAlarmModel) TrainAlarm() *domain.TrainAlarm { + alarm, _ := domain.NewTrainAlarmFromRepository( s.ID, s.Identifyer, s.Plattform, @@ -92,7 +92,7 @@ type SQLStateModel struct { } // GetOrCreateTrainAlarm creates a new alarm if none exists yet -func (s *SQLDatabase) GetOrCreateTrainAlarm(ctx context.Context, alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { +func (s *SQLDatabase) GetOrCreateTrainAlarm(ctx context.Context, alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { var oldModel SQLTrainAlarmModel if res := s.db.Where( "identifyer = ? AND plattform = ? AND train_name = ? AND station_eva = ? AND station_date = ?", @@ -117,7 +117,7 @@ func (s *SQLDatabase) GetOrCreateTrainAlarm(ctx context.Context, alarm *trainala } // GetTrainAlarm by id. returns NO error if nothing found -func (s *SQLDatabase) GetTrainAlarm(ctx context.Context, alarmID string) (*trainalarm.TrainAlarm, error) { +func (s *SQLDatabase) GetTrainAlarm(ctx context.Context, alarmID string) (*domain.TrainAlarm, error) { var alarm SQLTrainAlarmModel if res := s.db.Where("id = ?", alarmID).Take(&alarm); res.Error != nil { if res.RecordNotFound() { @@ -130,7 +130,7 @@ func (s *SQLDatabase) GetTrainAlarm(ctx context.Context, alarmID string) (*train } // GetTrainAlarms by identifyer and plattform. returns NO error if nothing found -func (s *SQLDatabase) GetTrainAlarms(ctx context.Context, identifyer string, plattform string) ([]*trainalarm.TrainAlarm, error) { +func (s *SQLDatabase) GetTrainAlarms(ctx context.Context, identifyer string, plattform string) ([]*domain.TrainAlarm, error) { var results []SQLTrainAlarmModel if res := s.db.Where("identifyer = ? AND plattform = ?", identifyer, plattform).Find(&results); res.Error != nil { if res.RecordNotFound() { @@ -140,7 +140,7 @@ func (s *SQLDatabase) GetTrainAlarms(ctx context.Context, identifyer string, pla } // convert - alarms := []*trainalarm.TrainAlarm{} + alarms := []*domain.TrainAlarm{} for _, result := range results { alarms = append(alarms, result.TrainAlarm()) } @@ -149,7 +149,7 @@ func (s *SQLDatabase) GetTrainAlarms(ctx context.Context, identifyer string, pla } // GetTrainAlarmsSortByLastNotificationAt with a limit. returns NO error if nothing found -func (s *SQLDatabase) GetTrainAlarmsSortByLastNotificationAt(ctx context.Context, limit int) ([]*trainalarm.TrainAlarm, error) { +func (s *SQLDatabase) GetTrainAlarmsSortByLastNotificationAt(ctx context.Context, limit int) ([]*domain.TrainAlarm, error) { var results []SQLTrainAlarmModel if res := s.db.Limit(limit).Order("last_notification_at").Find(&results); res.Error != nil { if res.RecordNotFound() { @@ -159,7 +159,7 @@ func (s *SQLDatabase) GetTrainAlarmsSortByLastNotificationAt(ctx context.Context } // convert - alarms := []*trainalarm.TrainAlarm{} + alarms := []*domain.TrainAlarm{} for _, result := range results { alarms = append(alarms, result.TrainAlarm()) } @@ -180,7 +180,7 @@ func (s *SQLDatabase) DeleteOldTrainAlarms(ctx context.Context, threshold time.T } // UpdateTrainAlarm create a transaction, find the model and save it after updateFn. returns a error if model not found or pipe the error from updateFn -func (s *SQLDatabase) UpdateTrainAlarm(ctx context.Context, alarmID string, updateFn func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error)) error { +func (s *SQLDatabase) UpdateTrainAlarm(ctx context.Context, alarmID string, updateFn func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error)) error { var model SQLTrainAlarmModel if res := s.db.Where("id = ?", alarmID).Take(&model); res.Error != nil { return res.Error diff --git a/pkg/infrastructure/repository/gorm_test.go b/pkg/trainalarms/repository/gorm_test.go similarity index 89% rename from pkg/infrastructure/repository/gorm_test.go rename to pkg/trainalarms/repository/gorm_test.go index f013d0b..72fc890 100644 --- a/pkg/infrastructure/repository/gorm_test.go +++ b/pkg/trainalarms/repository/gorm_test.go @@ -12,7 +12,7 @@ import ( _ "github.com/jinzhu/gorm/dialects/postgres" "github.com/stretchr/testify/assert" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) func TestNewSQLDatabase(t *testing.T) { @@ -22,8 +22,8 @@ func TestNewSQLDatabase(t *testing.T) { db.Close() } -func TestConvertModel(t *testing.T) { - alarm, err := trainalarm.NewTrainAlarm("mychatID", "telegram", "ICE 7", 123456, 1234567890, time.Now(), "Berlin Ostbahnhof") +func TestConvertTrainAlarmModel(t *testing.T) { + alarm, err := domain.NewTrainAlarm("mychatID", "telegram", "ICE 7", 123456, 1234567890, time.Now(), "Berlin Ostbahnhof") assert.NotNil(t, alarm) assert.Nil(t, err) @@ -127,7 +127,7 @@ func TestSQLGetTrainAlarms(t *testing.T) { assert.Len(t, alarms, 0) for i := 0; i < 4; i++ { - alarm, err := trainalarm.NewTrainAlarm("1234", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") + alarm, err := domain.NewTrainAlarm("1234", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") assert.Nil(t, err) if i > 1 { alarm.SetSuccessfulNotification() @@ -156,7 +156,7 @@ func TestSQLGetTrainAlarmsSortByLastNotificationAt(t *testing.T) { assert.Len(t, alarms, 0) for i := 0; i < 4; i++ { - alarm, err := trainalarm.NewTrainAlarm("identifyer", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") + alarm, err := domain.NewTrainAlarm("identifyer", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") assert.Nil(t, err) if i > 1 { alarm.SetSuccessfulNotification() @@ -216,7 +216,7 @@ func TestSQLUpdateTrainAlarm(t *testing.T) { // not found notRunning := true - err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { notRunning = false return nil, nil }) @@ -228,7 +228,7 @@ func TestSQLUpdateTrainAlarm(t *testing.T) { assert.Nil(t, err) running := false - err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { running = true return nil, errors.New("error") }) @@ -248,7 +248,7 @@ func TestSQLUpdateTrainAlarm(t *testing.T) { assert.Nil(t, err) running = false - err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { running = true alarm.SetSuccessfulNotification() return alarm, nil diff --git a/pkg/infrastructure/repository/memory.go b/pkg/trainalarms/repository/memory.go similarity index 88% rename from pkg/infrastructure/repository/memory.go rename to pkg/trainalarms/repository/memory.go index ba35785..a3b3847 100644 --- a/pkg/infrastructure/repository/memory.go +++ b/pkg/trainalarms/repository/memory.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) // MemoryStateModel save the current state @@ -21,7 +21,7 @@ type MemoryStateModel struct { // MemoryDatabase to test without persistence type MemoryDatabase struct { - TrainAlarms map[string]*trainalarm.TrainAlarm + TrainAlarms map[string]*domain.TrainAlarm CurrentState []*MemoryStateModel Mutex *sync.Mutex } @@ -29,14 +29,14 @@ type MemoryDatabase struct { // NewMemoryDatabase with mutex func NewMemoryDatabase() *MemoryDatabase { return &MemoryDatabase{ - TrainAlarms: map[string]*trainalarm.TrainAlarm{}, + TrainAlarms: map[string]*domain.TrainAlarm{}, CurrentState: []*MemoryStateModel{}, Mutex: &sync.Mutex{}, } } // GetOrCreateTrainAlarm creates a new alarm if none exists yet -func (m *MemoryDatabase) GetOrCreateTrainAlarm(ctx context.Context, alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { +func (m *MemoryDatabase) GetOrCreateTrainAlarm(ctx context.Context, alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { m.Mutex.Lock() defer m.Mutex.Unlock() @@ -54,7 +54,7 @@ func (m *MemoryDatabase) GetOrCreateTrainAlarm(ctx context.Context, alarm *train } // GetTrainAlarm by id. returns NO error if nothing found -func (m *MemoryDatabase) GetTrainAlarm(ctx context.Context, alarmID string) (*trainalarm.TrainAlarm, error) { +func (m *MemoryDatabase) GetTrainAlarm(ctx context.Context, alarmID string) (*domain.TrainAlarm, error) { m.Mutex.Lock() defer m.Mutex.Unlock() @@ -66,11 +66,11 @@ func (m *MemoryDatabase) GetTrainAlarm(ctx context.Context, alarmID string) (*tr } // GetTrainAlarms by identifyer and plattform. returns NO error if nothing found -func (m *MemoryDatabase) GetTrainAlarms(ctx context.Context, identifyer string, plattform string) ([]*trainalarm.TrainAlarm, error) { +func (m *MemoryDatabase) GetTrainAlarms(ctx context.Context, identifyer string, plattform string) ([]*domain.TrainAlarm, error) { m.Mutex.Lock() defer m.Mutex.Unlock() - alarms := []*trainalarm.TrainAlarm{} + alarms := []*domain.TrainAlarm{} for _, alarm := range m.TrainAlarms { if identifyer == alarm.GetIdentifyer() && plattform == alarm.GetPlattform() { @@ -82,11 +82,11 @@ func (m *MemoryDatabase) GetTrainAlarms(ctx context.Context, identifyer string, } // GetTrainAlarmsSortByLastNotificationAt with a limit. returns NO error if nothing found -func (m *MemoryDatabase) GetTrainAlarmsSortByLastNotificationAt(ctx context.Context, limit int) ([]*trainalarm.TrainAlarm, error) { - results := []*trainalarm.TrainAlarm{} +func (m *MemoryDatabase) GetTrainAlarmsSortByLastNotificationAt(ctx context.Context, limit int) ([]*domain.TrainAlarm, error) { + results := []*domain.TrainAlarm{} // convert map to slice - alarms := []*trainalarm.TrainAlarm{} + alarms := []*domain.TrainAlarm{} func() { m.Mutex.Lock() @@ -152,7 +152,7 @@ func (m *MemoryDatabase) DeleteOldTrainAlarms(ctx context.Context, threshold tim } // UpdateTrainAlarm create a transaction, find the model and save it after updateFn. returns a error if model not found or pipe the error from updateFn -func (m *MemoryDatabase) UpdateTrainAlarm(ctx context.Context, alarmID string, updateFn func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error)) error { +func (m *MemoryDatabase) UpdateTrainAlarm(ctx context.Context, alarmID string, updateFn func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error)) error { m.Mutex.Lock() defer m.Mutex.Unlock() @@ -170,7 +170,7 @@ func (m *MemoryDatabase) UpdateTrainAlarm(ctx context.Context, alarmID string, u } // findTrainAlarm returns NO error if nothing found -func (m *MemoryDatabase) findTrainAlarm(query *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { +func (m *MemoryDatabase) findTrainAlarm(query *domain.TrainAlarm) (*domain.TrainAlarm, error) { for _, alarm := range m.TrainAlarms { if alarm.Compare(query) { return alarm, nil diff --git a/pkg/infrastructure/repository/memory_test.go b/pkg/trainalarms/repository/memory_test.go similarity index 85% rename from pkg/infrastructure/repository/memory_test.go rename to pkg/trainalarms/repository/memory_test.go index 4b75433..b3aa7a4 100644 --- a/pkg/infrastructure/repository/memory_test.go +++ b/pkg/trainalarms/repository/memory_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/pkuebler/bahn-bot/pkg/domain/trainalarm" + "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" ) func TestNewMemoryDatabase(t *testing.T) { @@ -73,7 +73,7 @@ func TestGetTrainAlarms(t *testing.T) { assert.Len(t, alarms, 0) for i := 0; i < 4; i++ { - alarm, err := trainalarm.NewTrainAlarm("1234", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") + alarm, err := domain.NewTrainAlarm("1234", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") assert.Nil(t, err) if i > 1 { alarm.SetSuccessfulNotification() @@ -96,7 +96,7 @@ func TestGetTrainAlarmsSortByLastNotificationAt(t *testing.T) { assert.Len(t, alarms, 0) for i := 0; i < 4; i++ { - alarm, err := trainalarm.NewTrainAlarm("identifyer", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") + alarm, err := domain.NewTrainAlarm("identifyer", "telegram", fmt.Sprintf("ice %d", i), i, int64(i), time.Now(), "Berlin Ostbahnhof") assert.Nil(t, err) if i > 1 { alarm.SetSuccessfulNotification() @@ -142,7 +142,7 @@ func TestUpdateTrainAlarm(t *testing.T) { // not found notRunning := true - err := db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err := db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { notRunning = false return nil, nil }) @@ -152,7 +152,7 @@ func TestUpdateTrainAlarm(t *testing.T) { // update with error db.TrainAlarms[alarm.GetID()] = alarm running := false - err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { running = true return nil, errors.New("error") }) @@ -163,7 +163,7 @@ func TestUpdateTrainAlarm(t *testing.T) { // update db.TrainAlarms[alarm.GetID()] = alarm running = false - err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *trainalarm.TrainAlarm) (*trainalarm.TrainAlarm, error) { + err = db.UpdateTrainAlarm(ctx, alarm.GetID(), func(alarm *domain.TrainAlarm) (*domain.TrainAlarm, error) { running = true alarm.SetSuccessfulNotification() return alarm, nil @@ -199,8 +199,8 @@ func TestDeleteOldStates(t *testing.T) { assert.Len(t, db.CurrentState, 1) } -func createTestTrainAlarm(finalArrival time.Time) *trainalarm.TrainAlarm { - alarm, _ := trainalarm.NewTrainAlarm( +func createTestTrainAlarm(finalArrival time.Time) *domain.TrainAlarm { + alarm, _ := domain.NewTrainAlarm( "identifyer", "telegram", "ice 6", diff --git a/pkg/webhooks/application/addwebhook.go b/pkg/webhooks/application/addwebhook.go new file mode 100644 index 0000000..6d2e9cf --- /dev/null +++ b/pkg/webhooks/application/addwebhook.go @@ -0,0 +1,51 @@ +package application + +import ( + "context" + "errors" + + "github.com/lithammer/shortuuid" + "github.com/sirupsen/logrus" + + webhookDomain "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +// AddWebhookCmd for AddWebhook +type AddWebhookCmd struct { + Identifyer string + Plattform string + Protocol string +} + +// AddWebhook to database +func (a *Application) AddWebhook(ctx context.Context, cmd AddWebhookCmd) (*webhookDomain.Webhook, error) { + log := a.log.WithFields(logrus.Fields{ + "identifyer": cmd.Identifyer, + "plattform": cmd.Plattform, + "protocol": cmd.Protocol, + }) + + protocol, err := webhookDomain.NewWebhookProtocol(cmd.Protocol) + if err != nil { + log.Error(err) + return nil, errors.New("unknown protocol") + } + + token := shortuuid.New() + + hook, err := webhookDomain.NewWebhook( + cmd.Identifyer, + cmd.Plattform, + token, + protocol, + ) + + _, err = a.webhookRepo.CreateWebhook(ctx, hook) + if err != nil { + log.Error(err) + return nil, errors.New("internal server error") + } + + log.Trace("webhook created") + return hook, nil +} diff --git a/pkg/webhooks/application/addwebhook_test.go b/pkg/webhooks/application/addwebhook_test.go new file mode 100644 index 0000000..6eadb5c --- /dev/null +++ b/pkg/webhooks/application/addwebhook_test.go @@ -0,0 +1,26 @@ +package application + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + webhookDomain "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +func TestAddWebhook(t *testing.T) { + app, alarmRepo, webhookRepo := createTestCase(true) + ctx := context.Background() + + cmd := AddWebhookCmd{ + Identifyer: uuid.New().String(), + Plattform: "telegram", + Protocol: string(webhookDomain.TravelynxProtocol), + } + + _, err := app.AddWebhook(ctx, cmd) + assert.Nil(t, err) + assert.Len(t, webhookRepo.Webhooks, 1) +} diff --git a/pkg/webhooks/application/application.go b/pkg/webhooks/application/application.go new file mode 100644 index 0000000..387709d --- /dev/null +++ b/pkg/webhooks/application/application.go @@ -0,0 +1,24 @@ +package application + +import ( + "github.com/sirupsen/logrus" + + trainalarmDomain "github.com/pkuebler/bahn-bot/pkg/trainalarms/domain" + webhookDomain "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +// Application represents all usecases +type Application struct { + alarmRepo trainalarmDomain.Repository + webhookRepo webhookDomain.Repository + log *logrus.Entry +} + +// NewApplication returns a application service object +func NewApplication(alarmRepo trainalarmDomain.Repository, webhookRepo webhookDomain.Repository, log *logrus.Entry) *Application { + return &Application{ + alarmRepo: alarmRepo, + webhookRepo: webhookRepo, + log: log, + } +} diff --git a/pkg/webhooks/application/deletewebhook.go b/pkg/webhooks/application/deletewebhook.go new file mode 100644 index 0000000..2a847f6 --- /dev/null +++ b/pkg/webhooks/application/deletewebhook.go @@ -0,0 +1,42 @@ +package application + +import ( + "context" + "errors" + + "github.com/sirupsen/logrus" + + webhookDomain "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +// DeleteWebhookCmd for DeleteWebhook +type DeleteWebhookCmd struct { + WebhookID string +} + +// DeleteWebhook at database +func (a *Application) DeleteWebhook(ctx context.Context, cmd DeleteWebhookCmd) (*webhookDomain.Webhook, error) { + log := a.log.WithFields(logrus.Fields{ + "webhookID": cmd.WebhookID, + }) + + // search webhook at repository + webhook, err := a.webhookRepo.GetWebhook(ctx, cmd.WebhookID) + if err != nil { + log.Error(err) + return nil, errors.New("internal server error") + } + if webhook == nil { + log.Trace("not found") + return nil, errors.New("not found") + } + + // delete webhook at repository + if err := a.webhookRepo.DeleteWebhook(ctx, webhook.GetID()); err != nil { + log.Error(err) + return nil, errors.New("internal server error") + } + + log.Trace("webhook deleted") + return webhook, nil +} diff --git a/pkg/webhooks/application/deletewebhook_test.go b/pkg/webhooks/application/deletewebhook_test.go new file mode 100644 index 0000000..a77eb91 --- /dev/null +++ b/pkg/webhooks/application/deletewebhook_test.go @@ -0,0 +1,38 @@ +package application + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + webhookDomain "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +func TestDeleteWebhook(t *testing.T) { + app, alarmRepo, webhookRepo := createTestCase(true) + ctx := context.Background() + + hook, err := webhookDomain.NewWebhook(uuid.New().String(), "telegram", "12345", "12345", webhookDomain.TravelynxProtocol) + assert.Nil(t, err) + assert.NotNil(t, hook) + + cmd := DeleteWebhookCmd{ + WebhookID: hook.GetID(), + } + + // entry not found + a, err := app.DeleteWebhook(ctx, cmd) + assert.NotNil(t, err) + assert.Nil(t, a) + + // add entry + webhookRepo.Webhooks[hook.GetID()] = hook + + // entry deleted + a, err = app.DeleteWebhook(ctx, cmd) + assert.Nil(t, err) + assert.NotNil(t, a) + assert.Len(t, webhookRepo.Webhooks, 0) +} diff --git a/pkg/webhooks/application/utils_test.go b/pkg/webhooks/application/utils_test.go new file mode 100644 index 0000000..5763dc9 --- /dev/null +++ b/pkg/webhooks/application/utils_test.go @@ -0,0 +1,18 @@ +package application + +import ( + "github.com/sirupsen/logrus" + + trainalarmRepository "github.com/pkuebler/bahn-bot/pkg/trainalarms/repository" + webhookRepository "github.com/pkuebler/bahn-bot/pkg/webhooks/repository" +) + +func createTestCase(trainExists bool) (*Application, *trainalarmRepository.MemoryDatabase, *webhookRepository.MemoryDatabase) { + log := logrus.NewEntry(logrus.StandardLogger()) + + webhookRepo := webhookRepository.NewMemoryDatabase() + trainalarmRepo := trainalarmRepository.NewMemoryDatabase() + app := NewApplication(trainalarmRepo, webhookRepo, log) + + return app, trainalarmRepo, webhookRepo +} diff --git a/pkg/webhooks/domain/repository.go b/pkg/webhooks/domain/repository.go new file mode 100644 index 0000000..4fff6af --- /dev/null +++ b/pkg/webhooks/domain/repository.go @@ -0,0 +1,14 @@ +package domain + +import ( + "context" +) + +// Repository interface to datahandling between repository and application +type Repository interface { + CreateWebhook(ctx context.Context, webhook *Webhook) (*Webhook, error) + GetWebhook(ctx context.Context, id string) (*Webhook, error) + GetWebhooksByIdentifyer(ctx context.Context, identifyer string, plattform string) ([]*Webhook, error) + GetWebhookByURLHash(ctx context.Context, urlHash string) (*Webhook, error) + DeleteWebhook(ctx context.Context, webhookID string) error +} diff --git a/pkg/webhooks/domain/webhook.go b/pkg/webhooks/domain/webhook.go new file mode 100644 index 0000000..9a50272 --- /dev/null +++ b/pkg/webhooks/domain/webhook.go @@ -0,0 +1,103 @@ +package domain + +import ( + "errors" + + "github.com/google/uuid" + "github.com/lithammer/shortuuid/v3" +) + +// WebhookProtocol to support multiple services +type WebhookProtocol string + +const ( + // TravelynxProtocol to support travelynx webhooks + TravelynxProtocol WebhookProtocol = "travelynx" +) + +// NewWebhookProtocol converts string to WebhookProtocol +func NewWebhookProtocol(protocol string) (WebhookProtocol, error) { + switch protocol { + case "travelynx": + return TravelynxProtocol, nil + } + + return TravelynxProtocol, errors.New("unknown protocol") +} + +// Webhook represent a webhook to use it e.g. travelynx +type Webhook struct { + id string + identifyer string + plattform string + + urlHash string + token string + protocol WebhookProtocol +} + +// NewWebhook returns a new webhook +func NewWebhook( + identifyer string, + plattform string, + token string, + protocol WebhookProtocol, +) (*Webhook, error) { + return &Webhook{ + id: uuid.New().String(), + identifyer: identifyer, + plattform: plattform, + urlHash: shortuuid.New(), + token: token, + protocol: protocol, + }, nil +} + +// NewWebhookFromRepository returns a new webhook with all protected fields +func NewWebhookFromRepository( + id string, + identifyer string, + plattform string, + urlHash string, + token string, + protocol WebhookProtocol, +) (*Webhook, error) { + return &Webhook{ + id: id, + identifyer: identifyer, + plattform: plattform, + urlHash: urlHash, + token: token, + protocol: protocol, + }, nil +} + +// GetID from domain +func (w *Webhook) GetID() string { + return w.id +} + +// GetIdentifyer from webhook +func (w *Webhook) GetIdentifyer() string { + return w.identifyer +} + +// GetPlattform from webhook +func (w *Webhook) GetPlattform() string { + return w.plattform +} + +// GetURLHash from webhook +func (w *Webhook) GetURLHash() string { + return w.urlHash +} + +// GetToken from webhook +func (w *Webhook) GetToken() string { + return w.token +} + +// GetProtocol from webhook +func (w *Webhook) GetProtocol() WebhookProtocol { + return w.protocol +} diff --git a/pkg/webhooks/domain/webhook_test.go b/pkg/webhooks/domain/webhook_test.go new file mode 100644 index 0000000..dc92369 --- /dev/null +++ b/pkg/webhooks/domain/webhook_test.go @@ -0,0 +1,32 @@ +package domain + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestNewWebhook(t *testing.T) { + webhook, err := NewWebhook("identifyer", "telegram", "hook", "1233245345", "1234567890") + assert.Nil(t, err) + assert.NotNil(t, webhook) +} + +func TestWebhookGetters(t *testing.T) { + identifyer := uuid.New().String() + plattform := "telegram" + urlHash := uuid.New().String() + token := "234234234" + protocol := TravelynxProtocol + + webhook, err := NewWebhook(identifyer, plattform, urlHash, token, protocol) + assert.Nil(t, err) + assert.NotNil(t, webhook) + + assert.Equal(t, identifyer, webhook.GetIdentifyer()) + assert.Equal(t, plattform, webhook.GetPlattform()) + assert.Equal(t, urlHash, webhook.GetURLHash()) + assert.Equal(t, token, webhook.GetToken()) + assert.Equal(t, protocol, webhook.GetProtocol()) +} diff --git a/pkg/webhooks/repository/gorm.go b/pkg/webhooks/repository/gorm.go new file mode 100644 index 0000000..a7be4e7 --- /dev/null +++ b/pkg/webhooks/repository/gorm.go @@ -0,0 +1,134 @@ +package repository + +import ( + "context" + "time" + + "github.com/jinzhu/gorm" + + "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +// SQLDatabase to persistence +type SQLDatabase struct { + db *gorm.DB +} + +// NewSQLDatabase by gorm +func NewSQLDatabase(dialect string, path string) *SQLDatabase { + db, err := gorm.Open(dialect, path) + if err != nil { + panic(err) + } + + db.AutoMigrate(&SQLWebhookModel{}) + + return &SQLDatabase{ + db: db, + } +} + +// SQLWebhookModel to save webhooks +type SQLWebhookModel struct { + ID string `gorm:"primary_key"` + Identifyer string + Plattform string + URLHash string + Token string + Protocol string + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewSQLWebhookModel converts domain to model +func NewSQLWebhookModel(hook *domain.Webhook) *SQLWebhookModel { + return &SQLWebhookModel{ + ID: hook.GetID(), + Identifyer: hook.GetIdentifyer(), + Plattform: hook.GetPlattform(), + URLHash: hook.GetURLHash(), + Token: hook.GetToken(), + Protocol: string(hook.GetProtocol()), + } +} + +// Webhook convert to Webhook domain model +func (s *SQLWebhookModel) Webhook() *domain.Webhook { + protocol, _ := domain.NewWebhookProtocol(s.Protocol) + + webhook, _ := domain.NewWebhookFromRepository( + s.ID, + s.Identifyer, + s.Plattform, + s.URLHash, + s.Token, + protocol, + ) + + return webhook +} + +// Close database +func (s *SQLDatabase) Close() { + s.db.Close() +} + +// CreateWebhook at database +func (s *SQLDatabase) CreateWebhook(ctx context.Context, hook *domain.Webhook) (*domain.Webhook, error) { + model := NewSQLWebhookModel(hook) + if err := s.db.Create(model).Error; err != nil { + return nil, err + } + return model.Webhook(), nil +} + +// GetWebhook by id +func (s *SQLDatabase) GetWebhook(ctx context.Context, id string) (*domain.Webhook, error) { + var hook SQLWebhookModel + if res := s.db.Where("id = ?", id).Take(&hook); res.Error != nil { + if res.RecordNotFound() { + return nil, nil + } + return nil, res.Error + } + + return hook.Webhook(), nil +} + +// GetWebhooksByIdentifyer returns a empty array if nothing found +func (s *SQLDatabase) GetWebhooksByIdentifyer(ctx context.Context, identifyer string, plattform string) ([]*domain.Webhook, error) { + var results []SQLWebhookModel + if res := s.db.Where("identifyer = ? AND plattform = ?", identifyer, plattform).Find(&results); res.Error != nil { + if res.RecordNotFound() { + return nil, nil + } + return nil, res.Error + } + + // convert + hooks := []*domain.Webhook{} + for _, result := range results { + hooks = append(hooks, result.Webhook()) + } + + return hooks, nil +} + +// GetWebhookByURLHash returns nothing if not found +func (s *SQLDatabase) GetWebhookByURLHash(ctx context.Context, urlHash string) (*domain.Webhook, error) { + var hook SQLWebhookModel + if res := s.db.Where("urlHash = ?", urlHash).Take(&hook); res.Error != nil { + if res.RecordNotFound() { + return nil, nil + } + return nil, res.Error + } + + return hook.Webhook(), nil +} + +// DeleteWebhook returns a error if webhook not found +func (s *SQLDatabase) DeleteWebhook(ctx context.Context, webhookID string) error { + err := s.db.Where("id = ?", webhookID).Delete(SQLWebhookModel{}).Error + return err +} diff --git a/pkg/webhooks/repository/gorm_test.go b/pkg/webhooks/repository/gorm_test.go new file mode 100644 index 0000000..d9f82c1 --- /dev/null +++ b/pkg/webhooks/repository/gorm_test.go @@ -0,0 +1,166 @@ +package repository + +import ( + "context" + "os" + "testing" + + _ "github.com/jinzhu/gorm/dialects/mysql" + _ "github.com/jinzhu/gorm/dialects/postgres" + "github.com/stretchr/testify/assert" + + "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +func TestNewSQLDatabase(t *testing.T) { + db := NewSQLDatabase(os.Getenv("DB_DIALECT"), os.Getenv("DB_PATH")) + assert.NotNil(t, db) + + db.Close() +} + +func TestConvertWebhookModel(t *testing.T) { + hook, err := domain.NewWebhook("mychatID", "telegram", "1234567", "123456", domain.TravelynxProtocol) + assert.NotNil(t, hook) + assert.Nil(t, err) + + // convert + model := NewSQLWebhookModel(hook) + assert.NotNil(t, model) + + assert.Equal(t, hook.GetID(), model.ID) + assert.Equal(t, hook.GetIdentifyer(), model.Identifyer) + assert.Equal(t, hook.GetPlattform(), model.Plattform) + assert.Equal(t, hook.GetURLHash(), model.Path) + assert.Equal(t, hook.GetToken(), model.Token) + assert.Equal(t, hook.GetProtocol(), model.Protocol) + + // convert back + back := model.Webhook() + assert.NotNil(t, back) + + assert.Equal(t, model.ID, back.GetID()) + assert.Equal(t, model.Identifyer, back.GetIdentifyer()) + assert.Equal(t, model.Plattform, back.GetPlattform()) + assert.Equal(t, model.Path, back.GetURLHash()) + assert.Equal(t, model.Token, back.GetToken()) + assert.Equal(t, model.Protocol, back.GetProtocol()) +} + +func TestSQLCreateWebhook(t *testing.T) { + ctx := context.Background() + db := NewSQLDatabase(os.Getenv("DB_DIALECT"), os.Getenv("DB_PATH")) + assert.NotNil(t, db) + defer db.Close() + err := db.db.Delete(SQLWebhookModel{}).Error + assert.Nil(t, err) + + query := createTestWebhook() + + hook, err := db.CreateWebhook(ctx, query) + assert.Nil(t, err) + assert.NotNil(t, hook) + var dbHook SQLWebhookModel + err = db.db.Where("id = ?", hook.GetID()).First(&dbHook).Error + assert.Nil(t, err) + assert.Equal(t, hook.GetID(), dbHook.ID) +} + +func TestSQLGetWebhook(t *testing.T) { + ctx := context.Background() + db := NewSQLDatabase(os.Getenv("DB_DIALECT"), os.Getenv("DB_PATH")) + assert.NotNil(t, db) + defer db.Close() + err := db.db.Delete(SQLWebhookModel{}).Error + assert.Nil(t, err) + + query := createTestWebhook() + + // not found + hook, err := db.GetWebhook(ctx, query.GetID()) + assert.Nil(t, err) + assert.Nil(t, hook) + + // founded + err = db.db.Create(NewSQLWebhookModel(query)).Error + assert.Nil(t, err) + + hook, err = db.GetWebhook(ctx, query.GetID()) + assert.Nil(t, err) + assert.NotNil(t, hook) +} + +func TestSQLGetWebhooksByIdentifyer(t *testing.T) { + ctx := context.Background() + db := NewSQLDatabase(os.Getenv("DB_DIALECT"), os.Getenv("DB_PATH")) + assert.NotNil(t, db) + defer db.Close() + err := db.db.Delete(SQLWebhookModel{}).Error + assert.Nil(t, err) + + // empty database + hooks, err := db.GetWebhooksByIdentifyer(ctx, "1234", "telegram") + assert.Nil(t, err) + assert.Len(t, hooks, 0) + + for i := 0; i < 4; i++ { + hook, err := domain.NewWebhook("1234", "telegram", "hook", "123456789", domain.TravelynxProtocol) + assert.Nil(t, err) + err = db.db.Create(NewSQLWebhookModel(hook)).Error + assert.Nil(t, err) + } + + hooks, err = db.GetWebhooksByIdentifyer(ctx, "1234", "telegram") + assert.Nil(t, err) + assert.Len(t, hooks, 4) +} + +func TestSQLGetWebhookByURLHash(t *testing.T) { + ctx := context.Background() + db := NewSQLDatabase(os.Getenv("DB_DIALECT"), os.Getenv("DB_PATH")) + assert.NotNil(t, db) + defer db.Close() + err := db.db.Delete(SQLWebhookModel{}).Error + assert.Nil(t, err) + + query := createTestWebhook() + + // not found + hook, err := db.GetWebhookByURLHash(ctx, query.GetURLHash()) + assert.Nil(t, err) + assert.Nil(t, hook) + + // founded + err = db.db.Create(NewSQLWebhookModel(query)).Error + assert.Nil(t, err) + + hook, err = db.GetWebhookByURLHash(ctx, query.GetURLHash()) + assert.Nil(t, err) + assert.NotNil(t, hook) +} + +func TestSQLDeleteWebhook(t *testing.T) { + ctx := context.Background() + db := NewSQLDatabase(os.Getenv("DB_DIALECT"), os.Getenv("DB_PATH")) + assert.NotNil(t, db) + defer db.Close() + err := db.db.Delete(SQLWebhookModel{}).Error + assert.Nil(t, err) + + hook := createTestWebhook() + + // not found + err = db.DeleteWebhook(ctx, hook.GetID()) + assert.Nil(t, err) + + // delete + err = db.db.Create(NewSQLWebhookModel(hook)).Error + assert.Nil(t, err) + + err = db.DeleteWebhook(ctx, hook.GetID()) + assert.Nil(t, err) + + var dbHook SQLWebhookModel + err = db.db.Where("id = ?", hook.GetID()).First(&dbHook).Error + assert.NotNil(t, err) +} diff --git a/pkg/webhooks/repository/memory.go b/pkg/webhooks/repository/memory.go new file mode 100644 index 0000000..61ceb40 --- /dev/null +++ b/pkg/webhooks/repository/memory.go @@ -0,0 +1,88 @@ +package repository + +import ( + "context" + "errors" + "sync" + + "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +// MemoryDatabase to test without persistence +type MemoryDatabase struct { + Webhooks map[string]*domain.Webhook + Mutex *sync.Mutex +} + +// NewMemoryDatabase with mutex +func NewMemoryDatabase() *MemoryDatabase { + return &MemoryDatabase{ + Webhooks: map[string]*domain.Webhook{}, + Mutex: &sync.Mutex{}, + } +} + +// CreateWebhook at database +func (m *MemoryDatabase) CreateWebhook(ctx context.Context, webhook *domain.Webhook) (*domain.Webhook, error) { + m.Mutex.Lock() + defer m.Mutex.Unlock() + + m.Webhooks[webhook.GetID()] = webhook + return webhook, nil +} + +// GetWebhook by id +func (m *MemoryDatabase) GetWebhook(ctx context.Context, id string) (*domain.Webhook, error) { + m.Mutex.Lock() + defer m.Mutex.Unlock() + + if _, ok := m.Webhooks[id]; !ok { + return nil, nil + } + + return m.Webhooks[id], nil +} + +// GetWebhooksByIdentifyer returns a empty array if nothing found +func (m *MemoryDatabase) GetWebhooksByIdentifyer(ctx context.Context, identifyer string, plattform string) ([]*domain.Webhook, error) { + m.Mutex.Lock() + defer m.Mutex.Unlock() + + webhooks := []*domain.Webhook{} + + for _, hook := range m.Webhooks { + if identifyer == hook.GetIdentifyer() && plattform == hook.GetPlattform() { + webhooks = append(webhooks, hook) + } + } + + return webhooks, nil +} + +// GetWebhookByURLHash returns nothing if not found +func (m *MemoryDatabase) GetWebhookByURLHash(ctx context.Context, urlHash string) (*domain.Webhook, error) { + m.Mutex.Lock() + defer m.Mutex.Unlock() + + for _, hook := range m.Webhooks { + if urlHash == hook.GetURLHash() { + return hook, nil + } + } + + return nil, nil +} + +// DeleteWebhook returns a error if webhook not found +func (m *MemoryDatabase) DeleteWebhook(ctx context.Context, webhookID string) error { + m.Mutex.Lock() + defer m.Mutex.Unlock() + + if _, ok := m.Webhooks[webhookID]; !ok { + return errors.New("not found") + } + + delete(m.Webhooks, webhookID) + + return nil +} diff --git a/pkg/webhooks/repository/memory_test.go b/pkg/webhooks/repository/memory_test.go new file mode 100644 index 0000000..c7a5bc5 --- /dev/null +++ b/pkg/webhooks/repository/memory_test.go @@ -0,0 +1,116 @@ +package repository + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/pkuebler/bahn-bot/pkg/webhooks/domain" +) + +func TestNewMemoryDatabase(t *testing.T) { + db := NewMemoryDatabase() + assert.NotNil(t, db) + assert.NotNil(t, db.Mutex) +} + +func TestCreateWebhook(t *testing.T) { + db := NewMemoryDatabase() + ctx := context.Background() + + query := createTestWebhook() + + hook, err := db.CreateWebhook(ctx, query) + assert.Nil(t, err) + assert.NotNil(t, hook) + _, ok := db.Webhooks[hook.GetID()] + assert.True(t, ok) + assert.Equal(t, hook.GetID(), db.Webhooks[hook.GetID()].GetID()) +} + +func TestGetWebhook(t *testing.T) { + db := NewMemoryDatabase() + ctx := context.Background() + + query := createTestWebhook() + + // not found + alarm, err := db.GetWebhook(ctx, query.GetID()) + assert.Nil(t, err) + assert.Nil(t, alarm) + + // founded + db.Webhooks[query.GetID()] = query + alarm, err = db.GetWebhook(ctx, query.GetID()) + assert.Nil(t, err) + assert.NotNil(t, alarm) +} + +func TestGetWebhooksByIdentifyer(t *testing.T) { + db := NewMemoryDatabase() + ctx := context.Background() + + // empty database + webhooks, err := db.GetWebhooksByIdentifyer(ctx, "1234", "telegram") + assert.Nil(t, err) + assert.Len(t, webhooks, 0) + + for i := 0; i < 4; i++ { + hook, err := domain.NewWebhook("1234", "telegram", "hook", "123456789", domain.TravelynxProtocol) + assert.Nil(t, err) + db.Webhooks[hook.GetID()] = hook + } + + webhooks, err = db.GetWebhooksByIdentifyer(ctx, "1234", "telegram") + assert.Nil(t, err) + assert.Len(t, webhooks, 4) +} + +func TestGetWebhookByURLHash(t *testing.T) { + db := NewMemoryDatabase() + ctx := context.Background() + + query := createTestWebhook() + + // not found + hook, err := db.GetWebhookByURLHash(ctx, query.GetURLHash()) + assert.Nil(t, err) + assert.Nil(t, hook) + + // founded + db.Webhooks[query.GetID()] = query + hook, err = db.GetWebhookByURLHash(ctx, query.GetURLHash()) + assert.Nil(t, err) + assert.NotNil(t, hook) +} +func TestDeleteWebhook(t *testing.T) { + db := NewMemoryDatabase() + ctx := context.Background() + + query := createTestWebhook() + db.Webhooks[query.GetID()] = query + + // not found + err := db.DeleteWebhook(ctx, uuid.New().String()) + assert.NotNil(t, err) + assert.Len(t, db.Webhooks, 1) + + // found + err = db.DeleteWebhook(ctx, query.GetID()) + assert.Nil(t, err) + assert.Len(t, db.Webhooks, 0) +} + +func createTestWebhook() *domain.Webhook { + webhook, _ := domain.NewWebhook( + "identifyer", + "telegram", + uuid.New().String(), + "1234", + domain.TravelynxProtocol, + ) + + return webhook +}