ScuttleBot

scuttlebot / pkg / chathistory / chathistory.go
Source Blame History 183 lines
f64fe5f… noreply 1 // Package chathistory provides a synchronous wrapper around the IRCv3
f64fe5f… noreply 2 // CHATHISTORY extension for use with girc clients.
f64fe5f… noreply 3 //
f64fe5f… noreply 4 // Usage:
f64fe5f… noreply 5 //
f64fe5f… noreply 6 // fetcher := chathistory.New(client)
f64fe5f… noreply 7 // msgs, err := fetcher.Latest(ctx, "#channel", 50)
f64fe5f… noreply 8 package chathistory
f64fe5f… noreply 9
f64fe5f… noreply 10 import (
f64fe5f… noreply 11 "context"
f64fe5f… noreply 12 "fmt"
f64fe5f… noreply 13 "strings"
f64fe5f… noreply 14 "sync"
f64fe5f… noreply 15 "time"
f64fe5f… noreply 16
f64fe5f… noreply 17 "github.com/lrstanley/girc"
f64fe5f… noreply 18 )
f64fe5f… noreply 19
f64fe5f… noreply 20 // Message is a single message returned by a CHATHISTORY query.
f64fe5f… noreply 21 type Message struct {
f64fe5f… noreply 22 At time.Time
f64fe5f… noreply 23 Nick string
f64fe5f… noreply 24 Account string
f64fe5f… noreply 25 Text string
f64fe5f… noreply 26 MsgID string
f64fe5f… noreply 27 }
f64fe5f… noreply 28
f64fe5f… noreply 29 // Fetcher sends CHATHISTORY commands and collects the batched responses.
f64fe5f… noreply 30 type Fetcher struct {
f64fe5f… noreply 31 client *girc.Client
f64fe5f… noreply 32
f64fe5f… noreply 33 mu sync.Mutex
f64fe5f… noreply 34 batches map[string]*batch // batchRef → accumulator
f64fe5f… noreply 35 waiters map[string]chan []Message // channel → result (one waiter per channel)
f64fe5f… noreply 36 handlers bool
f64fe5f… noreply 37 }
f64fe5f… noreply 38
f64fe5f… noreply 39 type batch struct {
f64fe5f… noreply 40 channel string
f64fe5f… noreply 41 msgs []Message
f64fe5f… noreply 42 }
f64fe5f… noreply 43
f64fe5f… noreply 44 // New creates a Fetcher and registers the necessary BATCH handlers on the
f64fe5f… noreply 45 // client. The client's Config.SupportedCaps should include
f64fe5f… noreply 46 // "draft/chathistory" (or "chathistory") so the capability is negotiated.
f64fe5f… noreply 47 func New(client *girc.Client) *Fetcher {
f64fe5f… noreply 48 f := &Fetcher{
f64fe5f… noreply 49 client: client,
f64fe5f… noreply 50 batches: make(map[string]*batch),
f64fe5f… noreply 51 waiters: make(map[string]chan []Message),
f64fe5f… noreply 52 }
f64fe5f… noreply 53 f.registerHandlers()
f64fe5f… noreply 54 return f
f64fe5f… noreply 55 }
f64fe5f… noreply 56
f64fe5f… noreply 57 func (f *Fetcher) registerHandlers() {
f64fe5f… noreply 58 f.mu.Lock()
f64fe5f… noreply 59 defer f.mu.Unlock()
f64fe5f… noreply 60 if f.handlers {
f64fe5f… noreply 61 return
f64fe5f… noreply 62 }
f64fe5f… noreply 63 f.handlers = true
f64fe5f… noreply 64
f64fe5f… noreply 65 // BATCH open/close.
f64fe5f… noreply 66 f.client.Handlers.AddBg("BATCH", func(_ *girc.Client, e girc.Event) {
f64fe5f… noreply 67 if len(e.Params) < 1 {
f64fe5f… noreply 68 return
f64fe5f… noreply 69 }
f64fe5f… noreply 70 raw := e.Params[0]
f64fe5f… noreply 71 if strings.HasPrefix(raw, "+") {
f64fe5f… noreply 72 ref := raw[1:]
f64fe5f… noreply 73 if len(e.Params) >= 2 && e.Params[1] == "chathistory" {
f64fe5f… noreply 74 ch := ""
f64fe5f… noreply 75 if len(e.Params) >= 3 {
f64fe5f… noreply 76 ch = e.Params[2]
f64fe5f… noreply 77 }
f64fe5f… noreply 78 f.mu.Lock()
f64fe5f… noreply 79 f.batches[ref] = &batch{channel: ch}
f64fe5f… noreply 80 f.mu.Unlock()
f64fe5f… noreply 81 }
f64fe5f… noreply 82 } else if strings.HasPrefix(raw, "-") {
f64fe5f… noreply 83 ref := raw[1:]
f64fe5f… noreply 84 f.mu.Lock()
f64fe5f… noreply 85 b, ok := f.batches[ref]
f64fe5f… noreply 86 if ok {
f64fe5f… noreply 87 delete(f.batches, ref)
f64fe5f… noreply 88 if w, wok := f.waiters[b.channel]; wok {
f64fe5f… noreply 89 delete(f.waiters, b.channel)
f64fe5f… noreply 90 f.mu.Unlock()
f64fe5f… noreply 91 w <- b.msgs
f64fe5f… noreply 92 return
f64fe5f… noreply 93 }
f64fe5f… noreply 94 }
f64fe5f… noreply 95 f.mu.Unlock()
f64fe5f… noreply 96 }
f64fe5f… noreply 97 })
f64fe5f… noreply 98
f64fe5f… noreply 99 // Collect PRIVMSGs tagged with a tracked batch ref.
f64fe5f… noreply 100 f.client.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
f64fe5f… noreply 101 batchRef, ok := e.Tags.Get("batch")
f64fe5f… noreply 102 if !ok || batchRef == "" {
f64fe5f… noreply 103 return
f64fe5f… noreply 104 }
f64fe5f… noreply 105
f64fe5f… noreply 106 f.mu.Lock()
f64fe5f… noreply 107 b, tracked := f.batches[batchRef]
f64fe5f… noreply 108 if !tracked {
f64fe5f… noreply 109 f.mu.Unlock()
f64fe5f… noreply 110 return
f64fe5f… noreply 111 }
f64fe5f… noreply 112
f64fe5f… noreply 113 nick := ""
f64fe5f… noreply 114 if e.Source != nil {
f64fe5f… noreply 115 nick = e.Source.Name
f64fe5f… noreply 116 }
f64fe5f… noreply 117 acct, _ := e.Tags.Get("account")
f64fe5f… noreply 118 msgID, _ := e.Tags.Get("msgid")
f64fe5f… noreply 119
f64fe5f… noreply 120 b.msgs = append(b.msgs, Message{
f64fe5f… noreply 121 At: e.Timestamp,
f64fe5f… noreply 122 Nick: nick,
f64fe5f… noreply 123 Account: acct,
f64fe5f… noreply 124 Text: e.Last(),
f64fe5f… noreply 125 MsgID: msgID,
f64fe5f… noreply 126 })
f64fe5f… noreply 127 f.mu.Unlock()
f64fe5f… noreply 128 })
f64fe5f… noreply 129 }
f64fe5f… noreply 130
f64fe5f… noreply 131 // Latest fetches the N most recent messages from a channel using
f64fe5f… noreply 132 // CHATHISTORY LATEST. Blocks until the server responds or ctx expires.
f64fe5f… noreply 133 func (f *Fetcher) Latest(ctx context.Context, channel string, count int) ([]Message, error) {
f64fe5f… noreply 134 result := make(chan []Message, 1)
f64fe5f… noreply 135
f64fe5f… noreply 136 f.mu.Lock()
f64fe5f… noreply 137 f.waiters[channel] = result
f64fe5f… noreply 138 f.mu.Unlock()
f64fe5f… noreply 139
f64fe5f… noreply 140 if err := f.client.Cmd.SendRawf("CHATHISTORY LATEST %s * %d", channel, count); err != nil {
f64fe5f… noreply 141 f.mu.Lock()
f64fe5f… noreply 142 delete(f.waiters, channel)
f64fe5f… noreply 143 f.mu.Unlock()
f64fe5f… noreply 144 return nil, fmt.Errorf("chathistory: send: %w", err)
f64fe5f… noreply 145 }
f64fe5f… noreply 146
f64fe5f… noreply 147 select {
f64fe5f… noreply 148 case msgs := <-result:
f64fe5f… noreply 149 return msgs, nil
f64fe5f… noreply 150 case <-ctx.Done():
f64fe5f… noreply 151 f.mu.Lock()
f64fe5f… noreply 152 delete(f.waiters, channel)
f64fe5f… noreply 153 f.mu.Unlock()
f64fe5f… noreply 154 return nil, ctx.Err()
f64fe5f… noreply 155 }
f64fe5f… noreply 156 }
f64fe5f… noreply 157
f64fe5f… noreply 158 // Before fetches up to count messages before the given timestamp.
f64fe5f… noreply 159 func (f *Fetcher) Before(ctx context.Context, channel string, before time.Time, count int) ([]Message, error) {
f64fe5f… noreply 160 result := make(chan []Message, 1)
f64fe5f… noreply 161
f64fe5f… noreply 162 f.mu.Lock()
f64fe5f… noreply 163 f.waiters[channel] = result
f64fe5f… noreply 164 f.mu.Unlock()
f64fe5f… noreply 165
f64fe5f… noreply 166 ts := before.UTC().Format("2006-01-02T15:04:05.000Z")
f64fe5f… noreply 167 if err := f.client.Cmd.SendRawf("CHATHISTORY BEFORE %s timestamp=%s %d", channel, ts, count); err != nil {
f64fe5f… noreply 168 f.mu.Lock()
f64fe5f… noreply 169 delete(f.waiters, channel)
f64fe5f… noreply 170 f.mu.Unlock()
f64fe5f… noreply 171 return nil, fmt.Errorf("chathistory: send: %w", err)
f64fe5f… noreply 172 }
f64fe5f… noreply 173
f64fe5f… noreply 174 select {
f64fe5f… noreply 175 case msgs := <-result:
f64fe5f… noreply 176 return msgs, nil
f64fe5f… noreply 177 case <-ctx.Done():
f64fe5f… noreply 178 f.mu.Lock()
f64fe5f… noreply 179 delete(f.waiters, channel)
f64fe5f… noreply 180 f.mu.Unlock()
f64fe5f… noreply 181 return nil, ctx.Err()
f64fe5f… noreply 182 }
f64fe5f… noreply 183 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button