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
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, &currentUsers, &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, &currentUsers, &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 {