all repos — paprika @ 17ded8c89cc2a6df14f6e3d2431e3bbe349f8ccc

go rewrite of taigabot

plugins/tell.go (view raw)

  1package plugins
  2
  3import (
  4	"bytes"
  5	"crypto/sha1"
  6	"encoding/gob"
  7	"fmt"
  8	"io"
  9	"sort"
 10	"strings"
 11	"time"
 12
 13	"git.icyphox.sh/paprika/database"
 14	"github.com/dgraph-io/badger/v3"
 15	"github.com/dustin/go-humanize"
 16	"gopkg.in/irc.v3"
 17)
 18
 19func init() {
 20	Register(Tell{})
 21}
 22
 23type Tell struct {
 24	From    string
 25	To      string
 26	Message string
 27	Time    time.Time
 28}
 29
 30func (Tell) Triggers() []string {
 31	return []string{".tell", ""}
 32}
 33
 34// Encodes message into encoding/gob for storage.
 35// We use a hash of the message in the key to ensure
 36// we don't queue dupes.
 37func (t *Tell) saveTell() error {
 38	data := bytes.Buffer{}
 39	enc := gob.NewEncoder(&data)
 40
 41	if err := enc.Encode(t); err != nil {
 42		return err
 43	}
 44	// Store key as 'tell/nick/hash'; should help with
 45	// easy prefix scans for tells.
 46	hash := hashMessage(t.Message)
 47
 48	key := []byte(fmt.Sprintf("tell/%s/", t.To))
 49	key = append(key, hash...)
 50	err := database.DB.Set(key, data.Bytes())
 51	if err != nil {
 52		return err
 53	}
 54	return nil
 55}
 56
 57// Decodes tell data from encoding/gob into a Tell.
 58func getTell(data []byte) (*Tell, error) {
 59	r := bytes.NewReader(data)
 60	dec := gob.NewDecoder(r)
 61	t := Tell{}
 62	if err := dec.Decode(&t); err != nil {
 63		return nil, err
 64	}
 65
 66	return &t, nil
 67}
 68
 69// Hash (SHA1) a message for use as key.
 70// Helps ensure we don't queue the same
 71// message over and over.
 72func hashMessage(msg string) []byte {
 73	h := sha1.New()
 74	io.WriteString(h, msg)
 75	return h.Sum(nil)
 76}
 77
 78func (t Tell) Execute(m *irc.Message) (string, error) {
 79	parts := strings.SplitN(m.Trailing(), " ", 3)
 80
 81	if parts[0] == ".tell" {
 82		// No message passed.
 83		if len(parts) == 2 {
 84			return "Usage: .tell <nick> <message>", nil
 85		}
 86
 87		t.From = strings.ToLower(m.Prefix.Name)
 88		t.To = strings.ToLower(parts[1])
 89		t.Message = parts[2]
 90		t.Time = time.Now()
 91
 92		if err := t.saveTell(); err != nil {
 93			return "Error saving message", err
 94		}
 95
 96		return "Your message will be sent!", &IsPrivateNotice{t.From}
 97	} else {
 98		// React to all other messages here.
 99		// Iterate over key prefixes to check if our tell
100		// recepient has shown up. Then send his tell and delete
101		// the keys.
102
103		// All pending tells.
104		tells := []Tell{}
105
106		err := database.DB.Update(func(txn *badger.Txn) error {
107			it := txn.NewIterator(badger.DefaultIteratorOptions)
108			defer it.Close()
109			prefix := []byte("tell/" + strings.ToLower(m.Prefix.Name))
110			for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
111				item := it.Item()
112				k := item.Key()
113				err := item.Value(func(v []byte) error {
114					tell, err := getTell(v)
115					if err != nil {
116						return fmt.Errorf("degobbing: %w", err)
117					}
118					tells = append(tells, *tell)
119					return nil
120				})
121				if err != nil {
122					return fmt.Errorf("iterating: %w", err)
123				}
124				err = txn.Delete(k)
125				if err != nil {
126					return fmt.Errorf("deleting key: %w", err)
127				}
128			}
129			return nil
130		})
131		if err != nil {
132			return "", fmt.Errorf("fetching tells: %w", err)
133		}
134
135		// No tells for this user.
136		if len(tells) == 0 {
137			return "", NoReply
138		}
139
140		// Sort tells by time.
141		sort.Slice(tells, func(i, j int) bool {
142			return tells[j].Time.Before(tells[i].Time)
143		})
144
145		// Formatted tells in a slice, for joining into a string
146		// later.
147		tellsFmtd := strings.Builder{}
148		for _, tell := range tells {
149			s := fmt.Sprintf(
150				"%s sent you a message %s: %s\n",
151				tell.From, humanize.Time(tell.Time), tell.Message,
152			)
153			tellsFmtd.WriteString(s)
154		}
155		return tellsFmtd.String(), &IsPrivateNotice{To: tells[0].To}
156	}
157}