Skip to content

Commit

Permalink
Implement CHATHISTORY TARGETS
Browse files Browse the repository at this point in the history
  • Loading branch information
emersion committed May 25, 2021
1 parent b3bc961 commit 55b877e
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 5 deletions.
38 changes: 38 additions & 0 deletions downstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,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{
Expand Down Expand Up @@ -1883,6 +1887,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)
Expand Down
9 changes: 9 additions & 0 deletions msgstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
94 changes: 89 additions & 5 deletions msgstore_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

0 comments on commit 55b877e

Please sign in to comment.