diff --git a/bridge.go b/bridge.go index 4b2fe92..e69f519 100644 --- a/bridge.go +++ b/bridge.go @@ -7,6 +7,8 @@ import ( "strings" "gopkg.in/irc.v3" + + "git.sr.ht/~emersion/soju/xirc" ) func forwardChannel(ctx context.Context, dc *downstreamConn, ch *upstreamChannel) { @@ -27,7 +29,7 @@ func forwardChannel(ctx context.Context, dc *downstreamConn, ch *upstreamChannel } else { timestampStr := "*" if r != nil { - timestampStr = fmt.Sprintf("timestamp=%s", formatServerTime(r.Timestamp)) + timestampStr = fmt.Sprintf("timestamp=%s", xirc.FormatServerTime(r.Timestamp)) } dc.SendMessage(&irc.Message{ Prefix: dc.prefix(), diff --git a/downstream.go b/downstream.go index 1509095..ec2f0e2 100644 --- a/downstream.go +++ b/downstream.go @@ -18,6 +18,7 @@ import ( "gopkg.in/irc.v3" "git.sr.ht/~emersion/soju/database" + "git.sr.ht/~emersion/soju/xirc" ) type ircError struct { @@ -2472,7 +2473,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. dc.logger.Printf("broadcasting bouncer-wide %v: %v", msg.Command, text) broadcastTags := tags.Copy() - broadcastTags["time"] = irc.TagValue(formatServerTime(time.Now())) + broadcastTags["time"] = irc.TagValue(xirc.FormatServerTime(time.Now())) broadcastMsg := &irc.Message{ Tags: broadcastTags, Prefix: servicePrefix, @@ -2498,7 +2499,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. if casemapASCII(name) == serviceNickCM { if dc.caps.IsEnabled("echo-message") { echoTags := tags.Copy() - echoTags["time"] = irc.TagValue(formatServerTime(time.Now())) + echoTags["time"] = irc.TagValue(xirc.FormatServerTime(time.Now())) dc.SendMessage(&irc.Message{ Tags: echoTags, Prefix: dc.prefix(), @@ -2547,7 +2548,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. } echoTags := tags.Copy() - echoTags["time"] = irc.TagValue(formatServerTime(time.Now())) + echoTags["time"] = irc.TagValue(xirc.FormatServerTime(time.Now())) if uc.account != "" { echoTags["account"] = irc.TagValue(uc.account) } @@ -2871,7 +2872,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. Tags: irc.Tags{"batch": batchRef}, Prefix: dc.srv.prefix(), Command: "CHATHISTORY", - Params: []string{"TARGETS", target.Name, formatServerTime(target.LatestMessage)}, + Params: []string{"TARGETS", target.Name, xirc.FormatServerTime(target.LatestMessage)}, }) } }) @@ -2941,7 +2942,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. }} } - timestamp, err := time.Parse(serverTimeLayout, criteriaParts[1]) + timestamp, err := time.Parse(xirc.ServerTimeLayout, criteriaParts[1]) if err != nil { return ircError{&irc.Message{ Command: "FAIL", @@ -2967,7 +2968,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. timestampStr := "*" if !r.Timestamp.IsZero() { - timestampStr = fmt.Sprintf("timestamp=%s", formatServerTime(r.Timestamp)) + timestampStr = fmt.Sprintf("timestamp=%s", xirc.FormatServerTime(r.Timestamp)) } network.forEachDownstream(func(d *downstreamConn) { if broadcast || dc.id == d.id { @@ -3001,7 +3002,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc. value := string(v) switch name { case "before", "after": - timestamp, err := time.Parse(serverTimeLayout, value) + timestamp, err := time.Parse(xirc.ServerTimeLayout, value) if err != nil { return ircError{&irc.Message{ Command: "FAIL", diff --git a/irc.go b/irc.go index ba48de5..8e352a3 100644 --- a/irc.go +++ b/irc.go @@ -11,8 +11,11 @@ import ( "gopkg.in/irc.v3" "git.sr.ht/~emersion/soju/database" + "git.sr.ht/~emersion/soju/xirc" ) +// TODO: generalize and move helpers to the xirc package + const ( rpl_statsping = "246" rpl_localusers = "265" @@ -42,13 +45,6 @@ const ( maxSASLLength = 400 ) -// The server-time layout, as defined in the IRCv3 spec. -const serverTimeLayout = "2006-01-02T15:04:05.000Z" - -func formatServerTime(t time.Time) string { - return t.UTC().Format(serverTimeLayout) -} - type userModes string func (ms userModes) Has(c byte) bool { @@ -479,28 +475,6 @@ func (js *joinSorter) Swap(i, j int) { js.keys[i], js.keys[j] = js.keys[j], js.keys[i] } -// parseCTCPMessage parses a CTCP message. CTCP is defined in -// https://tools.ietf.org/html/draft-oakley-irc-ctcp-02 -func parseCTCPMessage(msg *irc.Message) (cmd string, params string, ok bool) { - if (msg.Command != "PRIVMSG" && msg.Command != "NOTICE") || len(msg.Params) < 2 { - return "", "", false - } - text := msg.Params[1] - - if !strings.HasPrefix(text, "\x01") { - return "", "", false - } - text = strings.Trim(text, "\x01") - - words := strings.SplitN(text, " ", 2) - cmd = strings.ToUpper(words[0]) - if len(words) > 1 { - params = words[1] - } - - return cmd, params, true -} - type casemapping func(string) string func casemapNone(name string) string { @@ -728,7 +702,7 @@ func parseChatHistoryBound(param string) time.Time { } switch parts[0] { case "timestamp": - timestamp, err := time.Parse(serverTimeLayout, parts[1]) + timestamp, err := time.Parse(xirc.ServerTimeLayout, parts[1]) if err != nil { return time.Time{} } diff --git a/msgstore_fs.go b/msgstore_fs.go index 73698c5..6c77ba5 100644 --- a/msgstore_fs.go +++ b/msgstore_fs.go @@ -15,6 +15,7 @@ import ( "gopkg.in/irc.v3" "git.sr.ht/~emersion/soju/database" + "git.sr.ht/~emersion/soju/xirc" ) const ( @@ -135,7 +136,7 @@ func (ms *fsMessageStore) Append(network *database.Network, entity string, msg * var t time.Time if tag, ok := msg.Tags["time"]; ok { var err error - t, err = time.Parse(serverTimeLayout, string(tag)) + t, err = time.Parse(xirc.ServerTimeLayout, string(tag)) if err != nil { return "", fmt.Errorf("failed to parse message time tag: %v", err) } @@ -245,7 +246,7 @@ func formatMessage(msg *irc.Message) string { case "NOTICE": return fmt.Sprintf("-%s- %s", msg.Prefix.Name, msg.Params[1]) case "PRIVMSG": - if cmd, params, ok := parseCTCPMessage(msg); ok && cmd == "ACTION" { + if cmd, params, ok := xirc.ParseCTCPMessage(msg); ok && cmd == "ACTION" { return fmt.Sprintf("* %s %s", msg.Prefix.Name, params) } else { return fmt.Sprintf("<%s> %s", msg.Prefix.Name, msg.Params[1]) @@ -392,7 +393,7 @@ func (ms *fsMessageStore) parseMessage(line string, network *database.Network, e msg := &irc.Message{ Tags: map[string]irc.TagValue{ - "time": irc.TagValue(formatServerTime(t)), + "time": irc.TagValue(xirc.FormatServerTime(t)), }, Prefix: prefix, Command: cmd, diff --git a/upstream.go b/upstream.go index cc8a7e3..ea93f5e 100644 --- a/upstream.go +++ b/upstream.go @@ -19,6 +19,7 @@ import ( "gopkg.in/irc.v3" "git.sr.ht/~emersion/soju/database" + "git.sr.ht/~emersion/soju/xirc" ) // permanentUpstreamCaps is the static list of upstream capabilities always @@ -464,7 +465,7 @@ func (uc *upstreamConn) handleMessage(ctx context.Context, msg *irc.Message) err } if _, ok := msg.Tags["time"]; !ok && !isNumeric(msg.Command) { - msg.Tags["time"] = irc.TagValue(formatServerTime(time.Now())) + msg.Tags["time"] = irc.TagValue(xirc.FormatServerTime(time.Now())) } switch msg.Command { diff --git a/xirc/xirc.go b/xirc/xirc.go new file mode 100644 index 0000000..6d65272 --- /dev/null +++ b/xirc/xirc.go @@ -0,0 +1,39 @@ +// Package xirc contains an extended IRC library. +package xirc + +import ( + "strings" + "time" + + "gopkg.in/irc.v3" +) + +// The server-time layout, as defined in the IRCv3 spec. +const ServerTimeLayout = "2006-01-02T15:04:05.000Z" + +// FormatServerTime formats a time with the server-time layout. +func FormatServerTime(t time.Time) string { + return t.UTC().Format(ServerTimeLayout) +} + +// ParseCTCPMessage parses a CTCP message. CTCP is defined in +// https://tools.ietf.org/html/draft-oakley-irc-ctcp-02 +func ParseCTCPMessage(msg *irc.Message) (cmd string, params string, ok bool) { + if (msg.Command != "PRIVMSG" && msg.Command != "NOTICE") || len(msg.Params) < 2 { + return "", "", false + } + text := msg.Params[1] + + if !strings.HasPrefix(text, "\x01") { + return "", "", false + } + text = strings.Trim(text, "\x01") + + words := strings.SplitN(text, " ", 2) + cmd = strings.ToUpper(words[0]) + if len(words) > 1 { + params = words[1] + } + + return cmd, params, true +}