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 {
|
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)
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user