Changeset 319 in code for trunk/logger.go


Ignore:
Timestamp:
Jun 5, 2020, 9:50:31 PM (5 years ago)
Author:
delthas
Message:

Add support for downstream CHATHISTORY

This adds support for the WIP (at the time of this commit)
draft/chathistory extension, based on the draft at [1] and the
additional comments at [2].

This gets the history by parsing the chat logs, and is therefore only
enabled when the logs are enabled and the log path is configured.

Getting the history only from the logs adds some restrictions:

  • we cannot get history by msgid (those are not logged)
  • we cannot get the users masks (maybe they could be inferred from the JOIN etc, but it is not worth the effort and would not work every time)

The regular soju network history is not sent to clients that support
draft/chathistory, so that they can fetch what they need by manually
calling CHATHISTORY.

The only supported command is BEFORE for now, because that is the only
required command for an app that offers an "infinite history scrollback"
feature.

Regarding implementation, rather than reading the file from the end in
reverse, we simply start from the beginning of each log file, store each
PRIVMSG into a ring, then add the last lines of that ring into the
history we'll return later. The message parsing implementation must be
kept somewhat fast because an app could potentially request thousands of
messages in several files. Here we are using simple sscanf and indexOf
rather than regexps.

In case some log files do not contain any message (for example because
the user had not joined a channel at that time), we try up to a 100 days
of empty log files before giving up.

[1]: https://github.com/prawnsalad/ircv3-specifications/pull/3/files
[2]: https://github.com/ircv3/ircv3-specifications/pull/393/files#r350210018

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/logger.go

    r250 r319  
    22
    33import (
     4        "bufio"
    45        "fmt"
    56        "os"
     
    133134        }
    134135}
     136
     137func parseMessagesBefore(network *network, entity string, timestamp time.Time, limit int) ([]*irc.Message, error) {
     138        year, month, day := timestamp.Date()
     139        path := logPath(network, entity, timestamp)
     140        f, err := os.Open(path)
     141        if err != nil {
     142                if os.IsNotExist(err) {
     143                        return nil, nil
     144                }
     145                return nil, err
     146        }
     147        defer f.Close()
     148
     149        historyRing := make([]*irc.Message, limit)
     150        cur := 0
     151
     152        sc := bufio.NewScanner(f)
     153        for sc.Scan() {
     154                line := sc.Text()
     155                var hour, minute, second int
     156                _, err := fmt.Sscanf(line, "[%02d:%02d:%02d] ", &hour, &minute, &second)
     157                if err != nil {
     158                        return nil, err
     159                }
     160                message := line[11:]
     161                // TODO: support NOTICE
     162                if !strings.HasPrefix(message, "<") {
     163                        continue
     164                }
     165                i := strings.Index(message, "> ")
     166                if i == -1 {
     167                        continue
     168                }
     169                t := time.Date(year, month, day, hour, minute, second, 0, time.Local)
     170                if !t.Before(timestamp) {
     171                        break
     172                }
     173
     174                sender := message[1:i]
     175                text := message[i+2:]
     176                historyRing[cur%limit] = &irc.Message{
     177                        Tags: map[string]irc.TagValue{
     178                                "time": irc.TagValue(t.UTC().Format(serverTimeLayout)),
     179                        },
     180                        Prefix: &irc.Prefix{
     181                                Name: sender,
     182                        },
     183                        Command: "PRIVMSG",
     184                        Params:  []string{entity, text},
     185                }
     186                cur++
     187        }
     188        if sc.Err() != nil {
     189                return nil, sc.Err()
     190        }
     191
     192        n := limit
     193        if cur < limit {
     194                n = cur
     195        }
     196        start := (cur - n + limit) % limit
     197
     198        if start+n <= limit { // ring doesnt wrap
     199                return historyRing[start : start+n], nil
     200        } else { // ring wraps
     201                history := make([]*irc.Message, n)
     202                r := copy(history, historyRing[start:])
     203                copy(history[r:], historyRing[:n-r])
     204                return history, nil
     205        }
     206}
Note: See TracChangeset for help on using the changeset viewer.