Introduce an xirc package

This commit is contained in:
Simon Ser 2022-05-09 16:15:00 +02:00
parent 89412187d4
commit b92afa7cca
6 changed files with 60 additions and 42 deletions

View file

@ -7,6 +7,8 @@ import (
"strings" "strings"
"gopkg.in/irc.v3" "gopkg.in/irc.v3"
"git.sr.ht/~emersion/soju/xirc"
) )
func forwardChannel(ctx context.Context, dc *downstreamConn, ch *upstreamChannel) { func forwardChannel(ctx context.Context, dc *downstreamConn, ch *upstreamChannel) {
@ -27,7 +29,7 @@ func forwardChannel(ctx context.Context, dc *downstreamConn, ch *upstreamChannel
} else { } else {
timestampStr := "*" timestampStr := "*"
if r != nil { if r != nil {
timestampStr = fmt.Sprintf("timestamp=%s", formatServerTime(r.Timestamp)) timestampStr = fmt.Sprintf("timestamp=%s", xirc.FormatServerTime(r.Timestamp))
} }
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.prefix(), Prefix: dc.prefix(),

View file

@ -18,6 +18,7 @@ import (
"gopkg.in/irc.v3" "gopkg.in/irc.v3"
"git.sr.ht/~emersion/soju/database" "git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/xirc"
) )
type ircError struct { 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) dc.logger.Printf("broadcasting bouncer-wide %v: %v", msg.Command, text)
broadcastTags := tags.Copy() broadcastTags := tags.Copy()
broadcastTags["time"] = irc.TagValue(formatServerTime(time.Now())) broadcastTags["time"] = irc.TagValue(xirc.FormatServerTime(time.Now()))
broadcastMsg := &irc.Message{ broadcastMsg := &irc.Message{
Tags: broadcastTags, Tags: broadcastTags,
Prefix: servicePrefix, Prefix: servicePrefix,
@ -2498,7 +2499,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
if casemapASCII(name) == serviceNickCM { if casemapASCII(name) == serviceNickCM {
if dc.caps.IsEnabled("echo-message") { if dc.caps.IsEnabled("echo-message") {
echoTags := tags.Copy() echoTags := tags.Copy()
echoTags["time"] = irc.TagValue(formatServerTime(time.Now())) echoTags["time"] = irc.TagValue(xirc.FormatServerTime(time.Now()))
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Tags: echoTags, Tags: echoTags,
Prefix: dc.prefix(), Prefix: dc.prefix(),
@ -2547,7 +2548,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
} }
echoTags := tags.Copy() echoTags := tags.Copy()
echoTags["time"] = irc.TagValue(formatServerTime(time.Now())) echoTags["time"] = irc.TagValue(xirc.FormatServerTime(time.Now()))
if uc.account != "" { if uc.account != "" {
echoTags["account"] = irc.TagValue(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}, Tags: irc.Tags{"batch": batchRef},
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: "CHATHISTORY", 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 { if err != nil {
return ircError{&irc.Message{ return ircError{&irc.Message{
Command: "FAIL", Command: "FAIL",
@ -2967,7 +2968,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
timestampStr := "*" timestampStr := "*"
if !r.Timestamp.IsZero() { 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) { network.forEachDownstream(func(d *downstreamConn) {
if broadcast || dc.id == d.id { if broadcast || dc.id == d.id {
@ -3001,7 +3002,7 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
value := string(v) value := string(v)
switch name { switch name {
case "before", "after": case "before", "after":
timestamp, err := time.Parse(serverTimeLayout, value) timestamp, err := time.Parse(xirc.ServerTimeLayout, value)
if err != nil { if err != nil {
return ircError{&irc.Message{ return ircError{&irc.Message{
Command: "FAIL", Command: "FAIL",

34
irc.go
View file

@ -11,8 +11,11 @@ import (
"gopkg.in/irc.v3" "gopkg.in/irc.v3"
"git.sr.ht/~emersion/soju/database" "git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/xirc"
) )
// TODO: generalize and move helpers to the xirc package
const ( const (
rpl_statsping = "246" rpl_statsping = "246"
rpl_localusers = "265" rpl_localusers = "265"
@ -42,13 +45,6 @@ const (
maxSASLLength = 400 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 type userModes string
func (ms userModes) Has(c byte) bool { 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] 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 type casemapping func(string) string
func casemapNone(name string) string { func casemapNone(name string) string {
@ -728,7 +702,7 @@ func parseChatHistoryBound(param string) time.Time {
} }
switch parts[0] { switch parts[0] {
case "timestamp": case "timestamp":
timestamp, err := time.Parse(serverTimeLayout, parts[1]) timestamp, err := time.Parse(xirc.ServerTimeLayout, parts[1])
if err != nil { if err != nil {
return time.Time{} return time.Time{}
} }

View file

@ -15,6 +15,7 @@ import (
"gopkg.in/irc.v3" "gopkg.in/irc.v3"
"git.sr.ht/~emersion/soju/database" "git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/xirc"
) )
const ( const (
@ -135,7 +136,7 @@ func (ms *fsMessageStore) Append(network *database.Network, entity string, msg *
var t time.Time var t time.Time
if tag, ok := msg.Tags["time"]; ok { if tag, ok := msg.Tags["time"]; ok {
var err error var err error
t, err = time.Parse(serverTimeLayout, string(tag)) t, err = time.Parse(xirc.ServerTimeLayout, string(tag))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse message time tag: %v", err) return "", fmt.Errorf("failed to parse message time tag: %v", err)
} }
@ -245,7 +246,7 @@ func formatMessage(msg *irc.Message) string {
case "NOTICE": case "NOTICE":
return fmt.Sprintf("-%s- %s", msg.Prefix.Name, msg.Params[1]) return fmt.Sprintf("-%s- %s", msg.Prefix.Name, msg.Params[1])
case "PRIVMSG": 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) return fmt.Sprintf("* %s %s", msg.Prefix.Name, params)
} else { } else {
return fmt.Sprintf("<%s> %s", msg.Prefix.Name, msg.Params[1]) 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{ msg := &irc.Message{
Tags: map[string]irc.TagValue{ Tags: map[string]irc.TagValue{
"time": irc.TagValue(formatServerTime(t)), "time": irc.TagValue(xirc.FormatServerTime(t)),
}, },
Prefix: prefix, Prefix: prefix,
Command: cmd, Command: cmd,

View file

@ -19,6 +19,7 @@ import (
"gopkg.in/irc.v3" "gopkg.in/irc.v3"
"git.sr.ht/~emersion/soju/database" "git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/xirc"
) )
// permanentUpstreamCaps is the static list of upstream capabilities always // 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) { 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 { switch msg.Command {

39
xirc/xirc.go Normal file
View file

@ -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
}