Compare commits

...

2 commits

Author SHA1 Message Date
fda955ad97
fix sourcehut blacklisting google
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-10 09:40:08 +01:00
417f9f9345
Refactor reminders into a more powerful package 2022-11-28 11:06:15 +01:00
12 changed files with 578 additions and 452 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
_data
.idea

View file

@ -7,7 +7,7 @@ RUN apk --no-cache add ca-certificates
WORKDIR /src
COPY ./go.mod ./go.sum ./
RUN go mod download
RUN GOPRIVATE=git.sr.ht go mod download
COPY ./ ./

View file

@ -50,12 +50,10 @@ 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:
@ -92,8 +90,6 @@ func (m *Module) OnUpdate(update tg.APIUpdate) {
ReplyID: &update.Message.MessageID,
})
}
return
}
}
func (m *Module) save() {

View file

@ -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
}
}
func metaforaAPI() string {

View file

@ -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("<b>Dice il saggio:</b>\n%s %s", proverbipairs.start[start], proverbipairs.end[end]),
})
return
}
}

View file

@ -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: "<b>Sintassi</b>\n/ricordami <i>[quando]</i> Messaggio\n\n<b>Formati supportati per [quando]</b>:\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: "<b>Heyla! Mi avevi chiesto di ricordarti questo:</b>\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()
}

View file

@ -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("<b>Reminder %d</b> (/%d):\n%s\n<b>Quando:</b> %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
}

19
modules/remind/data.go Normal file
View file

@ -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
}
}

View file

@ -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,10 +11,10 @@ import (
"git.fromouter.space/hamcha/tg"
"git.sr.ht/~hamcha/containers"
"github.com/cockroachdb/pebble"
jsoniter "github.com/json-iterator/go"
)
type Reminder struct {
ReminderID string
TargetID int64
When int64
Text string
@ -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
}
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, "")
// Check for updates to existing messages
if update.Callback != nil {
m.handleCallback(update.Callback)
}
if len(parts) < 3 {
m.client.SendTextMessage(tg.ClientTextMessageData{
ChatID: message.Chat.ChatID,
Text: "<b>Sintassi</b>\n/ricordami <i>[quando]</i> Messaggio\n\n<b>Formati supportati per [quando]</b>:\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,
})
if utils.IsCommand(update, m.name, "ricordami") || utils.IsCommand(update, m.name, "remind") {
m.cmdRicordami(update.Message)
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,
})
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 <b>%d</b> 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: "<b>Heyla! Mi avevi chiesto di ricordarti questo:</b>\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
}

103
modules/remind/utils.go Normal file
View file

@ -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
}

View file

@ -73,13 +73,11 @@ 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
@ -258,7 +256,6 @@ func (m *Module) OnUpdate(update tg.APIUpdate) {
Filename: "quote.jpg",
ReplyID: &message.MessageID,
})
}
}
func stripUnreadable(r rune) rune {

View file

@ -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