diff --git a/go.mod b/go.mod
index 3d664f0..a07efee 100644
--- a/go.mod
+++ b/go.mod
@@ -2,10 +2,13 @@ module git.fromouter.space/crunchy-rocks/clessy-ng
go 1.18
-require git.fromouter.space/hamcha/tg v0.1.0
+require (
+ git.fromouter.space/hamcha/tg v0.1.0
+ git.sr.ht/~hamcha/containers v0.0.3
+ github.com/json-iterator/go v1.1.12
+)
require (
- github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
)
diff --git a/go.sum b/go.sum
index caf6e52..2a890c5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
git.fromouter.space/hamcha/tg v0.1.0 h1:cJwL8pElkBtaDn7Bxa14zvlnBTTK8LdcCtcbBg7hEvk=
git.fromouter.space/hamcha/tg v0.1.0/go.mod h1:aIFj7n5FP+Zr/Zv6I6Kq4ZqhRxC12gXFQcC3iOakv9M=
+git.sr.ht/~hamcha/containers v0.0.3 h1:obG9X8s5iOIahVe+EGpkBDYmUAO78oTi9Y9gRurt334=
+git.sr.ht/~hamcha/containers v0.0.3/go.mod h1:RiZphUpy9t6EnL4Gf6uzByM9QrBoqRCEPo7kz2wzbhE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/main.go b/main.go
index 200df74..817466b 100644
--- a/main.go
+++ b/main.go
@@ -10,6 +10,7 @@ import (
"git.fromouter.space/crunchy-rocks/clessy-ng/modules/macro"
"git.fromouter.space/crunchy-rocks/clessy-ng/modules/metafora"
"git.fromouter.space/crunchy-rocks/clessy-ng/modules/proverbio"
+ "git.fromouter.space/crunchy-rocks/clessy-ng/modules/remind"
"git.fromouter.space/hamcha/tg"
)
@@ -18,6 +19,7 @@ var mods = map[string]modules.Module{
"metafora": &metafora.Module{},
"proverbio": &proverbio.Module{},
"macro": ¯o.Module{},
+ "remind": &remind.Module{},
}
func checkErr(err error, message string, args ...interface{}) {
diff --git a/modules/remind/mod.go b/modules/remind/mod.go
new file mode 100644
index 0000000..28c3b60
--- /dev/null
+++ b/modules/remind/mod.go
@@ -0,0 +1,322 @@
+package remind
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+ "unicode"
+
+ "git.fromouter.space/crunchy-rocks/clessy-ng/utils"
+ "git.fromouter.space/hamcha/tg"
+ "git.sr.ht/~hamcha/containers"
+ jsoniter "github.com/json-iterator/go"
+)
+
+type Reminder struct {
+ TargetID int64
+ When int64
+ Text string
+ Reference *ReminderReference
+}
+
+type ReminderReference struct {
+ Chat int64
+ Message int64
+}
+
+var remindPath string
+
+const reminderMaxDuration = time.Hour * 24 * 30 * 3
+
+var defaultLocation *time.Location = nil
+
+var pending = containers.NewRWSyncMap[string, Reminder]()
+
+type Module struct {
+ client *tg.Telegram
+ name string
+}
+
+func (m *Module) Initialize(api *tg.Telegram, name string) error {
+ m.client = api
+ m.name = name
+
+ var err error
+ defaultLocation, err = time.LoadLocation("Europe/Rome")
+ if err != nil {
+ log.Fatalf("[remind] Something is really wrong: %s\n", err.Error())
+ return err
+ }
+
+ file, err := os.Open(remindPath)
+ if err != nil {
+ if !errors.Is(err, os.ErrNotExist) {
+ return err
+
+ }
+ log.Println("[remind] WARN: Reminder file not found, no reminders loaded")
+ return nil
+ }
+ defer file.Close()
+
+ var reminders map[string]Reminder
+ err = jsoniter.ConfigFastest.NewDecoder(file).Decode(&reminders)
+ if err != nil {
+ log.Println("[remind] WARN: Could not load pending reminders (malformed or unreadable file): " + err.Error())
+ return err
+ }
+ for id, reminder := range reminders {
+ pending.SetKey(id, reminder)
+ go m.schedule(id)
+ }
+ log.Printf("[remind] Loaded %d pending reminders from %s\n", len(reminders), remindPath)
+
+ return nil
+}
+
+func (m *Module) OnUpdate(update tg.APIUpdate) {
+ // Not a message? Ignore
+ if update.Message == nil {
+ return
+ }
+ message := *update.Message
+
+ if utils.IsCommand(message, m.name, "ricordami") {
+ // Supported formats:
+ // Xs/m/h/d => in X seconds/minutes/hours/days
+ // HH:MM => at HH:MM (24 hour format)
+ // HH:MM:SS => at HH:MM:SS (24 hour format)
+ // dd/mm/yyyy => same hour, specific date
+ // dd/mm/yyyy-HH:MM => specific hour, specific dat
+ // dd/mm/yyyy-HH:MM:SS => specific hour, specific date
+
+ parts := strings.SplitN(*message.Text, " ", 3)
+
+ // Little hack to allow text-less reminders with replies
+ if len(parts) == 2 && message.ReplyTo != nil {
+ parts = append(parts, "")
+ }
+
+ if len(parts) < 3 {
+ m.client.SendTextMessage(tg.ClientTextMessageData{
+ ChatID: message.Chat.ChatID,
+ Text: "Sintassi\n/ricordami [quando] Messaggio\n\nFormati supportati per [quando]:\n 10s 10m 10h 10d (secondi/minuti/ore/giorni)\n 13:20 15:55:01 (ora dello stesso giorno, formato 24h)\n 11/02/2099 11/02/2099-11:20:01 (giorno diverso, stessa ora [1] o specifica [2])",
+ ReplyID: &message.MessageID,
+ })
+ return
+ }
+
+ format := parts[1]
+ remindText := parts[2]
+
+ loc := defaultLocation
+ /*TODO REDO
+ if uloc, ok := tzlocs[update.User.UserID]; ok {
+ loc = uloc
+ }
+ */
+ timestamp, err := parseDuration(format, loc)
+ if err != nil {
+ m.client.SendTextMessage(tg.ClientTextMessageData{
+ ChatID: message.Chat.ChatID,
+ Text: err.Error(),
+ ReplyID: &message.MessageID,
+ })
+ return
+ }
+
+ id := strconv.FormatInt(message.Chat.ChatID, 36) + "-" + strconv.FormatInt(message.MessageID, 36)
+ reminder := Reminder{
+ TargetID: message.User.UserID,
+ When: timestamp.Unix(),
+ Text: remindText,
+ }
+ if message.ReplyTo != nil {
+ reminder.Reference = &ReminderReference{
+ Chat: message.Chat.ChatID,
+ Message: message.ReplyTo.MessageID,
+ }
+ }
+ pending.SetKey(id, reminder)
+ save()
+ go m.schedule(id)
+
+ whenday := "più tardi"
+ _, todaym, todayd := time.Now().Date()
+ _, targetm, targetd := timestamp.Date()
+ if todaym != targetm || todayd != targetd {
+ whenday = "il " + timestamp.In(loc).Format("2/1")
+ }
+ whentime := "alle " + timestamp.In(loc).Format("15:04:05")
+ m.client.SendTextMessage(tg.ClientTextMessageData{
+ ChatID: message.Chat.ChatID,
+ Text: "Ok, vedrò di avvisarti " + whenday + " " + whentime,
+ ReplyID: &message.MessageID,
+ })
+ return
+ }
+
+ if utils.IsCommand(message, m.name, "reminders") {
+ // Should only work in private chats
+ if message.Chat.Type != tg.ChatTypePrivate {
+
+ m.client.SendTextMessage(tg.ClientTextMessageData{
+ ChatID: message.Chat.ChatID,
+ Text: "Per favore chiedimi in privato dei reminder",
+ ReplyID: &message.MessageID,
+ })
+ return
+ }
+
+ useritems := []Reminder{}
+ for _, reminder := range pending.Copy() {
+ if reminder.TargetID == message.User.UserID {
+ useritems = append(useritems, reminder)
+ }
+ }
+
+ if len(useritems) == 0 {
+ m.client.SendTextMessage(tg.ClientTextMessageData{
+ ChatID: message.Chat.ChatID,
+ Text: "Non ci sono reminder in coda per te",
+ ReplyID: &message.MessageID,
+ })
+ } else {
+ m.client.SendTextMessage(tg.ClientTextMessageData{
+ ChatID: message.Chat.ChatID,
+ Text: fmt.Sprintf("Ci sono %d reminder in coda per te", len(useritems)),
+ ReplyID: &message.MessageID,
+ })
+ }
+ }
+}
+
+func (m *Module) schedule(id string) {
+ // Get reminder
+ r := pending.GetKey(id)
+ remaining := r.When - time.Now().Unix()
+ if remaining > 0 {
+ // Wait remaining time
+ time.Sleep(time.Second * time.Duration(remaining))
+ }
+ // Remind!
+ m.client.SendTextMessage(tg.ClientTextMessageData{
+ ChatID: r.TargetID,
+ Text: "Heyla! Mi avevi chiesto di ricordarti questo:\n" + r.Text,
+ ReplyID: nil,
+ })
+ if r.Reference != nil {
+ m.client.ForwardMessage(tg.ClientForwardMessageData{
+ ChatID: r.TargetID,
+ FromChatID: r.Reference.Chat,
+ MessageID: r.Reference.Message,
+ })
+ }
+ // Delete reminder from pending list and save list to disk
+ pending.DeleteKey(id)
+ save()
+}
+
+func save() {
+ file, err := os.Create(remindPath)
+ if err != nil {
+ log.Println("[remind] WARN: Could not open pending reminders file: " + err.Error())
+ return
+ }
+ err = json.NewEncoder(file).Encode(pending)
+ if err != nil {
+ log.Println("[remind] WARN: Could not save pending reminders into file: " + err.Error())
+ }
+}
+
+func isSscanfValid(n int, err error) bool {
+ return err == nil
+}
+
+func scanMixedDelay(str string) (bool, time.Time, error) {
+ remaining := str
+ now := time.Now()
+ num := 0
+ sep := ' '
+ for len(remaining) > 1 {
+ _, err := fmt.Sscanf(remaining, "%d%c", &num, &sep)
+ if err != nil {
+ return false, now, err
+ }
+ dur := time.Duration(num)
+ switch unicode.ToLower(sep) {
+ case 's':
+ dur *= time.Second
+ case 'm':
+ dur *= time.Minute
+ case 'h':
+ dur *= time.Hour
+ case 'd':
+ dur *= time.Hour * 24
+ default:
+ return true, now, fmt.Errorf("La durata ha una unità che non conosco, usa una di queste: s (secondi) m (minuti) h (ore) d (giorni)")
+ }
+ return true, now.Add(dur), nil
+ }
+ /* TO FIX
+ now = now.Add(dur)
+ remaining = remaining[scanned:]
+ }
+ return true, now, nil
+ */
+ return false, now, nil
+}
+
+func parseDuration(date string, loc *time.Location) (time.Time, error) {
+ now := time.Now().In(loc)
+ hour := now.Hour()
+ min := now.Minute()
+ sec := now.Second()
+ day := now.Day()
+ month := now.Month()
+ year := now.Year()
+ dayunspecified := false
+ isDurationFmt, duration, err := scanMixedDelay(date)
+ switch {
+ case isSscanfValid(fmt.Sscanf(date, "%d/%d/%d-%d:%d:%d", &day, &month, &year, &hour, &min, &sec)):
+ case isSscanfValid(fmt.Sscanf(date, "%d/%d/%d-%d:%d", &day, &month, &year, &hour, &min)):
+ sec = 0
+ case isSscanfValid(fmt.Sscanf(date, "%d/%d/%d", &day, &month, &year)):
+ hour = now.Hour()
+ min = now.Minute()
+ sec = now.Second()
+ case isSscanfValid(fmt.Sscanf(date, "%d:%d:%d", &hour, &min, &sec)):
+ day = now.Day()
+ month = now.Month()
+ year = now.Year()
+ dayunspecified = true
+ case isSscanfValid(fmt.Sscanf(date, "%d:%d", &hour, &min)):
+ day = now.Day()
+ month = now.Month()
+ year = now.Year()
+ sec = 0
+ dayunspecified = true
+ case isDurationFmt:
+ return duration, err
+ default:
+ return now, fmt.Errorf("Non capisco quando dovrei ricordartelo!")
+ }
+ targetDate := time.Date(year, month, day, hour, min, sec, 0, loc)
+ if targetDate.Before(now) {
+ // If day was not specified assume tomorrow
+ if dayunspecified {
+ targetDate = targetDate.Add(time.Hour * 24)
+ } else {
+ return now, fmt.Errorf("Non posso ricordarti cose nel passato!")
+ }
+ }
+ if targetDate.After(now.Add(reminderMaxDuration)) {
+ return now, fmt.Errorf("Non credo riuscirei a ricordarmi qualcosa per così tanto")
+ }
+ return targetDate, nil
+}