summaryrefslogtreecommitdiff
path: root/msgstore.go
blob: deb63b4f5eced8c0630fed7be2ad40ddc11c23ef (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package soju

import (
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"time"

	"git.sr.ht/~sircmpwn/go-bare"
	"gopkg.in/irc.v3"
)

// messageStore is a per-user store for IRC messages.
type messageStore interface {
	Close() error
	// LastMsgID queries the last message ID for the given network, entity and
	// date. The message ID returned may not refer to a valid message, but can be
	// used in history queries.
	LastMsgID(network *Network, entity string, t time.Time) (string, error)
	// LoadLatestID queries the latest non-event messages for the given network,
	// entity and date, up to a count of limit messages, sorted from oldest to newest.
	LoadLatestID(ctx context.Context, network *Network, entity, id string, limit int) ([]*irc.Message, error)
	Append(network *Network, entity string, msg *irc.Message) (id string, err error)
}

type chatHistoryTarget struct {
	Name          string
	LatestMessage time.Time
}

// chatHistoryMessageStore is a message store that supports chat history
// operations.
type chatHistoryMessageStore interface {
	messageStore

	// ListTargets lists channels and nicknames by time of the latest message.
	// It returns up to limit targets, starting from start and ending on end,
	// both excluded. end may be before or after start.
	// If events is false, only PRIVMSG/NOTICE messages are considered.
	ListTargets(ctx context.Context, network *Network, start, end time.Time, limit int, events bool) ([]chatHistoryTarget, error)
	// LoadBeforeTime loads up to limit messages before start down to end. The
	// returned messages must be between and excluding the provided bounds.
	// end is before start.
	// If events is false, only PRIVMSG/NOTICE messages are considered.
	LoadBeforeTime(ctx context.Context, network *Network, entity string, start, end time.Time, limit int, events bool) ([]*irc.Message, error)
	// LoadBeforeTime loads up to limit messages after start up to end. The
	// returned messages must be between and excluding the provided bounds.
	// end is after start.
	// If events is false, only PRIVMSG/NOTICE messages are considered.
	LoadAfterTime(ctx context.Context, network *Network, entity string, start, end time.Time, limit int, events bool) ([]*irc.Message, error)
}

type msgIDType uint

const (
	msgIDNone msgIDType = iota
	msgIDMemory
	msgIDFS
)

const msgIDVersion uint = 0

type msgIDHeader struct {
	Version uint
	Network bare.Int
	Target  string
	Type    msgIDType
}

type msgIDBody interface {
	msgIDType() msgIDType
}

func formatMsgID(netID int64, target string, body msgIDBody) string {
	var buf bytes.Buffer
	w := bare.NewWriter(&buf)

	header := msgIDHeader{
		Version: msgIDVersion,
		Network: bare.Int(netID),
		Target:  target,
		Type:    body.msgIDType(),
	}
	if err := bare.MarshalWriter(w, &header); err != nil {
		panic(err)
	}
	if err := bare.MarshalWriter(w, body); err != nil {
		panic(err)
	}
	return base64.RawURLEncoding.EncodeToString(buf.Bytes())
}

func parseMsgID(s string, body msgIDBody) (netID int64, target string, err error) {
	b, err := base64.RawURLEncoding.DecodeString(s)
	if err != nil {
		return 0, "", fmt.Errorf("invalid internal message ID: %v", err)
	}

	r := bare.NewReader(bytes.NewReader(b))

	var header msgIDHeader
	if err := bare.UnmarshalBareReader(r, &header); err != nil {
		return 0, "", fmt.Errorf("invalid internal message ID: %v", err)
	}

	if header.Version != msgIDVersion {
		return 0, "", fmt.Errorf("invalid internal message ID: got version %v, want %v", header.Version, msgIDVersion)
	}

	if body != nil {
		typ := body.msgIDType()
		if header.Type != typ {
			return 0, "", fmt.Errorf("invalid internal message ID: got type %v, want %v", header.Type, typ)
		}

		if err := bare.UnmarshalBareReader(r, body); err != nil {
			return 0, "", fmt.Errorf("invalid internal message ID: %v", err)
		}
	}

	return int64(header.Network), header.Target, nil
}