cae248f672
READ lets downstream clients share information between each other about what messages have been read by other downstreams. Each target/entity has an optional corresponding read receipt, which is stored as a timestamp. - When a downstream sends: READ #chan timestamp=2020-01-01T01:23:45.000Z the read receipt for that target is set to that date - soju sends READ to downstreams: - on JOIN, if the client uses the soju.im/read capability - when the read receipt timestamp is set by any downstream The read receipt date is clamped by the previous receipt date and the current time.
117 lines
3.0 KiB
Go
117 lines
3.0 KiB
Go
package soju
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gopkg.in/irc.v3"
|
|
)
|
|
|
|
func forwardChannel(ctx context.Context, dc *downstreamConn, ch *upstreamChannel) {
|
|
if !ch.complete {
|
|
panic("Tried to forward a partial channel")
|
|
}
|
|
|
|
// RPL_NOTOPIC shouldn't be sent on JOIN
|
|
if ch.Topic != "" {
|
|
sendTopic(dc, ch)
|
|
}
|
|
|
|
if dc.caps["soju.im/read"] {
|
|
channelCM := ch.conn.network.casemap(ch.Name)
|
|
r, err := dc.srv.db.GetReadReceipt(ctx, ch.conn.network.ID, channelCM)
|
|
if err != nil {
|
|
dc.logger.Printf("failed to get the read receipt for %q: %v", ch.Name, err)
|
|
} else {
|
|
timestampStr := "*"
|
|
if r != nil {
|
|
timestampStr = fmt.Sprintf("timestamp=%s", r.Timestamp.UTC().Format(serverTimeLayout))
|
|
}
|
|
dc.SendMessage(&irc.Message{
|
|
Prefix: dc.prefix(),
|
|
Command: "READ",
|
|
Params: []string{dc.marshalEntity(ch.conn.network, ch.Name), timestampStr},
|
|
})
|
|
}
|
|
}
|
|
|
|
sendNames(dc, ch)
|
|
}
|
|
|
|
func sendTopic(dc *downstreamConn, ch *upstreamChannel) {
|
|
downstreamName := dc.marshalEntity(ch.conn.network, ch.Name)
|
|
|
|
if ch.Topic != "" {
|
|
dc.SendMessage(&irc.Message{
|
|
Prefix: dc.srv.prefix(),
|
|
Command: irc.RPL_TOPIC,
|
|
Params: []string{dc.nick, downstreamName, ch.Topic},
|
|
})
|
|
if ch.TopicWho != nil {
|
|
topicWho := dc.marshalUserPrefix(ch.conn.network, ch.TopicWho)
|
|
topicTime := strconv.FormatInt(ch.TopicTime.Unix(), 10)
|
|
dc.SendMessage(&irc.Message{
|
|
Prefix: dc.srv.prefix(),
|
|
Command: rpl_topicwhotime,
|
|
Params: []string{dc.nick, downstreamName, topicWho.String(), topicTime},
|
|
})
|
|
}
|
|
} else {
|
|
dc.SendMessage(&irc.Message{
|
|
Prefix: dc.srv.prefix(),
|
|
Command: irc.RPL_NOTOPIC,
|
|
Params: []string{dc.nick, downstreamName, "No topic is set"},
|
|
})
|
|
}
|
|
}
|
|
|
|
func sendNames(dc *downstreamConn, ch *upstreamChannel) {
|
|
downstreamName := dc.marshalEntity(ch.conn.network, ch.Name)
|
|
|
|
emptyNameReply := &irc.Message{
|
|
Prefix: dc.srv.prefix(),
|
|
Command: irc.RPL_NAMREPLY,
|
|
Params: []string{dc.nick, string(ch.Status), downstreamName, ""},
|
|
}
|
|
maxLength := maxMessageLength - len(emptyNameReply.String())
|
|
|
|
var buf strings.Builder
|
|
for _, entry := range ch.Members.innerMap {
|
|
nick := entry.originalKey
|
|
memberships := entry.value.(*memberships)
|
|
s := memberships.Format(dc) + dc.marshalEntity(ch.conn.network, nick)
|
|
|
|
n := buf.Len() + 1 + len(s)
|
|
if buf.Len() != 0 && n > maxLength {
|
|
// There's not enough space for the next space + nick.
|
|
dc.SendMessage(&irc.Message{
|
|
Prefix: dc.srv.prefix(),
|
|
Command: irc.RPL_NAMREPLY,
|
|
Params: []string{dc.nick, string(ch.Status), downstreamName, buf.String()},
|
|
})
|
|
buf.Reset()
|
|
}
|
|
|
|
if buf.Len() != 0 {
|
|
buf.WriteByte(' ')
|
|
}
|
|
buf.WriteString(s)
|
|
}
|
|
|
|
if buf.Len() != 0 {
|
|
dc.SendMessage(&irc.Message{
|
|
Prefix: dc.srv.prefix(),
|
|
Command: irc.RPL_NAMREPLY,
|
|
Params: []string{dc.nick, string(ch.Status), downstreamName, buf.String()},
|
|
})
|
|
}
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
Prefix: dc.srv.prefix(),
|
|
Command: irc.RPL_ENDOFNAMES,
|
|
Params: []string{dc.nick, downstreamName, "End of /NAMES list"},
|
|
})
|
|
}
|