diff --git a/.gitignore b/.gitignore index c419d97..53d5164 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -_data \ No newline at end of file +_data +.idea \ No newline at end of file diff --git a/modules/macro/mod.go b/modules/macro/mod.go index 043db14..0a7258b 100644 --- a/modules/macro/mod.go +++ b/modules/macro/mod.go @@ -50,49 +50,45 @@ func (m *Module) Initialize(options modules.ModuleOptions) error { } func (m *Module) OnUpdate(update tg.APIUpdate) { - // Not a message? Ignore - if update.Message == nil { + if !utils.IsCommand(update, m.name, "macro") { return } - if utils.IsCommand(*update.Message, m.name, "macro") { - parts := strings.SplitN(*(update.Message.Text), " ", 3) - switch len(parts) { - case 2: - name := strings.ToLower(parts[1]) - item, ok := macros[name] - var out string - if ok { - out = fmt.Sprintf("%s\n%s\n%s - %s", name, item.Value, item.Author.Username, item.Time.Format("02-01-06 15:04")) - } else { - out = fmt.Sprintf("%s\nmacro inesistente\n(configura con /macro %s contenuto)", name, name) - } - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: update.Message.Chat.ChatID, - Text: out, - ReplyID: &update.Message.MessageID, - }) - case 3: - name := strings.ToLower(parts[1]) - macros[name] = Macro{ - Value: parts[2], - Author: update.Message.User, - Time: time.Now(), - } - m.save() - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: update.Message.Chat.ChatID, - Text: fmt.Sprintf("%s → %s", name, parts[2]), - ReplyID: &update.Message.MessageID, - }) - default: - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: update.Message.Chat.ChatID, - Text: "Sintassi\nLeggi: /macro nome-macro\nScrivi: /macro nome-macro contenuto macro", - ReplyID: &update.Message.MessageID, - }) + parts := strings.SplitN(*(update.Message.Text), " ", 3) + switch len(parts) { + case 2: + name := strings.ToLower(parts[1]) + item, ok := macros[name] + var out string + if ok { + out = fmt.Sprintf("%s\n%s\n%s - %s", name, item.Value, item.Author.Username, item.Time.Format("02-01-06 15:04")) + } else { + out = fmt.Sprintf("%s\nmacro inesistente\n(configura con /macro %s contenuto)", name, name) } - return + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: update.Message.Chat.ChatID, + Text: out, + ReplyID: &update.Message.MessageID, + }) + case 3: + name := strings.ToLower(parts[1]) + macros[name] = Macro{ + Value: parts[2], + Author: update.Message.User, + Time: time.Now(), + } + m.save() + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: update.Message.Chat.ChatID, + Text: fmt.Sprintf("%s → %s", name, parts[2]), + ReplyID: &update.Message.MessageID, + }) + default: + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: update.Message.Chat.ChatID, + Text: "Sintassi\nLeggi: /macro nome-macro\nScrivi: /macro nome-macro contenuto macro", + ReplyID: &update.Message.MessageID, + }) } } diff --git a/modules/metafora/mod.go b/modules/metafora/mod.go index 20fa1aa..63ce6d7 100644 --- a/modules/metafora/mod.go +++ b/modules/metafora/mod.go @@ -44,18 +44,14 @@ func (m *Module) Initialize(options modules.ModuleOptions) error { } func (m *Module) OnUpdate(update tg.APIUpdate) { - // Not a message? Ignore - if update.Message == nil { + if !utils.IsCommand(update, m.name, "metafora") { return } - if utils.IsCommand(*update.Message, m.name, "metafora") { - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: update.Message.Chat.ChatID, - Text: metaforaAPI(), - }) - return - } + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: update.Message.Chat.ChatID, + Text: metaforaAPI(), + }) } func metaforaAPI() string { diff --git a/modules/proverbio/mod.go b/modules/proverbio/mod.go index becc147..6bd0764 100644 --- a/modules/proverbio/mod.go +++ b/modules/proverbio/mod.go @@ -46,18 +46,14 @@ func (m *Module) Initialize(options modules.ModuleOptions) error { } func (m *Module) OnUpdate(update tg.APIUpdate) { - // Not a message? Ignore - if update.Message == nil { + if !utils.IsCommand(update, m.name, "proverbio") { return } - if utils.IsCommand(*update.Message, m.name, "proverbio") { - start := rand.Intn(len(proverbipairs.start)) - end := rand.Intn(len(proverbipairs.end)) - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: update.Message.Chat.ChatID, - Text: fmt.Sprintf("Dice il saggio:\n%s %s", proverbipairs.start[start], proverbipairs.end[end]), - }) - return - } + start := rand.Intn(len(proverbipairs.start)) + end := rand.Intn(len(proverbipairs.end)) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: update.Message.Chat.ChatID, + Text: fmt.Sprintf("Dice il saggio:\n%s %s", proverbipairs.start[start], proverbipairs.end[end]), + }) } diff --git a/modules/remind/cmd.remind.go b/modules/remind/cmd.remind.go new file mode 100644 index 0000000..ce682b8 --- /dev/null +++ b/modules/remind/cmd.remind.go @@ -0,0 +1,102 @@ +package remind + +import ( + "git.fromouter.space/hamcha/tg" + "strconv" + "strings" + "time" +) + +func (m *Module) cmdRicordami(message *tg.APIMessage) { + // 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{ + ReminderID: id, + 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) + m.save() + go m.schedule(id) + + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "Ok, vedrò di avvisarti " + formatWhen(timestamp, loc), + 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) + m.save() +} diff --git a/modules/remind/cmd.reminders.go b/modules/remind/cmd.reminders.go new file mode 100644 index 0000000..daa77c6 --- /dev/null +++ b/modules/remind/cmd.reminders.go @@ -0,0 +1,123 @@ +package remind + +import ( + "fmt" + "git.fromouter.space/hamcha/tg" + "log" + "sort" + "time" +) + +func (m *Module) cmdReminder(message *tg.APIMessage) { + + // 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 + } + + reminders := remindersOfUser(message.User.UserID) + + if len(reminders) == 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: renderReminderEntry(0, reminders, defaultLocation), + ReplyID: &message.MessageID, + ReplyMarkup: &tg.APIInlineKeyboardMarkup{ + InlineKeyboard: [][]tg.APIInlineKeyboardButton{makeReminderInlineKeyboard(0, reminders)}, + }, + }) + } +} + +func (m *Module) handleCallback(callback *tg.APICallbackQuery) { + if callback.Data == nil { + return + } + + var command, reminderID string + _, err := fmt.Sscan(*callback.Data, &command, &reminderID) + if err != nil { + log.Println("[remind] WARN: weird callback received: " + err.Error()) + return + } + + reminders := remindersOfUser(callback.User.UserID) + + switch command { + case "GET": + current := 0 + for index, reminder := range reminders { + if reminder.ReminderID == reminderID { + current = index + break + } + } + // Modify message with requested reminder + m.client.EditText(tg.ClientEditTextData{ + ChatID: callback.Message.Chat.ChatID, + MessageID: callback.Message.MessageID, + Text: renderReminderEntry(current, reminders, defaultLocation), + ReplyMarkup: &tg.APIInlineKeyboardMarkup{ + InlineKeyboard: [][]tg.APIInlineKeyboardButton{makeReminderInlineKeyboard(current, reminders)}, + }, + }) + } +} + +func remindersOfUser(userID int64) (reminders []Reminder) { + for _, reminder := range pending.Copy() { + if reminder.TargetID == userID { + reminders = append(reminders, reminder) + } + } + if len(reminders) > 0 { + sort.Slice(reminders, func(a, b int) bool { return reminders[a].When < reminders[b].When }) + } + return +} + +func renderReminderEntry(current int, reminders []Reminder, loc *time.Location) string { + reminder := reminders[current] + when := time.Unix(reminder.When, 0) + message := reminder.Text + if reminder.Reference != nil { + if message != "" { + message += ", " + } + message += fmt.Sprintf("https://t.me/c/%d/%d", reminder.Reference.Chat, reminder.Reference.Message) + } + + return fmt.Sprintf("Reminder %d (/%d):\n%s\nQuando: %s", current+1, len(reminders), message, formatWhen(when, loc)) +} + +func makeReminderInlineKeyboard(current int, reminders []Reminder) (buttons []tg.APIInlineKeyboardButton) { + if current > 0 { + buttons = append(buttons, tg.APIInlineKeyboardButton{ + Text: "⬅", + CallbackData: fmt.Sprintf("GET %s", reminders[current-1].ReminderID), + }) + } + buttons = append(buttons, tg.APIInlineKeyboardButton{ + Text: "❌", + CallbackData: fmt.Sprintf("DELETE %s", reminders[current].ReminderID), + }) + if current < len(reminders)-1 { + buttons = append(buttons, tg.APIInlineKeyboardButton{ + Text: "➡", + CallbackData: fmt.Sprintf("GET %s", reminders[current+1].ReminderID), + }) + } + return +} diff --git a/modules/remind/data.go b/modules/remind/data.go new file mode 100644 index 0000000..db8c84e --- /dev/null +++ b/modules/remind/data.go @@ -0,0 +1,19 @@ +package remind + +import ( + "github.com/cockroachdb/pebble" + jsoniter "github.com/json-iterator/go" + "log" +) + +func (m *Module) save() { + byt, err := jsoniter.ConfigFastest.Marshal(pending.Copy()) + if err != nil { + log.Println("[remind] WARN: Could not encode reminders: " + err.Error()) + } + err = m.kv.Set([]byte(reminderKey), byt, &pebble.WriteOptions{Sync: true}) + if err != nil { + log.Println("[remind] WARN: Could not save reminders to db: " + err.Error()) + return + } +} diff --git a/modules/remind/mod.go b/modules/remind/mod.go index 10a0e87..4502d13 100644 --- a/modules/remind/mod.go +++ b/modules/remind/mod.go @@ -2,12 +2,8 @@ package remind import ( "errors" - "fmt" "log" - "strconv" - "strings" "time" - "unicode" "git.fromouter.space/crunchy-rocks/clessy-ng/modules" "git.fromouter.space/crunchy-rocks/clessy-ng/utils" @@ -15,14 +11,14 @@ import ( "git.fromouter.space/hamcha/tg" "git.sr.ht/~hamcha/containers" "github.com/cockroachdb/pebble" - jsoniter "github.com/json-iterator/go" ) type Reminder struct { - TargetID int64 - When int64 - Text string - Reference *ReminderReference + ReminderID string + TargetID int64 + When int64 + Text string + Reference *ReminderReference } type ReminderReference struct { @@ -63,6 +59,20 @@ func (m *Module) Initialize(options modules.ModuleOptions) error { return err } } + + // Fix old reminders + dirty := false + for reminderID, reminder := range reminders { + if reminder.ReminderID == "" { + reminder.ReminderID = reminderID + dirty = true + } + } + if dirty { + m.save() + } + + // Set schedule for id, reminder := range reminders { pending.SetKey(id, reminder) go m.schedule(id) @@ -73,240 +83,18 @@ func (m *Module) Initialize(options modules.ModuleOptions) error { } func (m *Module) OnUpdate(update tg.APIUpdate) { - // Not a message? Ignore - if update.Message == nil { - return + // Check for updates to existing messages + if update.Callback != nil { + m.handleCallback(update.Callback) } - 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) - m.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, - }) + if utils.IsCommand(update, m.name, "ricordami") || utils.IsCommand(update, m.name, "remind") { + m.cmdRicordami(update.Message) 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) - m.save() -} - -func (m *Module) save() { - byt, err := jsoniter.ConfigFastest.Marshal(pending.Copy()) - if err != nil { - log.Println("[remind] WARN: Could not encode reminders: " + err.Error()) - } - err = m.kv.Set([]byte(reminderKey), byt, &pebble.WriteOptions{Sync: true}) - if err != nil { - log.Println("[remind] WARN: Could not save reminders to db: " + err.Error()) + if utils.IsCommand(update, m.name, "reminders") { + m.cmdReminder(update.Message) return } } - -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)") - } - now = now.Add(dur) - nextIndex := strings.IndexRune(remaining, sep) - remaining = remaining[nextIndex+1:] - } - fmt.Printf("tot: %s", now.Sub(time.Now())) - return true, 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 -} diff --git a/modules/remind/utils.go b/modules/remind/utils.go new file mode 100644 index 0000000..467082e --- /dev/null +++ b/modules/remind/utils.go @@ -0,0 +1,103 @@ +package remind + +import ( + "fmt" + "strings" + "time" + "unicode" +) + +func formatWhen(timestamp time.Time, loc *time.Location) string { + day := "più tardi" + _, todaym, todayd := time.Now().Date() + _, targetm, targetd := timestamp.Date() + if todaym != targetm || todayd != targetd { + day = "il " + timestamp.In(loc).Format("2/1") + } + + return fmt.Sprintf("%s alle %s", day, timestamp.In(loc).Format("15:04:05")) +} + +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)") + } + now = now.Add(dur) + nextIndex := strings.IndexRune(remaining, sep) + remaining = remaining[nextIndex+1:] + } + fmt.Printf("tot: %s", now.Sub(time.Now())) + return true, 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 +} diff --git a/modules/unsplash/mod.go b/modules/unsplash/mod.go index 9d64c81..b2b392f 100644 --- a/modules/unsplash/mod.go +++ b/modules/unsplash/mod.go @@ -73,53 +73,21 @@ func (m *Module) Initialize(options modules.ModuleOptions) error { } func (m *Module) OnUpdate(update tg.APIUpdate) { - // Not a message? Ignore - if update.Message == nil { + if !utils.IsCommand(update, m.name, "unsplash") { return } message := *update.Message - if utils.IsCommand(message, m.name, "unsplash") { - text := "" - user := message.User + text := "" + user := message.User - if message.ReplyTo != nil { - switch { - case message.ReplyTo.Text != nil: - text = *(message.ReplyTo.Text) - case message.ReplyTo.Caption != nil: - text = *(message.ReplyTo.Caption) - default: - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: message.Chat.ChatID, - Text: "Non c'e' niente di 'ispiratore' in questo..", - ReplyID: &message.MessageID, - }) - return - } - - // For forwarded message take the original user - if message.FwdUser != nil { - user = *message.FwdUser - } else { - user = message.ReplyTo.User - } - } else { - if strings.Index(*(message.Text), " ") > 0 { - text = strings.TrimSpace(strings.SplitN(*(message.Text), " ", 2)[1]) - } - } - - // Cleanup chars - text = strings.Map(stripUnreadable, text) - - author := user.FirstName - if user.LastName != "" { - author = user.FirstName + " " + user.LastName - } - author += " (" + user.Username + ")" - - if strings.TrimSpace(text) == "" { + if message.ReplyTo != nil { + switch { + case message.ReplyTo.Text != nil: + text = *(message.ReplyTo.Text) + case message.ReplyTo.Caption != nil: + text = *(message.ReplyTo.Caption) + default: m.client.SendTextMessage(tg.ClientTextMessageData{ ChatID: message.Chat.ChatID, Text: "Non c'e' niente di 'ispiratore' in questo..", @@ -128,137 +96,166 @@ func (m *Module) OnUpdate(update tg.APIUpdate) { return } - file, err := os.Open(pics[rand.Intn(len(pics))]) - if err != nil { - log.Printf("[unsplash] Could not open original image file: %s\n", err.Error()) - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: message.Chat.ChatID, - Text: "ERRORE! @hamcha controlla la console!", - ReplyID: &message.MessageID, - }) - return + // For forwarded message take the original user + if message.FwdUser != nil { + user = *message.FwdUser + } else { + user = message.ReplyTo.User } - defer file.Close() - - img, _, err := image.Decode(file) - if err != nil { - log.Printf("[unsplash] Image decode error: %s\n", err.Error()) - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: message.Chat.ChatID, - Text: "ERRORE! @hamcha controlla la console!", - ReplyID: &message.MessageID, - }) - return + } else { + if strings.Index(*(message.Text), " ") > 0 { + text = strings.TrimSpace(strings.SplitN(*(message.Text), " ", 2)[1]) } + } - m.client.SendChatAction(tg.ClientChatActionData{ - ChatID: message.Chat.ChatID, - Action: tg.ActionUploadingPhoto, + // Cleanup chars + text = strings.Map(stripUnreadable, text) + + author := user.FirstName + if user.LastName != "" { + author = user.FirstName + " " + user.LastName + } + author += " (" + user.Username + ")" + + if strings.TrimSpace(text) == "" { + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "Non c'e' niente di 'ispiratore' in questo..", + ReplyID: &message.MessageID, }) + return + } - // Darken image - img = imaging.AdjustBrightness(imaging.AdjustGamma(imaging.AdjustSigmoid(img, 0.5, -6.0), 0.8), -20) + file, err := os.Open(pics[rand.Intn(len(pics))]) + if err != nil { + log.Printf("[unsplash] Could not open original image file: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! @hamcha controlla la console!", + ReplyID: &message.MessageID, + }) + return + } + defer file.Close() - // Create target image - bounds := img.Bounds() - iwidth := float64(bounds.Size().X) - iheight := float64(bounds.Size().Y) + img, _, err := image.Decode(file) + if err != nil { + log.Printf("[unsplash] Image decode error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! @hamcha controlla la console!", + ReplyID: &message.MessageID, + }) + return + } - timg := image.NewRGBA(bounds) - gc := draw2dimg.NewGraphicContext(timg) - gc.Emojis = m.emojis - gc.SetFontData(quoteFontData) - gc.DrawImage(img) - gc.SetStrokeColor(image.Black) - gc.SetFillColor(image.White) + m.client.SendChatAction(tg.ClientChatActionData{ + ChatID: message.Chat.ChatID, + Action: tg.ActionUploadingPhoto, + }) - text = strings.ToUpper(strings.TrimSpace(text)) - gc.Restore() - gc.Save() + // Darken image + img = imaging.AdjustBrightness(imaging.AdjustGamma(imaging.AdjustSigmoid(img, 0.5, -6.0), 0.8), -20) - // Detect appropriate font size - scale := iheight / iwidth * (iwidth / 10) * 0.8 - gc.SetFontSize(scale) - gc.SetLineWidth(scale / 15) + // Create target image + bounds := img.Bounds() + iwidth := float64(bounds.Size().X) + iheight := float64(bounds.Size().Y) - // Get NEW bounds - left, top, right, bottom := gc.GetStringBounds(text) + timg := image.NewRGBA(bounds) + gc := draw2dimg.NewGraphicContext(timg) + gc.Emojis = m.emojis + gc.SetFontData(quoteFontData) + gc.DrawImage(img) + gc.SetStrokeColor(image.Black) + gc.SetFillColor(image.White) - width := right - left - texts := []string{text} - if width*1.2 > iwidth { - // Split text - texts = utils.SplitCenter(text) + text = strings.ToUpper(strings.TrimSpace(text)) + gc.Restore() + gc.Save() - // Get longest line - longer := float64(0) - longid := 0 - widths := make([]float64, len(texts)) - for id := range texts { - tleft, _, tright, _ := gc.GetStringBounds(texts[id]) - widths[id] = tright - tleft - if width > longer { - longer = widths[id] - longid = id - } - } + // Detect appropriate font size + scale := iheight / iwidth * (iwidth / 10) * 0.8 + gc.SetFontSize(scale) + gc.SetLineWidth(scale / 15) - // Still too big? Decrease font size again - iter := 0 - for width*1.2 > iwidth && iter < 10 { - scale *= (0.9 - 0.05*float64(iter)) - gc.SetFontSize(scale) - left, top, right, bottom = gc.GetStringBounds(texts[longid]) - width = right - left - iter++ + // Get NEW bounds + left, top, right, bottom := gc.GetStringBounds(text) + + width := right - left + texts := []string{text} + if width*1.2 > iwidth { + // Split text + texts = utils.SplitCenter(text) + + // Get longest line + longer := float64(0) + longid := 0 + widths := make([]float64, len(texts)) + for id := range texts { + tleft, _, tright, _ := gc.GetStringBounds(texts[id]) + widths[id] = tright - tleft + if width > longer { + longer = widths[id] + longid = id } } - texts = append(texts, author) - height := bottom - top + 20 - margin := float64(height / 50) - txtheight := (height + margin) * float64(len(texts)) + // Still too big? Decrease font size again + iter := 0 + for width*1.2 > iwidth && iter < 10 { + scale *= (0.9 - 0.05*float64(iter)) + gc.SetFontSize(scale) + left, top, right, bottom = gc.GetStringBounds(texts[longid]) + width = right - left + iter++ + } + } + texts = append(texts, author) + height := bottom - top + 20 + margin := float64(height / 50) + txtheight := (height + margin) * float64(len(texts)) + + gc.Save() + for id, txt := range texts { gc.Save() - for id, txt := range texts { - gc.Save() + left, _, right, _ = gc.GetStringBounds(txt) + width = right - left + + x := (iwidth - width) / 2 + y := (iheight-txtheight)/2 + (height+margin*2)*float64(id+1) + if id == len(texts)-1 { + gc.SetFontSize(scale * 0.7) left, _, right, _ = gc.GetStringBounds(txt) width = right - left - - x := (iwidth - width) / 2 - y := (iheight-txtheight)/2 + (height+margin*2)*float64(id+1) - if id == len(texts)-1 { - gc.SetFontSize(scale * 0.7) - left, _, right, _ = gc.GetStringBounds(txt) - width = right - left - x = (iwidth - width) / 1.5 - y = (iheight-txtheight)/2 + (height+margin)*float64(id+1) + margin*6 - } - - gc.Translate(x, y) - gc.StrokeString(txt) - gc.FillString(txt) - gc.Restore() + x = (iwidth - width) / 1.5 + y = (iheight-txtheight)/2 + (height+margin)*float64(id+1) + margin*6 } - buf := new(bytes.Buffer) - err = jpeg.Encode(buf, timg, &(jpeg.Options{Quality: 80})) - if err != nil { - log.Printf("[unsplash] Image encode error: %s\n", err.Error()) - m.client.SendTextMessage(tg.ClientTextMessageData{ - ChatID: message.Chat.ChatID, - Text: "ERRORE! @hamcha controlla la console!", - ReplyID: &message.MessageID, - }) - return - } - m.client.SendPhoto(tg.ClientPhotoData{ - ChatID: message.Chat.ChatID, - Bytes: buf.Bytes(), - Filename: "quote.jpg", - ReplyID: &message.MessageID, - }) + gc.Translate(x, y) + gc.StrokeString(txt) + gc.FillString(txt) + gc.Restore() } + + buf := new(bytes.Buffer) + err = jpeg.Encode(buf, timg, &(jpeg.Options{Quality: 80})) + if err != nil { + log.Printf("[unsplash] Image encode error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! @hamcha controlla la console!", + ReplyID: &message.MessageID, + }) + return + } + m.client.SendPhoto(tg.ClientPhotoData{ + ChatID: message.Chat.ChatID, + Bytes: buf.Bytes(), + Filename: "quote.jpg", + ReplyID: &message.MessageID, + }) } func stripUnreadable(r rune) rune { diff --git a/utils/command.go b/utils/command.go index 1c18521..87fc91f 100644 --- a/utils/command.go +++ b/utils/command.go @@ -6,12 +6,17 @@ import ( "git.fromouter.space/hamcha/tg" ) -func IsCommand(update tg.APIMessage, botname string, cmdname string) bool { - if update.Text == nil { +func IsCommand(update tg.APIUpdate, botname string, cmdname string) bool { + if update.Message == nil { + return false + } + message := update.Message + + if message.Text == nil { return false } - text := strings.TrimSpace(*(update.Text)) + text := strings.TrimSpace(*(message.Text)) shortcmd := "/" + cmdname fullcmd := shortcmd + "@" + botname