commit 5369aea
delthas
·
2023-11-15 00:38:33 +0000 UTC
parent 7fbbcec
Add miscellaneous commands and replies support See: https://todo.sr.ht/~taiite/senpai/125
6 files changed,
+679,
-96
M
app.go
+21,
-60
1@@ -7,7 +7,6 @@ import (
2 "net"
3 "os"
4 "os/exec"
5- "strconv"
6 "strings"
7 "sync"
8 "time"
9@@ -1059,67 +1058,38 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
10 }
11 app.win.RemoveNetworkBuffers(ev.ID)
12 }
13- case irc.ErrorEvent:
14- if isBlackListed(msg.Command) {
15- break
16+ case irc.InfoEvent:
17+ var head string
18+ if ev.Prefix != "" {
19+ head = ev.Prefix + " --"
20+ } else {
21+ head = "--"
22 }
23- switch ev.Code {
24- case "372":
25- app.win.AddLine(netID, "", ui.Line{
26- At: msg.TimeOrNow(),
27- Head: "MOTD --",
28- Body: ui.PlainString(ev.Message),
29- })
30- return
31- case "324":
32- channel, line, ok := strings.Cut(ev.Message, " ")
33- if ok {
34- text := fmt.Sprintf("%s has modes %s", channel, line)
35- app.win.AddLine(netID, channel, ui.Line{
36- At: time.Now(),
37- Head: "--",
38- HeadColor: app.cfg.Colors.Status,
39- Body: ui.Styled(text, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
40- })
41- return
42- }
43- case "329":
44- channel, line, ok := strings.Cut(ev.Message, " ")
45- if ok {
46- creation, err := strconv.ParseInt(line, 10, 64)
47- if err == nil {
48- t := time.Unix(creation, 0)
49- text := fmt.Sprintf("%s was created on %s", channel, t.Local().Format("January 2, 2006"))
50- app.win.AddLine(netID, channel, ui.Line{
51- At: time.Now(),
52- Head: "--",
53- HeadColor: app.cfg.Colors.Status,
54- Body: ui.Styled(text, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
55- })
56- return
57- }
58- }
59- case "305", "306":
60+ app.addStatusLine(netID, ui.Line{
61+ At: msg.TimeOrNow(),
62+ Head: head,
63+ HeadColor: app.cfg.Colors.Status,
64+ Body: ui.Styled(ev.Message, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
65+ })
66+ return
67+ case irc.ErrorEvent:
68+ var head string
69+ var body string
70+ switch ev.Severity {
71+ case irc.SeverityNote:
72 app.addStatusLine(netID, ui.Line{
73- At: time.Now(),
74- Head: "--",
75+ At: msg.TimeOrNow(),
76+ Head: fmt.Sprintf("(%s) --", ev.Code),
77 HeadColor: app.cfg.Colors.Status,
78 Body: ui.Styled(ev.Message, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
79 })
80 return
81- }
82- var head string
83- var body string
84- switch ev.Severity {
85 case irc.SeverityFail:
86 head = "--"
87 body = fmt.Sprintf("Error (code %s): %s", ev.Code, ev.Message)
88 case irc.SeverityWarn:
89 head = "--"
90 body = fmt.Sprintf("Warning (code %s): %s", ev.Code, ev.Message)
91- case irc.SeverityNote:
92- head = ev.Code + " --"
93- body = ev.Message
94 default:
95 panic("unreachable")
96 }
97@@ -1131,15 +1101,6 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
98 }
99 }
100
101-func isBlackListed(command string) bool {
102- switch command {
103- case "002", "003", "004", "375", "376", "396", "422":
104- // useless connection messages
105- return true
106- }
107- return false
108-}
109-
110 func isWordBoundary(r rune) bool {
111 switch r {
112 case '-', '_', '|': // inspired from weechat.look.highlight_regex
113@@ -1616,7 +1577,7 @@ func (app *App) printTopic(netID, buffer string) (ok bool) {
114 if who == nil {
115 body = fmt.Sprintf("Topic: %s", topic)
116 } else {
117- body = fmt.Sprintf("Topic (by %s, %s): %s", who.Name, at.Local().Format("Mon Jan 2 15:04:05"), topic)
118+ body = fmt.Sprintf("Topic (set by %s on %s): %s", who.Name, at.Local().Format("January 2 2006 at 15:04:05"), topic)
119 }
120 app.win.AddLine(netID, buffer, ui.Line{
121 At: time.Now(),
+116,
-25
1@@ -28,7 +28,7 @@ type command struct {
2 MaxArgs int
3 Usage string
4 Desc string
5- Handle func(app *App, args []string) error
6+ Handle func(app *App, args []string) error // nil = passthrough
7 }
8
9 type commandSet map[string]*command
10@@ -74,7 +74,6 @@ func init() {
11 "MOTD": {
12 AllowHome: true,
13 Desc: "show the message of the day (MOTD)",
14- Handle: commandDoMOTD,
15 },
16 "NAMES": {
17 Desc: "show the member list of the current channel",
18@@ -94,7 +93,6 @@ func init() {
19 MaxArgs: 2,
20 Usage: "<username> <password>",
21 Desc: "log in to an operator account",
22- Handle: commandDoOper,
23 },
24 "MODE": {
25 AllowHome: true,
26@@ -135,7 +133,6 @@ func init() {
27 },
28 "LIST": {
29 AllowHome: true,
30- MinArgs: 0,
31 MaxArgs: 1,
32 Usage: "[pattern]",
33 Desc: "list public channels",
34@@ -168,9 +165,17 @@ func init() {
35 MinArgs: 0,
36 MaxArgs: 1,
37 Usage: "<nick>",
38- Desc: "get information about someone",
39+ Desc: "get information about someone who is connected",
40 Handle: commandDoWhois,
41 },
42+ "WHOWAS": {
43+ AllowHome: true,
44+ MinArgs: 0,
45+ MaxArgs: 1,
46+ Usage: "<nick>",
47+ Desc: "get information about someone who is disconnected",
48+ Handle: commandDoWhowas,
49+ },
50 "INVITE": {
51 AllowHome: true,
52 MinArgs: 1,
53@@ -203,6 +208,27 @@ func init() {
54 Desc: "remove effect of a ban from the user",
55 Handle: commandDoUnban,
56 },
57+ "CONNECT": {
58+ AllowHome: true,
59+ MinArgs: 1,
60+ MaxArgs: 3,
61+ Usage: "<target server> [<port> [remote server]]",
62+ Desc: "connect a server to the network",
63+ },
64+ "SQUIT": {
65+ AllowHome: true,
66+ MinArgs: 1,
67+ MaxArgs: 2,
68+ Usage: "<server> [comment]",
69+ Desc: "disconnect a server from the network",
70+ },
71+ "KILL": {
72+ AllowHome: true,
73+ MinArgs: 1,
74+ MaxArgs: 2,
75+ Usage: "<nick> [message]",
76+ Desc: "eject someone from the server",
77+ },
78 "SEARCH": {
79 MaxArgs: 1,
80 Usage: "<text>",
81@@ -230,6 +256,58 @@ func init() {
82 Desc: "send a tableflip to the current channel (╯°□°)╯︵ ┻━┻",
83 Handle: commandDoTableFlip,
84 },
85+ "VERSION": {
86+ AllowHome: true,
87+ MaxArgs: 1,
88+ Usage: "[target]",
89+ Desc: "query the server software version",
90+ },
91+ "ADMIN": {
92+ AllowHome: true,
93+ MaxArgs: 1,
94+ Usage: "[target]",
95+ Desc: "query the server administrative information",
96+ },
97+ "LUSERS": {
98+ AllowHome: true,
99+ Desc: "query the server user information",
100+ },
101+ "TIME": {
102+ AllowHome: true,
103+ MaxArgs: 1,
104+ Usage: "[target]",
105+ Desc: "query the server local time",
106+ },
107+ "STATS": {
108+ AllowHome: true,
109+ MinArgs: 1,
110+ MaxArgs: 2,
111+ Usage: "<query> [target]",
112+ Desc: "query server statistics",
113+ },
114+ "INFO": {
115+ AllowHome: true,
116+ Desc: "query server information",
117+ },
118+ "REHASH": {
119+ AllowHome: true,
120+ Desc: "make the server reload its configuration",
121+ },
122+ "RESTART": {
123+ AllowHome: true,
124+ Desc: "make the server restart",
125+ },
126+ "LINKS": {
127+ AllowHome: true,
128+ Desc: "query the servers of the network",
129+ },
130+ "WALLOPS": {
131+ AllowHome: true,
132+ MinArgs: 1,
133+ MaxArgs: 1,
134+ Usage: "<text>",
135+ Desc: "broadcast a message to all users",
136+ },
137 }
138 }
139
140@@ -403,16 +481,6 @@ func commandDoMsg(app *App, args []string) (err error) {
141 return commandSendMessage(app, target, content)
142 }
143
144-func commandDoMOTD(app *App, args []string) (err error) {
145- netID, _ := app.win.CurrentBuffer()
146- s := app.sessions[netID]
147- if s == nil {
148- return errOffline
149- }
150- s.MOTD()
151- return nil
152-}
153-
154 func commandDoNames(app *App, args []string) (err error) {
155 netID, buffer := app.win.CurrentBuffer()
156 s := app.sessions[netID]
157@@ -458,15 +526,6 @@ func commandDoNick(app *App, args []string) (err error) {
158 return
159 }
160
161-func commandDoOper(app *App, args []string) (err error) {
162- s := app.CurrentSession()
163- if s == nil {
164- return errOffline
165- }
166- s.Oper(args[0], args[1])
167- return
168-}
169-
170 func commandDoMode(app *App, args []string) (err error) {
171 _, target := app.win.CurrentBuffer()
172 if len(args) > 0 && !strings.HasPrefix(args[0], "+") && !strings.HasPrefix(args[0], "-") {
173@@ -637,6 +696,25 @@ func commandDoWhois(app *App, args []string) (err error) {
174 return nil
175 }
176
177+func commandDoWhowas(app *App, args []string) (err error) {
178+ netID, channel := app.win.CurrentBuffer()
179+ s := app.sessions[netID]
180+ if s == nil {
181+ return errOffline
182+ }
183+ var nick string
184+ if len(args) == 0 {
185+ if channel == "" || s.IsChannel(channel) {
186+ return fmt.Errorf("either send this command from a DM, or specify the user")
187+ }
188+ nick = channel
189+ } else {
190+ nick = args[0]
191+ }
192+ s.Whowas(nick)
193+ return nil
194+}
195+
196 func commandDoInvite(app *App, args []string) (err error) {
197 nick := args[0]
198 netID, channel := app.win.CurrentBuffer()
199@@ -895,7 +973,20 @@ func (app *App) handleInput(buffer, content string) error {
200 return fmt.Errorf("command %s cannot be executed from a server buffer", chosenCMDName)
201 }
202
203- return cmd.Handle(app, args)
204+ if cmd.Handle != nil {
205+ return cmd.Handle(app, args)
206+ } else {
207+ if s := app.CurrentSession(); s != nil {
208+ if rawArgs != "" {
209+ s.Send(cmdName, args...)
210+ } else {
211+ s.Send(cmdName)
212+ }
213+ return nil
214+ } else {
215+ return errOffline
216+ }
217+ }
218 }
219
220 func getSong() (string, error) {
+45,
-0
1@@ -189,6 +189,12 @@ _name_ is matched case-insensitively. It can be one of the following:
2 *BUFFER* <name>
3 Switch to the buffer containing _name_.
4
5+*WHOIS* <nickname>
6+ Get information about someone who is connected.
7+
8+*WHOWAS* <nickname>
9+ Get information about someone who is disconnected.
10+
11 *NICK* <nickname>
12 Change your nickname.
13
14@@ -221,12 +227,51 @@ _name_ is matched case-insensitively. It can be one of the following:
15 *BACK*
16 Mark yourself as back from being away.
17
18+*VERSION* [target]
19+ Query the server software version.
20+
21+*ADMIN* [target]
22+ Query the server administrative information.
23+
24+*LUSERS*
25+ Query the server user information.
26+
27+*TIME* [target]
28+ Query the server local time.
29+
30+*INFO*
31+ Query server information.
32+
33+*LINKS*
34+ Query the servers of the network.
35+
36 *SHRUG*
37 Send a shrug emoji to the current channel. ¯\\\_(ツ)\_/¯
38
39 *TABLEFLIP*
40 Send a table flip emoji to the current channel. (╯°□°)╯︵ ┻━┻
41
42+*STATS* <query> [target]
43+ Query server statistics (advanced).
44+
45+*CONNECT* <target server> [<port> [remote server]]
46+ Connect a server to the network (advanced).
47+
48+*SQUIT* <server> [comment]
49+ Disconnects a server from the network (advanced).
50+
51+*KILL* <nick> [message]
52+ Eject someone from the server (advanced).
53+
54+*REHASH*
55+ Make the server reload its configuration (advanced).
56+
57+*RESTART*
58+ Make the server restart (advanced).
59+
60+*WALLOPS* [text]
61+ Broadcast a message to all users (advanced).
62+
63 # SEE ALSO
64
65 *senpai*(5)
+5,
-0
1@@ -4,6 +4,11 @@ import "time"
2
3 type Event interface{}
4
5+type InfoEvent struct {
6+ Prefix string
7+ Message string
8+}
9+
10 type ErrorEvent struct {
11 Severity Severity
12 Code string
+31,
-3
1@@ -8,7 +8,10 @@ const (
2 rplMyinfo = "004" // <servername> <version> <umodes> <chan modes> <chan modes with a parameter>
3 rplIsupport = "005" // 1*13<TOKEN[=value]> :are supported by this server
4
5+ rplStatscommands = "212" // <command> <count> [<byte count> <remote count>]
6+ rplEndofstats = "219" // <stats letter> :End of /STATS report
7 rplUmodeis = "221" // <modes>
8+ rplStatsuptime = "242" // :Server Up <days> days <hours>:<minutes>:<seconds>
9 rplLuserclient = "251" // :<int> users and <int> services on <int> servers
10 rplLuserop = "252" // <int> :operator(s) online
11 rplLuserunknown = "253" // <int> :unknown connection(s)
12@@ -17,44 +20,63 @@ const (
13 rplAdminme = "256" // <server> :Admin info
14 rplAdminloc1 = "257" // :<info>
15 rplAdminloc2 = "258" // :<info>
16- rplAdminmail = "259" // :<info>
17+ rplAdminemail = "259" // :<info>
18+ rplLocalusers = "265" // [<u> <m>] :Current local users <u>, max <m>
19+ rplGlobalusers = "266" // [<u> <m>] :Current global users <u>, max <m>
20+ rplWhoiscertfp = "276" // <nick> :has client certificate fingerprint <fingerprint>
21
22 rplAway = "301" // <nick> :<away message>
23+ rplUserhost = "302" // :[<reply>{ <reply>}]
24 rplUnaway = "305" // :You are no longer marked as being away
25 rplNowaway = "306" // :You have been marked as being away
26+ rplWhoisregnick = "307" // <nick> :has identified for this nick
27 rplWhoisuser = "311" // <nick> <user> <host> * :<realname>
28 rplWhoisserver = "312" // <nick> <server> :<server info>
29 rplWhoisoperator = "313" // <nick> :is an IRC operator
30+ rplWhowasuser = "314" // <nick> <username> <host> * :<realname>
31 rplEndofwho = "315" // <name> :End of WHO list
32 rplWhoisidle = "317" // <nick> <integer> [<integer>] :seconds idle [, signon time]
33 rplEndofwhois = "318" // <nick> :End of WHOIS list
34 rplWhoischannels = "319" // <nick> :*( (@/+) <channel> " " )
35+ rplWhoisspecial = "320" // <nick> :blah blah blah
36+ rplListstart = "321" // Channel :Users Name
37 rplList = "322" // <channel> <# of visible members> <topic>
38 rplListend = "323" // :End of list
39 rplChannelmodeis = "324" // <channel> <modes> <mode params>
40+ rplCreationTime = "329" // <channel> <creationtime>
41+ rplWhoisaccount = "330" // <nick> <account> :is logged in as
42 rplNotopic = "331" // <channel> :No topic set
43 rplTopic = "332" // <channel> <topic>
44 rplTopicwhotime = "333" // <channel> <nick> <setat>
45+ rplInvitelist = "336" // <channel>
46+ rplEndofinvitelist = "337" // <channel> :End of invite list
47+ rplWhoisactually = "338" // <nick> [<username>@<hostname>] [<ip>] :Is actually using host
48 rplInviting = "341" // <nick> <channel>
49- rplInvitelist = "346" // <channel> <invite mask>
50- rplEndofinvitelist = "347" // <channel> :End of invite list
51+ rplInvexlist = "346" // <channel> <mask>
52+ rplEndofinvexlist = "347" // <channel> :End of Channel Invite Exception List
53 rplExceptlist = "348" // <channel> <exception mask>
54 rplEndofexceptlist = "349" // <channel> :End of exception list
55 rplVersion = "351" // <version> <servername> :<comments>
56 rplWhoreply = "352" // <channel> <user> <host> <server> <nick> "H"/"G" ["*"] [("@"/"+")] :<hop count> <nick>
57 rplNamreply = "353" // <=/*/@> <channel> :1*(@/ /+user)
58 rplWhospecialreply = "354" // [token] [channel] [user] [ip] [host] [server] [nick] [flags] [hopcount] [idle] [account] [oplevel] [:realname]
59+ rplLinks = "364" // * <server> :<hopcount> <server info>
60+ rplEndoflinks = "365" // * :End of /LINKS list
61 rplEndofnames = "366" // <channel> :End of names list
62 rplBanlist = "367" // <channel> <ban mask>
63 rplEndofbanlist = "368" // <channel> :End of ban list
64+ rplEndofwhowas = "369" // <nick> :End of WHOWAS
65 rplInfo = "371" // :<info>
66 rplMotd = "372" // :- <text>
67 rplEndofinfo = "374" // :End of INFO
68 rplMotdstart = "375" // :- <servername> Message of the day -
69 rplEndofmotd = "376" // :End of MOTD command
70+ rplWhoishost = "378" // <nick> :is connecting from *@localhost 127.0.0.1
71+ rplWhoismodes = "379" // <nick> :is using modes +ailosw
72 rplYoureoper = "381" // :You are now an operator
73 rplRehashing = "382" // <config file> :Rehashing
74 rplTime = "391" // <servername> :<time in whatever format>
75+ rplHostHidden = "396"
76
77 errNosuchnick = "401" // <nick> :No such nick/channel
78 errNosuchchannel = "403" // <channel> :No such channel
79@@ -88,6 +110,12 @@ const (
80 errUmodeunknownflag = "501" // :Unknown mode flag
81 errUsersdontmatch = "502" // :Can't change mode for other users
82
83+ rplWhoissecure = "671" // <nick> :is using a secure connection
84+
85+ rplHelpstart = "704" // <subject> :<first line of help section>
86+ rplHelptxt = "705" // <subject> :<line of help text>
87+ rplEndofhelp = "706" // <subject> :<last line of help text>
88+
89 rplMononline = "730" // <nick> :target[!user@host][,target[!user@host]]*
90 rplMonoffline = "731" // <nick> :target[,target2]*
91 rplMonlist = "732" // <nick> :target[,target2]*
+461,
-8
1@@ -342,6 +342,10 @@ func (s *Session) SendRaw(raw string) {
2 s.out <- NewMessage(raw)
3 }
4
5+func (s *Session) Send(command string, params ...string) {
6+ s.out <- NewMessage(command, params...)
7+}
8+
9 func (s *Session) List(pattern string) {
10 if pattern != "" {
11 s.out <- NewMessage("LIST", pattern)
12@@ -376,14 +380,6 @@ func (s *Session) ChangeNick(nick string) {
13 s.out <- NewMessage("NICK", nick)
14 }
15
16-func (s *Session) Oper(username string, password string) {
17- s.out <- NewMessage("OPER", username, password)
18-}
19-
20-func (s *Session) MOTD() {
21- s.out <- NewMessage("MOTD")
22-}
23-
24 func (s *Session) Who(target string) {
25 if s.whox {
26 // only request what we need, to optimize server who cache hits and reduce traffic
27@@ -618,6 +614,10 @@ func (s *Session) Whois(nick string) {
28 s.out <- NewMessage("WHOIS", nick)
29 }
30
31+func (s *Session) Whowas(nick string) {
32+ s.out <- NewMessage("WHOWAS", nick)
33+}
34+
35 func (s *Session) Invite(nick, channel string) {
36 s.out <- NewMessage("INVITE", nick, channel)
37 }
38@@ -1453,6 +1453,459 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
39 // silence monlist full error, we don't care because we do it best-effort
40 case rplAway:
41 // we display user away status, we don't care about automatic AWAY replies
42+ case rplYourhost, rplCreated, rplMyinfo:
43+ // useless conection messages
44+ case rplAdminme:
45+ // useless admin info header
46+ case rplMotdstart, rplEndofmotd, errNomotd:
47+ // useless motd related messages
48+ case rplHostHidden:
49+ // useless host message
50+ case rplEndofstats:
51+ // useless stats delimiter
52+ case rplEndofwhois:
53+ // useless whois delimiter
54+ case rplListstart, rplListend:
55+ // useless list delimiter
56+ case rplEndofinvitelist, rplEndofinvexlist:
57+ // useless invite list delimiter
58+ case rplEndofexceptlist:
59+ // useless exception list delimiter
60+ case rplEndoflinks:
61+ // useless links delimiter
62+ case rplEndofbanlist:
63+ // useless ban list delimiter
64+ case rplEndofwhowas:
65+ // useless whois delimiter
66+ case rplEndofinfo:
67+ // useless info delimiter
68+ case rplHelpstart, rplEndofhelp:
69+ // useless help delimiter
70+ case rplStatscommands:
71+ var command, count string
72+ if err := msg.ParseParams(nil, &command, &count); err != nil {
73+ return nil, err
74+ }
75+ return InfoEvent{
76+ Prefix: "Stats",
77+ Message: fmt.Sprintf("The command %s was used %s times on the server", command, count),
78+ }, nil
79+ case rplUmodeis:
80+ if !s.receivedUserMode {
81+ // ignore the first RPL_UMODEIS on join
82+ s.receivedUserMode = true
83+ return nil, nil
84+ }
85+ return InfoEvent{
86+ Message: fmt.Sprintf("The current user modes are: %s", strings.Join(msg.Params[1:], " ")),
87+ }, nil
88+ case rplStatsuptime:
89+ return InfoEvent{
90+ Prefix: "Stats",
91+ Message: fmt.Sprintf("The server current uptime is: %s", msg.Params[len(msg.Params)-1]),
92+ }, nil
93+ case rplLuserclient:
94+ return InfoEvent{
95+ Prefix: "Stats",
96+ Message: msg.Params[len(msg.Params)-1],
97+ }, nil
98+ case rplLuserop:
99+ var ops string
100+ if err := msg.ParseParams(nil, &ops); err != nil {
101+ return nil, err
102+ }
103+ return InfoEvent{
104+ Prefix: "Stats",
105+ Message: fmt.Sprintf("There are %s operators online", ops),
106+ }, nil
107+ case rplLuserunknown:
108+ var connections string
109+ if err := msg.ParseParams(nil, &connections); err != nil {
110+ return nil, err
111+ }
112+ return InfoEvent{
113+ Prefix: "Stats",
114+ Message: fmt.Sprintf("There are %s unknown user connections", connections),
115+ }, nil
116+ case rplLuserchannels:
117+ var channels string
118+ if err := msg.ParseParams(nil, &channels); err != nil {
119+ return nil, err
120+ }
121+ return InfoEvent{
122+ Prefix: "Stats",
123+ Message: fmt.Sprintf("There are %s channels on the server", channels),
124+ }, nil
125+ case rplLuserme:
126+ return InfoEvent{
127+ Prefix: "Stats",
128+ Message: fmt.Sprintf("The server current stats are: %s", msg.Params[len(msg.Params)-1]),
129+ }, nil
130+ case rplAdminloc1:
131+ return InfoEvent{
132+ Prefix: "Admin",
133+ Message: fmt.Sprintf("The server location/environment information is: %s", msg.Params[len(msg.Params)-1]),
134+ }, nil
135+ case rplAdminloc2:
136+ return InfoEvent{
137+ Prefix: "Admin",
138+ Message: fmt.Sprintf("The server organization information is: %s", msg.Params[len(msg.Params)-1]),
139+ }, nil
140+ case rplAdminemail:
141+ return InfoEvent{
142+ Prefix: "Admin",
143+ Message: fmt.Sprintf("The server email contact is: %s", msg.Params[len(msg.Params)-1]),
144+ }, nil
145+ case rplLocalusers:
146+ if len(msg.Params) >= 4 {
147+ var currentUsers, maxUsers string
148+ if err := msg.ParseParams(nil, ¤tUsers, &maxUsers); err != nil {
149+ return nil, err
150+ }
151+ return InfoEvent{
152+ Prefix: "Stats",
153+ Message: fmt.Sprintf("There are %s online users on this server, out of a maximum of %s users", currentUsers, maxUsers),
154+ }, nil
155+ } else {
156+ return InfoEvent{
157+ Prefix: "Stats",
158+ Message: fmt.Sprintf("The server current local user counts are: %s", msg.Params[len(msg.Params)-1]),
159+ }, nil
160+ }
161+ case rplGlobalusers:
162+ if len(msg.Params) >= 4 {
163+ var currentUsers, maxUsers string
164+ if err := msg.ParseParams(nil, ¤tUsers, &maxUsers); err != nil {
165+ return nil, err
166+ }
167+ return InfoEvent{
168+ Prefix: "Stats",
169+ Message: fmt.Sprintf("There are %s online users on the network, out of a maximum of %s users", currentUsers, maxUsers),
170+ }, nil
171+ } else {
172+ return InfoEvent{
173+ Prefix: "Stats",
174+ Message: fmt.Sprintf("The server current global user counts are: %s", msg.Params[len(msg.Params)-1]),
175+ }, nil
176+ }
177+ case rplWhoiscertfp:
178+ var nick, text string
179+ if err := msg.ParseParams(nil, &nick, &text); err != nil {
180+ return nil, err
181+ }
182+ return InfoEvent{
183+ Prefix: "User",
184+ Message: fmt.Sprintf("The user %s %s", nick, text),
185+ }, nil
186+ case rplUnaway:
187+ return InfoEvent{
188+ Message: "You are now marked as back from being away",
189+ }, nil
190+ case rplNowaway:
191+ return InfoEvent{
192+ Message: "You are now marked as away",
193+ }, nil
194+ case rplWhoisregnick:
195+ var nick string
196+ if err := msg.ParseParams(nil, &nick); err != nil {
197+ return nil, err
198+ }
199+ return InfoEvent{
200+ Prefix: "User",
201+ Message: fmt.Sprintf("The user %s has identified and is registered to the server", nick),
202+ }, nil
203+ case rplWhoisuser:
204+ var nick, username, host, realname string
205+ if err := msg.ParseParams(nil, &nick, &username, &host, nil, &realname); err != nil {
206+ return nil, err
207+ }
208+ return InfoEvent{
209+ Prefix: "User",
210+ Message: fmt.Sprintf("The user %s has username %s and host %s (mask %s!%s@%s); their realname is %s", nick, username, host, nick, username, host, realname),
211+ }, nil
212+ case rplWhoisserver:
213+ var nick, server, serverInfo string
214+ if err := msg.ParseParams(nil, &nick, &server, &serverInfo); err != nil {
215+ return nil, err
216+ }
217+ return InfoEvent{
218+ Prefix: "User",
219+ Message: fmt.Sprintf("The user %s is connected through the server %s (%s)", nick, server, serverInfo),
220+ }, nil
221+ case rplWhoisoperator:
222+ var nick string
223+ if err := msg.ParseParams(nil, &nick); err != nil {
224+ return nil, err
225+ }
226+ return InfoEvent{
227+ Prefix: "User",
228+ Message: fmt.Sprintf("The user %s is an operator of this server", nick),
229+ }, nil
230+ case rplWhowasuser:
231+ var nick, username, host, realname string
232+ if err := msg.ParseParams(nil, &nick, &username, &host, nil, &realname); err != nil {
233+ return nil, err
234+ }
235+ return InfoEvent{
236+ Prefix: "User",
237+ Message: fmt.Sprintf("The user %s was last seen with username %s and host %s (mask %s!%s@%s); their realname was %s", nick, username, host, nick, username, host, realname),
238+ }, nil
239+ case rplWhoisidle:
240+ var nick, idleText, signonText string
241+ if err := msg.ParseParams(nil, &nick, &idleText, &signonText); err != nil {
242+ return nil, err
243+ }
244+ idleSeconds, err := strconv.ParseInt(idleText, 10, 64)
245+ if err != nil {
246+ return nil, err
247+ }
248+ signon, err := strconv.ParseInt(signonText, 10, 64)
249+ if err != nil {
250+ return nil, err
251+ }
252+ idle := (time.Duration(idleSeconds) * time.Second).String()
253+ t := time.Unix(signon, 0)
254+ text := fmt.Sprintf("The user %s was idle for %s; they signed-on on %s", nick, idle, t.Local().Format("January 2 at 15:04"))
255+ return InfoEvent{
256+ Prefix: "User",
257+ Message: text,
258+ }, nil
259+ case rplWhoischannels:
260+ var nick, text string
261+ if err := msg.ParseParams(nil, &nick, &text); err != nil {
262+ return nil, err
263+ }
264+ return InfoEvent{
265+ Prefix: "User",
266+ Message: fmt.Sprintf("The user %s has joined channels: %s", nick, text),
267+ }, nil
268+ case rplWhoisspecial:
269+ var nick, text string
270+ if err := msg.ParseParams(nil, &nick, &text); err != nil {
271+ return nil, err
272+ }
273+ return InfoEvent{
274+ Prefix: "User",
275+ Message: fmt.Sprintf("The user %s is also: %s", nick, text),
276+ }, nil
277+ case rplList:
278+ var channel, count, topic string
279+ if err := msg.ParseParams(nil, &channel, &count, &topic); err != nil {
280+ return nil, err
281+ }
282+ text := fmt.Sprintf("There are %4s users on channel %s", count, channel)
283+ if topic != "" {
284+ text += " -- " + topic
285+ }
286+ return InfoEvent{
287+ Prefix: "List",
288+ Message: text,
289+ }, nil
290+ case rplChannelmodeis:
291+ var channel string
292+ if err := msg.ParseParams(nil, &channel); err != nil {
293+ return nil, err
294+ }
295+ text := fmt.Sprintf("%s has modes %s", channel, strings.Join(msg.Params[2:], " "))
296+ return InfoEvent{
297+ Message: text,
298+ }, nil
299+ case rplCreationTime:
300+ var channel, creationTime string
301+ if err := msg.ParseParams(nil, &channel, &creationTime); err != nil {
302+ return nil, err
303+ }
304+ creation, err := strconv.ParseInt(creationTime, 10, 64)
305+ if err != nil {
306+ return nil, err
307+ }
308+ t := time.Unix(creation, 0)
309+ text := fmt.Sprintf("%s was created on %s", channel, t.Local().Format("January 2, 2006"))
310+ return InfoEvent{
311+ Message: text,
312+ }, nil
313+ case rplWhoisaccount:
314+ var nick, account string
315+ if err := msg.ParseParams(nil, &nick, &account); err != nil {
316+ return nil, err
317+ }
318+ if nick != account {
319+ return InfoEvent{
320+ Prefix: "User",
321+ Message: fmt.Sprintf("The user %s is authenticated as %s", nick, account),
322+ }, nil
323+ } else {
324+ return InfoEvent{
325+ Prefix: "User",
326+ Message: fmt.Sprintf("The user %s is authenticated", nick),
327+ }, nil
328+ }
329+ case rplInvitelist, rplInvexlist:
330+ if len(msg.Params) == 2 { // RPL_INVITELIST
331+ var channel string
332+ if err := msg.ParseParams(nil, &channel); err != nil {
333+ return nil, err
334+ }
335+ return InfoEvent{
336+ Prefix: "Invite",
337+ Message: fmt.Sprintf("You were previously invited to the channel %s", channel),
338+ }, nil
339+ } else { // RPL_INVEXLIST
340+ var channel, mask string
341+ if err := msg.ParseParams(nil, &channel, &mask); err != nil {
342+ return nil, err
343+ }
344+ return InfoEvent{
345+ Prefix: "Invite-free",
346+ Message: fmt.Sprintf("The channel %s can be joined without invites from host %s", channel, mask),
347+ }, nil
348+ }
349+ case rplWhoisactually:
350+ if len(msg.Params) == 3 {
351+ var nick, text string
352+ if err := msg.ParseParams(nil, &nick, &text); err != nil {
353+ return nil, err
354+ }
355+ return InfoEvent{
356+ Prefix: "User",
357+ Message: fmt.Sprintf("The user %s %s", nick, text),
358+ }, nil
359+ } else if len(msg.Params) >= 4 {
360+ var nick string
361+ if err := msg.ParseParams(nil, &nick); err != nil {
362+ return nil, err
363+ }
364+ return InfoEvent{
365+ Prefix: "User",
366+ Message: fmt.Sprintf("The user %s is actually using the host %s", nick, msg.Params[len(msg.Params)-2]),
367+ }, nil
368+ }
369+ case rplExceptlist:
370+ var channel, mask string
371+ if err := msg.ParseParams(nil, &channel, &mask); err != nil {
372+ return nil, err
373+ }
374+ return InfoEvent{
375+ Prefix: "Exempt",
376+ Message: fmt.Sprintf("The channel %s exempts from bans users from host %s", channel, mask),
377+ }, nil
378+ case rplVersion:
379+ var version string
380+ if err := msg.ParseParams(nil, &version); err != nil {
381+ return nil, err
382+ }
383+ return InfoEvent{
384+ Message: fmt.Sprintf("The server is running: %s", version),
385+ }, nil
386+ case rplLinks:
387+ var prefix, last string
388+ if err := msg.ParseParams(nil, &prefix, nil, &last); err != nil {
389+ return nil, err
390+ }
391+ hop, info, ok := strings.Cut(last, " ")
392+ if !ok {
393+ hop = "0"
394+ info = last
395+ }
396+ var count int
397+ if c, err := strconv.Atoi(hop); err == nil {
398+ count = c
399+ }
400+ return InfoEvent{
401+ Prefix: "Link",
402+ Message: fmt.Sprintf("The network has server %s%s (%s)", strings.Repeat("* ", count), prefix, info),
403+ }, nil
404+ case rplBanlist:
405+ if len(msg.Params) >= 5 {
406+ var channel, mask, who, whenText string
407+ if err := msg.ParseParams(nil, &channel, &mask, &who, &whenText); err != nil {
408+ return nil, err
409+ }
410+ when, err := strconv.ParseInt(whenText, 10, 64)
411+ if err != nil {
412+ return nil, err
413+ }
414+ t := time.Unix(when, 0).Local().Format("January 2 2006 at 15:04")
415+ return InfoEvent{
416+ Prefix: "Ban",
417+ Message: fmt.Sprintf("The channel %s has %s banned by %s on %s", channel, mask, who, t),
418+ }, nil
419+ } else {
420+ var channel, mask string
421+ if err := msg.ParseParams(nil, &channel, &mask); err != nil {
422+ return nil, err
423+ }
424+ return InfoEvent{
425+ Prefix: "Ban",
426+ Message: fmt.Sprintf("The channel %s has %s banned", channel, mask),
427+ }, nil
428+ }
429+ case rplInfo:
430+ var text string
431+ if err := msg.ParseParams(nil, &text); err != nil {
432+ return nil, err
433+ }
434+ return InfoEvent{
435+ Prefix: "Info",
436+ Message: text,
437+ }, nil
438+ case rplMotd:
439+ return InfoEvent{
440+ Prefix: "MotD",
441+ Message: msg.Params[1],
442+ }, nil
443+ case rplWhoishost:
444+ var nick, text string
445+ if err := msg.ParseParams(nil, &nick, &text); err != nil {
446+ return nil, err
447+ }
448+ return InfoEvent{
449+ Prefix: "User",
450+ Message: fmt.Sprintf("The user %s %s", nick, text),
451+ }, nil
452+ case rplWhoismodes:
453+ var nick, text string
454+ if err := msg.ParseParams(nil, &nick, &text); err != nil {
455+ return nil, err
456+ }
457+ return InfoEvent{
458+ Prefix: "User",
459+ Message: fmt.Sprintf("The user %s %s", nick, text),
460+ }, nil
461+ case rplYoureoper:
462+ var text string
463+ if err := msg.ParseParams(nil, &text); err != nil {
464+ return nil, err
465+ }
466+ return InfoEvent{
467+ Message: text,
468+ }, nil
469+ case rplRehashing:
470+ return InfoEvent{
471+ Message: "The server configuration is now reloading (rehash)",
472+ }, nil
473+ case rplTime:
474+ return InfoEvent{
475+ Message: fmt.Sprintf("The server current local time is: %s", msg.Params[len(msg.Params)-1]),
476+ }, nil
477+ case rplWhoissecure:
478+ var nick, text string
479+ if err := msg.ParseParams(nil, &nick, &text); err != nil {
480+ return nil, err
481+ }
482+ return InfoEvent{
483+ Prefix: "User",
484+ Message: fmt.Sprintf("The user %s %s", nick, text),
485+ }, nil
486+ case rplHelptxt:
487+ var text string
488+ if err := msg.ParseParams(nil, nil, &text); err != nil {
489+ return nil, err
490+ }
491+ return InfoEvent{
492+ Prefix: "Help",
493+ Message: text,
494+ }, nil
495 default:
496 if msg.IsReply() {
497 if len(msg.Params) < 2 {