package main import ( "encoding/json" "fmt" "log" "os" "strconv" "strings" "time" "unicode" "github.com/hamcha/tg" ) var remindpath *string type Reminder struct { TargetID int64 When int64 Text string Reference *ReminderReference } type ReminderReference struct { Chat int64 Message int64 } var reminders map[string]Reminder const ReminderMaxDuration = time.Hour * 24 * 30 * 3 func remind_init() { reminders = make(map[string]Reminder) file, err := os.Open(*remindpath) if err != nil { return } defer file.Close() err = json.NewDecoder(file).Decode(&reminders) if err != nil { log.Println("[remind] WARN: Could not load pending reminders (malformed or unreadable file): " + err.Error()) return } for id := range reminders { go runreminder(id) } log.Printf("[remind] Loaded %d pending reminders from %s\n", len(reminders), *remindpath) } func remind_message(broker *tg.Broker, update tg.APIMessage) { if isCommand(update, "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(*(update.Text), " ", 3) // Little hack to allow text-less reminders with replies if len(parts) == 2 && update.ReplyTo != nil { parts = append(parts, "") } if len(parts) < 3 { broker.SendTextMessage(update.Chat, "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])", &update.MessageID) return } format := parts[1] message := parts[2] timestamp, err := parseDuration(format) if err != nil { broker.SendTextMessage(update.Chat, err.Error(), &update.MessageID) return } id := strconv.FormatInt(update.Chat.ChatID, 36) + "-" + strconv.FormatInt(update.MessageID, 36) reminder := Reminder{ TargetID: update.User.UserID, When: timestamp.Unix(), Text: message, } if update.ReplyTo != nil { reminder.Reference = &ReminderReference{ Chat: update.Chat.ChatID, Message: update.ReplyTo.MessageID, } } reminders[id] = reminder savereminder() go runreminder(id) whenday := "più tardi" _, todaym, todayd := time.Now().Date() _, targetm, targetd := timestamp.Date() if todaym != targetm || todayd != targetd { whenday = "il " + timestamp.Format("2/1") } whentime := "alle " + timestamp.Format("15:04:05") broker.SendTextMessage(update.Chat, "Ok, vedrò di avvisarti "+whenday+" "+whentime, &update.MessageID) } if isCommand(update, "reminders") { // Should only work in private chats if update.Chat.Type != tg.ChatTypePrivate { broker.SendTextMessage(update.Chat, "Per favore chiedimi in privato dei reminder", &update.MessageID) return } useritems := []Reminder{} for _, reminder := range reminders { if reminder.TargetID == update.User.UserID { useritems = append(useritems, reminder) } } broker.SendTextMessage(update.Chat, fmt.Sprintf("Ci sono %d reminder in coda per te", len(useritems)), &update.MessageID) } } func runreminder(id string) { // In case this is called in the init, wait until the broker is available for broker == nil { // Wait time.Sleep(time.Second) } // Get reminder r := reminders[id] remaining := r.When - time.Now().Unix() if remaining > 0 { // Wait remaining time time.Sleep(time.Second * time.Duration(remaining)) } // Remind! broker.SendTextMessage(&tg.APIChat{ChatID: r.TargetID}, "Heyla! Mi avevi chiesto di ricordarti questo:\n"+r.Text, nil) if r.Reference != nil { broker.ForwardMessage(&tg.APIChat{ChatID: r.TargetID}, tg.APIMessage{MessageID: r.Reference.Message, Chat: &tg.APIChat{ChatID: r.Reference.Chat}}) } // Delete reminder from pending list and save list to disk delete(reminders, id) savereminder() } func savereminder() { 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(reminders) if err != nil { log.Println("[macro] 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) (time.Time, error) { now := time.Now() 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!") } loc, _ := time.LoadLocation("Local") 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 }