[423] | 1 | package soju
|
---|
| 2 |
|
---|
| 3 | import (
|
---|
[488] | 4 | "bytes"
|
---|
[667] | 5 | "context"
|
---|
[488] | 6 | "encoding/base64"
|
---|
[440] | 7 | "fmt"
|
---|
[423] | 8 | "time"
|
---|
| 9 |
|
---|
[488] | 10 | "git.sr.ht/~sircmpwn/go-bare"
|
---|
[423] | 11 | "gopkg.in/irc.v3"
|
---|
| 12 | )
|
---|
| 13 |
|
---|
| 14 | // messageStore is a per-user store for IRC messages.
|
---|
[439] | 15 | type messageStore interface {
|
---|
| 16 | Close() error
|
---|
| 17 | // LastMsgID queries the last message ID for the given network, entity and
|
---|
| 18 | // date. The message ID returned may not refer to a valid message, but can be
|
---|
| 19 | // used in history queries.
|
---|
[666] | 20 | LastMsgID(network *Network, entity string, t time.Time) (string, error)
|
---|
[665] | 21 | // LoadLatestID queries the latest non-event messages for the given network,
|
---|
| 22 | // entity and date, up to a count of limit messages, sorted from oldest to newest.
|
---|
[667] | 23 | LoadLatestID(ctx context.Context, network *Network, entity, id string, limit int) ([]*irc.Message, error)
|
---|
[666] | 24 | Append(network *Network, entity string, msg *irc.Message) (id string, err error)
|
---|
[423] | 25 | }
|
---|
[440] | 26 |
|
---|
[549] | 27 | type chatHistoryTarget struct {
|
---|
| 28 | Name string
|
---|
| 29 | LatestMessage time.Time
|
---|
| 30 | }
|
---|
| 31 |
|
---|
[441] | 32 | // chatHistoryMessageStore is a message store that supports chat history
|
---|
| 33 | // operations.
|
---|
| 34 | type chatHistoryMessageStore interface {
|
---|
| 35 | messageStore
|
---|
| 36 |
|
---|
[549] | 37 | // ListTargets lists channels and nicknames by time of the latest message.
|
---|
| 38 | // It returns up to limit targets, starting from start and ending on end,
|
---|
| 39 | // both excluded. end may be before or after start.
|
---|
[665] | 40 | // If events is false, only PRIVMSG/NOTICE messages are considered.
|
---|
[667] | 41 | ListTargets(ctx context.Context, network *Network, start, end time.Time, limit int, events bool) ([]chatHistoryTarget, error)
|
---|
[516] | 42 | // LoadBeforeTime loads up to limit messages before start down to end. The
|
---|
| 43 | // returned messages must be between and excluding the provided bounds.
|
---|
| 44 | // end is before start.
|
---|
[665] | 45 | // If events is false, only PRIVMSG/NOTICE messages are considered.
|
---|
[667] | 46 | LoadBeforeTime(ctx context.Context, network *Network, entity string, start, end time.Time, limit int, events bool) ([]*irc.Message, error)
|
---|
[516] | 47 | // LoadBeforeTime loads up to limit messages after start up to end. The
|
---|
| 48 | // returned messages must be between and excluding the provided bounds.
|
---|
| 49 | // end is after start.
|
---|
[665] | 50 | // If events is false, only PRIVMSG/NOTICE messages are considered.
|
---|
[667] | 51 | LoadAfterTime(ctx context.Context, network *Network, entity string, start, end time.Time, limit int, events bool) ([]*irc.Message, error)
|
---|
[441] | 52 | }
|
---|
| 53 |
|
---|
[488] | 54 | type msgIDType uint
|
---|
| 55 |
|
---|
| 56 | const (
|
---|
| 57 | msgIDNone msgIDType = iota
|
---|
| 58 | msgIDMemory
|
---|
| 59 | msgIDFS
|
---|
| 60 | )
|
---|
| 61 |
|
---|
| 62 | const msgIDVersion uint = 0
|
---|
| 63 |
|
---|
| 64 | type msgIDHeader struct {
|
---|
| 65 | Version uint
|
---|
| 66 | Network bare.Int
|
---|
| 67 | Target string
|
---|
| 68 | Type msgIDType
|
---|
[440] | 69 | }
|
---|
| 70 |
|
---|
[488] | 71 | type msgIDBody interface {
|
---|
| 72 | msgIDType() msgIDType
|
---|
| 73 | }
|
---|
| 74 |
|
---|
| 75 | func formatMsgID(netID int64, target string, body msgIDBody) string {
|
---|
| 76 | var buf bytes.Buffer
|
---|
| 77 | w := bare.NewWriter(&buf)
|
---|
| 78 |
|
---|
| 79 | header := msgIDHeader{
|
---|
| 80 | Version: msgIDVersion,
|
---|
| 81 | Network: bare.Int(netID),
|
---|
| 82 | Target: target,
|
---|
| 83 | Type: body.msgIDType(),
|
---|
[440] | 84 | }
|
---|
[488] | 85 | if err := bare.MarshalWriter(w, &header); err != nil {
|
---|
| 86 | panic(err)
|
---|
| 87 | }
|
---|
| 88 | if err := bare.MarshalWriter(w, body); err != nil {
|
---|
| 89 | panic(err)
|
---|
| 90 | }
|
---|
| 91 | return base64.RawURLEncoding.EncodeToString(buf.Bytes())
|
---|
| 92 | }
|
---|
| 93 |
|
---|
| 94 | func parseMsgID(s string, body msgIDBody) (netID int64, target string, err error) {
|
---|
| 95 | b, err := base64.RawURLEncoding.DecodeString(s)
|
---|
[440] | 96 | if err != nil {
|
---|
[488] | 97 | return 0, "", fmt.Errorf("invalid internal message ID: %v", err)
|
---|
[440] | 98 | }
|
---|
[488] | 99 |
|
---|
| 100 | r := bare.NewReader(bytes.NewReader(b))
|
---|
| 101 |
|
---|
| 102 | var header msgIDHeader
|
---|
| 103 | if err := bare.UnmarshalBareReader(r, &header); err != nil {
|
---|
| 104 | return 0, "", fmt.Errorf("invalid internal message ID: %v", err)
|
---|
| 105 | }
|
---|
| 106 |
|
---|
| 107 | if header.Version != msgIDVersion {
|
---|
| 108 | return 0, "", fmt.Errorf("invalid internal message ID: got version %v, want %v", header.Version, msgIDVersion)
|
---|
| 109 | }
|
---|
| 110 |
|
---|
| 111 | if body != nil {
|
---|
| 112 | typ := body.msgIDType()
|
---|
| 113 | if header.Type != typ {
|
---|
| 114 | return 0, "", fmt.Errorf("invalid internal message ID: got type %v, want %v", header.Type, typ)
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | if err := bare.UnmarshalBareReader(r, body); err != nil {
|
---|
| 118 | return 0, "", fmt.Errorf("invalid internal message ID: %v", err)
|
---|
| 119 | }
|
---|
| 120 | }
|
---|
| 121 |
|
---|
| 122 | return int64(header.Network), header.Target, nil
|
---|
[440] | 123 | }
|
---|