Implement CHATHISTORY TARGETS

References: https://github.com/ircv3/ircv3-specifications/pull/450
This commit is contained in:
Simon Ser 2021-05-18 16:50:19 +02:00
parent 95ae92860f
commit 18439f0de5
3 changed files with 136 additions and 5 deletions

View File

@ -2030,6 +2030,10 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
if err := parseMessageParams(msg, nil, &target, &boundsStr[0], &boundsStr[1], &limitStr); err != nil { if err := parseMessageParams(msg, nil, &target, &boundsStr[0], &boundsStr[1], &limitStr); err != nil {
return err return err
} }
case "TARGETS":
if err := parseMessageParams(msg, nil, &boundsStr[0], &boundsStr[1], &limitStr); err != nil {
return err
}
default: default:
// TODO: support LATEST, AROUND // TODO: support LATEST, AROUND
return ircError{&irc.Message{ return ircError{&irc.Message{
@ -2092,6 +2096,40 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
} else { } else {
history, err = store.LoadBeforeTime(uc.network, entity, bounds[0], bounds[1], limit) history, err = store.LoadBeforeTime(uc.network, entity, bounds[0], bounds[1], limit)
} }
case "TARGETS":
// TODO: support TARGETS in multi-upstream mode
targets, err := store.ListTargets(uc.network, bounds[0], bounds[1], limit)
if err != nil {
dc.logger.Printf("failed fetching targets for chathistory: %v", target, err)
return ircError{&irc.Message{
Command: "FAIL",
Params: []string{"CHATHISTORY", "MESSAGE_ERROR", subcommand, "Failed to retrieve targets"},
}}
}
batchRef := "history-targets"
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"+" + batchRef, "draft/chathistory-targets"},
})
for _, target := range targets {
dc.SendMessage(&irc.Message{
Tags: irc.Tags{"batch": irc.TagValue(batchRef)},
Prefix: dc.srv.prefix(),
Command: "CHATHISTORY",
Params: []string{"TARGETS", target.Name, target.LatestMessage.UTC().Format(serverTimeLayout)},
})
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"-" + batchRef},
})
return nil
} }
if err != nil { if err != nil {
dc.logger.Printf("failed fetching %q messages for chathistory: %v", target, err) dc.logger.Printf("failed fetching %q messages for chathistory: %v", target, err)

View File

@ -21,11 +21,20 @@ type messageStore interface {
Append(network *network, entity string, msg *irc.Message) (id string, err 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 // chatHistoryMessageStore is a message store that supports chat history
// operations. // operations.
type chatHistoryMessageStore interface { type chatHistoryMessageStore interface {
messageStore 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.
ListTargets(network *network, start, end time.Time, limit int) ([]chatHistoryTarget, error)
// LoadBeforeTime loads up to limit messages before start down to end. The // LoadBeforeTime loads up to limit messages before start down to end. The
// returned messages must be between and excluding the provided bounds. // returned messages must be between and excluding the provided bounds.
// end is before start. // end is before start.

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"time" "time"
@ -393,11 +394,6 @@ func (ms *fsMessageStore) LoadAfterTime(network *network, entity string, start t
return history, nil return history, nil
} }
func truncateDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func (ms *fsMessageStore) LoadLatestID(network *network, entity, id string, limit int) ([]*irc.Message, error) { func (ms *fsMessageStore) LoadLatestID(network *network, entity, id string, limit int) ([]*irc.Message, error) {
var afterTime time.Time var afterTime time.Time
var afterOffset int64 var afterOffset int64
@ -441,3 +437,91 @@ func (ms *fsMessageStore) LoadLatestID(network *network, entity, id string, limi
return history[remaining:], nil return history[remaining:], nil
} }
func (ms *fsMessageStore) ListTargets(network *network, start, end time.Time, limit int) ([]chatHistoryTarget, error) {
rootPath := filepath.Join(ms.root, escapeFilename.Replace(network.GetName()))
root, err := os.Open(rootPath)
if err != nil {
return nil, err
}
// The returned targets are escaped, and there is no way to un-escape
// TODO: switch to ReadDir (Go 1.16+)
targetNames, err := root.Readdirnames(0)
root.Close()
if err != nil {
return nil, err
}
var targets []chatHistoryTarget
for _, target := range targetNames {
// target is already escaped here
targetPath := filepath.Join(rootPath, target)
targetDir, err := os.Open(targetPath)
if err != nil {
return nil, err
}
entries, err := targetDir.Readdir(0)
targetDir.Close()
if err != nil {
return nil, err
}
// We use mtime here, which may give imprecise or incorrect results
var t time.Time
for _, entry := range entries {
if entry.ModTime().After(t) {
t = entry.ModTime()
}
}
// The timestamps we get from logs have second granularity
t = truncateSecond(t)
// Filter out targets that don't fullfil the time bounds
if !isTimeBetween(t, start, end) {
continue
}
targets = append(targets, chatHistoryTarget{
Name: target,
LatestMessage: t,
})
}
// Sort targets by latest message time, backwards or forwards depending on
// the order of the time bounds
sort.Slice(targets, func(i, j int) bool {
t1, t2 := targets[i].LatestMessage, targets[j].LatestMessage
if start.Before(end) {
return t1.Before(t2)
} else {
return !t1.Before(t2)
}
})
// Truncate the result if necessary
if len(targets) > limit {
targets = targets[:limit]
}
return targets, nil
}
func truncateDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func truncateSecond(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, t.Hour(), t.Minute(), t.Second(), 0, t.Location())
}
func isTimeBetween(t, start, end time.Time) bool {
if end.Before(start) {
end, start = start, end
}
return start.Before(t) && t.Before(end)
}