Implement CHATHISTORY TARGETS
References: https://github.com/ircv3/ircv3-specifications/pull/450
This commit is contained in:
parent
95ae92860f
commit
18439f0de5
@ -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 {
|
||||
return err
|
||||
}
|
||||
case "TARGETS":
|
||||
if err := parseMessageParams(msg, nil, &boundsStr[0], &boundsStr[1], &limitStr); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// TODO: support LATEST, AROUND
|
||||
return ircError{&irc.Message{
|
||||
@ -2092,6 +2096,40 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
|
||||
} else {
|
||||
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 {
|
||||
dc.logger.Printf("failed fetching %q messages for chathistory: %v", target, err)
|
||||
|
@ -21,11 +21,20 @@ type messageStore interface {
|
||||
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.
|
||||
ListTargets(network *network, start, end time.Time, limit int) ([]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.
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -393,11 +394,6 @@ func (ms *fsMessageStore) LoadAfterTime(network *network, entity string, start t
|
||||
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) {
|
||||
var afterTime time.Time
|
||||
var afterOffset int64
|
||||
@ -441,3 +437,91 @@ func (ms *fsMessageStore) LoadLatestID(network *network, entity, id string, limi
|
||||
|
||||
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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user