diff --git a/Dockerfile b/Dockerfile index 945a5d1..af7d28f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ WORKDIR /app COPY --from=builder /src/statusbot . -CMD ./statusbot -b ${STATUSBOT_TOKEN} -t ${NOTFICATION_TIME} -z ${TIMEZONE} +CMD ./statusbot -b ${STATUSBOT_TOKEN} -t ${NOTFICATION_TIME} -z ${TIMEZONE} -d ${DATABASE} diff --git a/README.md b/README.md index 58ba21b..6e1ef07 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,14 @@ cd statusbot - Using go ```bash -go run main.go -b -t -z +go run main.go -b -t -z -d ``` - Using Docker ```bash docker build -t statusbot . -docker run -e STATUSBOT_TOKEN= -e NOTFICATION_TIME= -e TIMEZONE= -it statusbot +docker run -e STATUSBOT_TOKEN= -e NOTFICATION_TIME= -e TIMEZONE= -e DATABASE=-it statusbot ``` ## Create a bot diff --git a/cmd/root.go b/cmd/root.go index 9c85cab..6406765 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,8 +2,9 @@ package cmd import ( "os" + "path/filepath" - "github.com/codescalers/statusbot/internal" + "github.com/codescalers/statusbot/internal/bot" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -20,26 +21,35 @@ var rootCmd = &cobra.Command{ token, err := cmd.Flags().GetString("bot-token") if err != nil || token == "" { - log.Error().Err(err).Msg("error in token") - return + log.Fatal().Err(err).Msg("error in token") } time, err := cmd.Flags().GetString("time") if err != nil || time == "" { - log.Error().Err(err).Msg("error in time") - return + log.Fatal().Err(err).Msg("error in time") } timezone, err := cmd.Flags().GetString("timezone") if err != nil || timezone == "" { - log.Error().Err(err).Msg("error in timezone") - return + log.Fatal().Err(err).Msg("error in timezone") } - bot, err := internal.NewBot(token, time, timezone) + db, err := cmd.Flags().GetString("database") if err != nil { - log.Error().Err(err).Msg("failed to create bot") - return + log.Fatal().Err(err).Msg("error in database") + } + + if db == "" { + defaultPath, err := os.UserHomeDir() + if err != nil { + log.Fatal().Err(err).Send() + } + db = filepath.Join(defaultPath, ".statusbot") + } + + bot, err := internal.NewBot(token, time, timezone, db) + if err != nil { + log.Fatal().Err(err).Msg("failed to create bot") } bot.Start() @@ -57,4 +67,5 @@ func init() { rootCmd.Flags().StringP("bot-token", "b", "", "Enter a valid telegram bot token") rootCmd.Flags().StringP("time", "t", "17:00", "Enter a valid time") rootCmd.Flags().StringP("timezone", "z", "Africa/Cairo", "Enter a valid timezone") + rootCmd.Flags().StringP("database", "d", "", "Enter path to store chats info") } diff --git a/internal/bot.go b/internal/bot/bot.go similarity index 88% rename from internal/bot.go rename to internal/bot/bot.go index 636f96c..59f9ce4 100644 --- a/internal/bot.go +++ b/internal/bot/bot.go @@ -7,6 +7,7 @@ import ( "time" _ "time/tzdata" + database "github.com/codescalers/statusbot/internal/db" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/rs/zerolog/log" ) @@ -18,10 +19,11 @@ type Bot struct { removeChan chan int64 time time.Time location *time.Location + db database.DB } // NewBot creates new bot with a valid bot api and communication channels -func NewBot(token string, inputTime string, timezone string) (Bot, error) { +func NewBot(token, inputTime, timezone, dbPath string) (Bot, error) { bot := Bot{} botAPI, err := tgbotapi.NewBotAPI(token) @@ -44,11 +46,17 @@ func NewBot(token string, inputTime string, timezone string) (Bot, error) { log.Printf("notfications is set to %s", parsedTime) + db, err := database.NewDB(dbPath) + if err != nil { + return bot, err + } + bot.location = loc bot.botAPI = *botAPI bot.time = parsedTime bot.addChan = make(chan int64) bot.removeChan = make(chan int64) + bot.db = db return bot, nil } @@ -88,7 +96,6 @@ func (bot Bot) Start() { } func (bot Bot) runBot() { - chatIDs := make(map[int64]bool) weekends := []time.Weekday{time.Friday, time.Saturday} // set ticker every day at 12:00 to update time with location in case of new changes in timezone. @@ -98,10 +105,10 @@ func (bot Bot) runBot() { for { select { case chatID := <-bot.addChan: - chatIDs[chatID] = true + bot.db.Update(chatID, database.ChatInfo{ChatID: chatID}) case chatID := <-bot.removeChan: - delete(chatIDs, chatID) + bot.db.Delete(chatID) case <-updateTicker.C: // parse the time with location again to make sure the timezone is always up to date @@ -114,16 +121,23 @@ func (bot Bot) runBot() { updateTicker.Reset(24 * time.Hour) case <-reminderTicker.C: + chats := bot.db.List() + // skip weekends if !slices.Contains(weekends, bot.time.Weekday()) { - for chatID := range chatIDs { - bot.sendReminder(chatID) + for _, chat := range chats { + bot.sendReminder(chat.ChatID) } } + bot.time = bot.time.AddDate(0, 0, 1) reminderTicker.Reset(24 * time.Hour) log.Printf("next notfications is set to %s", bot.time) } + + if err := bot.db.Save(); err != nil { + log.Fatal().Err(err).Msg("failed to save updates to db") + } } } diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..f5ff835 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,75 @@ +package db + +import ( + "encoding/json" + "os" + "path/filepath" +) + +type ChatInfo struct { + ChatID int64 +} + +type DB struct { + path string + chatsIDs map[int64]ChatInfo +} + +// NewDB open and load db if exist, or create new one if not exist +func NewDB(path string) (DB, error) { + if err := os.MkdirAll(path, 0777); err != nil { + return DB{}, err + } + + chatsIDs := make(map[int64]ChatInfo) + path = filepath.Join(path, "db.json") + + data, err := os.ReadFile(path) + if os.IsNotExist(err) || len(data) == 0 { + return DB{ + path: path, + chatsIDs: chatsIDs, + }, nil + } + if err != nil { + return DB{}, err + } + + if err := json.Unmarshal(data, &chatsIDs); err != nil { + return DB{}, err + } + + return DB{ + path: path, + chatsIDs: chatsIDs, + }, nil +} + +func (db *DB) Save() error { + file, err := json.MarshalIndent(db.chatsIDs, "", " ") + if err != nil { + return err + } + + return os.WriteFile(db.path, file, 0777) +} + +func (db *DB) Get(key int64) ChatInfo { + return db.chatsIDs[key] +} + +func (db *DB) Update(key int64, value ChatInfo) { + db.chatsIDs[key] = value +} + +func (db *DB) Delete(key int64) { + delete(db.chatsIDs, key) +} + +func (db *DB) List() []ChatInfo { + chatsInfo := make([]ChatInfo, 0, len(db.chatsIDs)) + for _, val := range db.chatsIDs { + chatsInfo = append(chatsInfo, val) + } + return chatsInfo +}