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}