feat: implement IRC numerics batch 2 — connection registration, channel ops, user queries (#59)
All checks were successful
check / check (push) Successful in 5s
All checks were successful
check / check (push) Successful in 5s
## Summary Implements the remaining important/commonly-used IRC numeric reply codes, as requested in [issue #52](#52). ### Connection Registration (001-005) - **002 RPL_YOURHOST** — "Your host is <server>, running version <ver>" - **003 RPL_CREATED** — "This server was created <date>" - **004 RPL_MYINFO** — "<server> <version> <usermodes> <chanmodes>" - **005 RPL_ISUPPORT** — CHANTYPES=#, NICKLEN=32, CHANMODES, NETWORK=neoirc, CASEMAPPING=ascii All sent automatically after RPL_WELCOME during session creation/login. ### Server Statistics (251-255) - **251 RPL_LUSERCLIENT** — user count - **252 RPL_LUSEROP** — operator count - **254 RPL_LUSERCHANNELS** — channel count - **255 RPL_LUSERME** — local client count Sent during connection registration and available via LUSERS command. ### Channel Operations - **MODE command** — query channel modes (324 RPL_CHANNELMODEIS + 329 RPL_CREATIONTIME) and user modes (221 RPL_UMODEIS) - **NAMES command** — query channel member list (reuses 353/366) - **LIST command** — list all channels with member counts (322 RPL_LIST + 323 end) ### User Queries - **WHOIS command** — 311 RPL_WHOISUSER, 312 RPL_WHOISSERVER, 319 RPL_WHOISCHANNELS, 318 RPL_ENDOFWHOIS - **WHO command** — 352 RPL_WHOREPLY, 315 RPL_ENDOFWHO ### Database Additions - `GetChannelCount()` — total channel count for LUSERS - `ListAllChannelsWithCounts()` — channels with member counts for LIST - `GetChannelCreatedAt()` — channel creation time for RPL_CREATIONTIME - `GetSessionCreatedAt()` — session creation time ### Other Changes - Added `StartTime` to `Globals` struct for RPL_CREATED - Updated README with comprehensive documentation of all new commands and numerics - Updated roadmap to reflect implemented features `docker build .` passes (lint, tests, build all green). closes [#52](#52) <!-- session: agent:sdlc-manager:subagent:1f3dcab8-ad6a-4c4c-af72-34a617640c9d --> Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Co-authored-by: clawbot <clawbot@git.eeqj.de> Reviewed-on: #59 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud> Co-committed-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #59.
This commit is contained in:
156
README.md
156
README.md
@@ -764,21 +764,98 @@ not pollute the message queue.
|
|||||||
|
|
||||||
**IRC reference:** RFC 1459 §4.6.2, §4.6.3
|
**IRC reference:** RFC 1459 §4.6.2, §4.6.3
|
||||||
|
|
||||||
#### MODE — Set/Query Modes (Planned)
|
#### MODE — Query Modes
|
||||||
|
|
||||||
Set channel or user modes.
|
Query channel or user modes. Returns the current mode string and, for
|
||||||
|
channels, the creation timestamp.
|
||||||
|
|
||||||
**C2S:**
|
**C2S:**
|
||||||
```json
|
```json
|
||||||
{"command": "MODE", "to": "#general", "params": ["+m"]}
|
{"command": "MODE", "to": "#general"}
|
||||||
{"command": "MODE", "to": "#general", "params": ["+o", "alice"]}
|
{"command": "MODE", "to": "alice"}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status:** Not yet implemented. See [Channel Modes](#channel-modes) for the
|
**S2C (via message queue):**
|
||||||
planned mode set.
|
|
||||||
|
For channels, the server sends RPL_CHANNELMODEIS (324) and
|
||||||
|
RPL_CREATIONTIME (329):
|
||||||
|
```json
|
||||||
|
{"command": "324", "to": "alice", "params": ["#general", "+n"]}
|
||||||
|
{"command": "329", "to": "alice", "params": ["#general", "1709251200"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
For users, the server sends RPL_UMODEIS (221):
|
||||||
|
```json
|
||||||
|
{"command": "221", "to": "alice", "body": ["+"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Mode changes (setting/unsetting modes) are not yet implemented.
|
||||||
|
Currently only query is supported.
|
||||||
|
|
||||||
**IRC reference:** RFC 1459 §4.2.3
|
**IRC reference:** RFC 1459 §4.2.3
|
||||||
|
|
||||||
|
#### NAMES — Channel Member List
|
||||||
|
|
||||||
|
Request the member list for a channel. Returns RPL_NAMREPLY (353) and
|
||||||
|
RPL_ENDOFNAMES (366).
|
||||||
|
|
||||||
|
**C2S:**
|
||||||
|
```json
|
||||||
|
{"command": "NAMES", "to": "#general"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IRC reference:** RFC 1459 §4.2.5
|
||||||
|
|
||||||
|
#### LIST — List Channels
|
||||||
|
|
||||||
|
Request a list of all channels with member counts. Returns RPL_LIST (322)
|
||||||
|
for each channel followed by RPL_LISTEND (323).
|
||||||
|
|
||||||
|
**C2S:**
|
||||||
|
```json
|
||||||
|
{"command": "LIST"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IRC reference:** RFC 1459 §4.2.6
|
||||||
|
|
||||||
|
#### WHOIS — User Information
|
||||||
|
|
||||||
|
Query information about a user. Returns RPL_WHOISUSER (311),
|
||||||
|
RPL_WHOISSERVER (312), RPL_WHOISCHANNELS (319), and RPL_ENDOFWHOIS (318).
|
||||||
|
|
||||||
|
**C2S:**
|
||||||
|
```json
|
||||||
|
{"command": "WHOIS", "to": "alice"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IRC reference:** RFC 1459 §4.5.2
|
||||||
|
|
||||||
|
#### WHO — Channel User List
|
||||||
|
|
||||||
|
Query users in a channel. Returns RPL_WHOREPLY (352) for each user followed
|
||||||
|
by RPL_ENDOFWHO (315).
|
||||||
|
|
||||||
|
**C2S:**
|
||||||
|
```json
|
||||||
|
{"command": "WHO", "to": "#general"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IRC reference:** RFC 1459 §4.5.1
|
||||||
|
|
||||||
|
#### LUSERS — Server Statistics
|
||||||
|
|
||||||
|
Request server user/channel statistics. Returns RPL_LUSERCLIENT (251),
|
||||||
|
RPL_LUSEROP (252), RPL_LUSERCHANNELS (254), and RPL_LUSERME (255).
|
||||||
|
|
||||||
|
**C2S:**
|
||||||
|
```json
|
||||||
|
{"command": "LUSERS"}
|
||||||
|
```
|
||||||
|
|
||||||
|
LUSERS replies are also sent automatically during connection registration.
|
||||||
|
|
||||||
|
**IRC reference:** RFC 1459 §4.3.2
|
||||||
|
|
||||||
#### KICK — Kick User (Planned)
|
#### KICK — Kick User (Planned)
|
||||||
|
|
||||||
Remove a user from a channel.
|
Remove a user from a channel.
|
||||||
@@ -828,12 +905,27 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
|||||||
| Code | Name | When Sent | Example |
|
| Code | Name | When Sent | Example |
|
||||||
|------|----------------------|-----------|---------|
|
|------|----------------------|-----------|---------|
|
||||||
| `001` | RPL_WELCOME | After session creation | `{"command":"001","to":"alice","body":["Welcome to the network, alice"]}` |
|
| `001` | RPL_WELCOME | After session creation | `{"command":"001","to":"alice","body":["Welcome to the network, alice"]}` |
|
||||||
| `002` | RPL_YOURHOST | After session creation | `{"command":"002","to":"alice","body":["Your host is neoirc-server, running version 0.1"]}` |
|
| `002` | RPL_YOURHOST | After session creation | `{"command":"002","to":"alice","body":["Your host is neoirc, running version 0.1"]}` |
|
||||||
| `003` | RPL_CREATED | After session creation | `{"command":"003","to":"alice","body":["This server was created 2026-02-10"]}` |
|
| `003` | RPL_CREATED | After session creation | `{"command":"003","to":"alice","body":["This server was created 2026-02-10"]}` |
|
||||||
| `004` | RPL_MYINFO | After session creation | `{"command":"004","to":"alice","params":["neoirc-server","0.1","","imnst"]}` |
|
| `004` | RPL_MYINFO | After session creation | `{"command":"004","to":"alice","params":["neoirc","0.1","","imnst"]}` |
|
||||||
|
| `005` | RPL_ISUPPORT | After session creation | `{"command":"005","to":"alice","params":["CHANTYPES=#","NICKLEN=32","NETWORK=neoirc"],"body":["are supported by this server"]}` |
|
||||||
|
| `221` | RPL_UMODEIS | In response to user MODE query | `{"command":"221","to":"alice","body":["+"]}` |
|
||||||
|
| `251` | RPL_LUSERCLIENT | On connect or LUSERS command | `{"command":"251","to":"alice","body":["There are 5 users and 0 invisible on 1 servers"]}` |
|
||||||
|
| `252` | RPL_LUSEROP | On connect or LUSERS command | `{"command":"252","to":"alice","params":["0"],"body":["operator(s) online"]}` |
|
||||||
|
| `254` | RPL_LUSERCHANNELS | On connect or LUSERS command | `{"command":"254","to":"alice","params":["3"],"body":["channels formed"]}` |
|
||||||
|
| `255` | RPL_LUSERME | On connect or LUSERS command | `{"command":"255","to":"alice","body":["I have 5 clients and 1 servers"]}` |
|
||||||
|
| `311` | RPL_WHOISUSER | In response to WHOIS | `{"command":"311","to":"alice","params":["bob","bob","neoirc","*"],"body":["bob"]}` |
|
||||||
|
| `312` | RPL_WHOISSERVER | In response to WHOIS | `{"command":"312","to":"alice","params":["bob","neoirc"],"body":["neoirc server"]}` |
|
||||||
|
| `315` | RPL_ENDOFWHO | End of WHO response | `{"command":"315","to":"alice","params":["#general"],"body":["End of /WHO list"]}` |
|
||||||
|
| `318` | RPL_ENDOFWHOIS | End of WHOIS response | `{"command":"318","to":"alice","params":["bob"],"body":["End of /WHOIS list"]}` |
|
||||||
|
| `319` | RPL_WHOISCHANNELS | In response to WHOIS | `{"command":"319","to":"alice","params":["bob"],"body":["#general #dev"]}` |
|
||||||
| `322` | RPL_LIST | In response to LIST | `{"command":"322","to":"alice","params":["#general","5"],"body":["General discussion"]}` |
|
| `322` | RPL_LIST | In response to LIST | `{"command":"322","to":"alice","params":["#general","5"],"body":["General discussion"]}` |
|
||||||
| `323` | RPL_LISTEND | End of LIST response | `{"command":"323","to":"alice","body":["End of /LIST"]}` |
|
| `323` | RPL_LISTEND | End of LIST response | `{"command":"323","to":"alice","body":["End of /LIST"]}` |
|
||||||
|
| `324` | RPL_CHANNELMODEIS | In response to channel MODE query | `{"command":"324","to":"alice","params":["#general","+n"]}` |
|
||||||
|
| `329` | RPL_CREATIONTIME | After channel MODE query | `{"command":"329","to":"alice","params":["#general","1709251200"]}` |
|
||||||
|
| `331` | RPL_NOTOPIC | Channel has no topic (on JOIN) | `{"command":"331","to":"alice","params":["#general"],"body":["No topic is set"]}` |
|
||||||
| `332` | RPL_TOPIC | On JOIN or TOPIC query | `{"command":"332","to":"alice","params":["#general"],"body":["Welcome!"]}` |
|
| `332` | RPL_TOPIC | On JOIN or TOPIC query | `{"command":"332","to":"alice","params":["#general"],"body":["Welcome!"]}` |
|
||||||
|
| `352` | RPL_WHOREPLY | In response to WHO | `{"command":"352","to":"alice","params":["#general","bob","neoirc","neoirc","bob","H"],"body":["0 bob"]}` |
|
||||||
| `353` | RPL_NAMREPLY | On JOIN or NAMES query | `{"command":"353","to":"alice","params":["=","#general"],"body":["@op1 alice bob +voiced1"]}` |
|
| `353` | RPL_NAMREPLY | On JOIN or NAMES query | `{"command":"353","to":"alice","params":["=","#general"],"body":["@op1 alice bob +voiced1"]}` |
|
||||||
| `366` | RPL_ENDOFNAMES | End of NAMES response | `{"command":"366","to":"alice","params":["#general"],"body":["End of /NAMES list"]}` |
|
| `366` | RPL_ENDOFNAMES | End of NAMES response | `{"command":"366","to":"alice","params":["#general"],"body":["End of /NAMES list"]}` |
|
||||||
| `372` | RPL_MOTD | MOTD line | `{"command":"372","to":"alice","body":["Welcome to the server"]}` |
|
| `372` | RPL_MOTD | MOTD line | `{"command":"372","to":"alice","body":["Welcome to the server"]}` |
|
||||||
@@ -841,8 +933,11 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
|||||||
| `376` | RPL_ENDOFMOTD | End of MOTD | `{"command":"376","to":"alice","body":["End of /MOTD command"]}` |
|
| `376` | RPL_ENDOFMOTD | End of MOTD | `{"command":"376","to":"alice","body":["End of /MOTD command"]}` |
|
||||||
| `401` | ERR_NOSUCHNICK | DM to nonexistent nick | `{"command":"401","to":"alice","params":["bob"],"body":["No such nick/channel"]}` |
|
| `401` | ERR_NOSUCHNICK | DM to nonexistent nick | `{"command":"401","to":"alice","params":["bob"],"body":["No such nick/channel"]}` |
|
||||||
| `403` | ERR_NOSUCHCHANNEL | Action on nonexistent channel | `{"command":"403","to":"alice","params":["#nope"],"body":["No such channel"]}` |
|
| `403` | ERR_NOSUCHCHANNEL | Action on nonexistent channel | `{"command":"403","to":"alice","params":["#nope"],"body":["No such channel"]}` |
|
||||||
|
| `421` | ERR_UNKNOWNCOMMAND | Unrecognized command | `{"command":"421","to":"alice","params":["FOO"],"body":["Unknown command"]}` |
|
||||||
|
| `432` | ERR_ERRONEUSNICKNAME | Invalid nick format | `{"command":"432","to":"alice","params":["bad nick!"],"body":["Erroneous nickname"]}` |
|
||||||
| `433` | ERR_NICKNAMEINUSE | NICK to taken nick | `{"command":"433","to":"*","params":["alice"],"body":["Nickname is already in use"]}` |
|
| `433` | ERR_NICKNAMEINUSE | NICK to taken nick | `{"command":"433","to":"*","params":["alice"],"body":["Nickname is already in use"]}` |
|
||||||
| `442` | ERR_NOTONCHANNEL | Action on unjoined channel | `{"command":"442","to":"alice","params":["#general"],"body":["You're not on that channel"]}` |
|
| `442` | ERR_NOTONCHANNEL | Action on unjoined channel | `{"command":"442","to":"alice","params":["#general"],"body":["You're not on that channel"]}` |
|
||||||
|
| `461` | ERR_NEEDMOREPARAMS | Missing required fields | `{"command":"461","to":"alice","params":["JOIN"],"body":["Not enough parameters"]}` |
|
||||||
| `482` | ERR_CHANOPRIVSNEEDED | Non-op tries op action | `{"command":"482","to":"alice","params":["#general"],"body":["You're not channel operator"]}` |
|
| `482` | ERR_CHANOPRIVSNEEDED | Non-op tries op action | `{"command":"482","to":"alice","params":["#general"],"body":["You're not channel operator"]}` |
|
||||||
|
|
||||||
**Note:** Numeric replies are now implemented. All IRC command responses
|
**Note:** Numeric replies are now implemented. All IRC command responses
|
||||||
@@ -1061,6 +1156,12 @@ reference with all required and optional fields.
|
|||||||
| `PART` | `to` | `body` | 200 OK |
|
| `PART` | `to` | `body` | 200 OK |
|
||||||
| `NICK` | `body` | | 200 OK |
|
| `NICK` | `body` | | 200 OK |
|
||||||
| `TOPIC` | `to`, `body` | | 200 OK |
|
| `TOPIC` | `to`, `body` | | 200 OK |
|
||||||
|
| `MODE` | `to` | | 200 OK |
|
||||||
|
| `NAMES` | `to` | | 200 OK |
|
||||||
|
| `LIST` | | | 200 OK |
|
||||||
|
| `WHOIS` | `to` or `body` | | 200 OK |
|
||||||
|
| `WHO` | `to` | | 200 OK |
|
||||||
|
| `LUSERS` | | | 200 OK |
|
||||||
| `QUIT` | | `body` | 200 OK |
|
| `QUIT` | | `body` | 200 OK |
|
||||||
| `PING` | | | 200 OK |
|
| `PING` | | | 200 OK |
|
||||||
|
|
||||||
@@ -1095,10 +1196,29 @@ auth tokens (401), and server errors (500).
|
|||||||
| Numeric | Name | When |
|
| Numeric | Name | When |
|
||||||
|---------|------|------|
|
|---------|------|------|
|
||||||
| 001 | RPL_WELCOME | Sent on session creation/login |
|
| 001 | RPL_WELCOME | Sent on session creation/login |
|
||||||
|
| 002 | RPL_YOURHOST | Sent on session creation/login |
|
||||||
|
| 003 | RPL_CREATED | Sent on session creation/login |
|
||||||
|
| 004 | RPL_MYINFO | Sent on session creation/login |
|
||||||
|
| 005 | RPL_ISUPPORT | Sent on session creation/login |
|
||||||
|
| 221 | RPL_UMODEIS | In response to user MODE query |
|
||||||
|
| 251 | RPL_LUSERCLIENT | On connect or LUSERS command |
|
||||||
|
| 252 | RPL_LUSEROP | On connect or LUSERS command |
|
||||||
|
| 254 | RPL_LUSERCHANNELS | On connect or LUSERS command |
|
||||||
|
| 255 | RPL_LUSERME | On connect or LUSERS command |
|
||||||
|
| 311 | RPL_WHOISUSER | WHOIS user info |
|
||||||
|
| 312 | RPL_WHOISSERVER | WHOIS server info |
|
||||||
|
| 315 | RPL_ENDOFWHO | End of WHO list |
|
||||||
|
| 318 | RPL_ENDOFWHOIS | End of WHOIS list |
|
||||||
|
| 319 | RPL_WHOISCHANNELS | WHOIS channels list |
|
||||||
|
| 322 | RPL_LIST | Channel in LIST response |
|
||||||
|
| 323 | RPL_LISTEND | End of LIST |
|
||||||
|
| 324 | RPL_CHANNELMODEIS | Channel mode query response |
|
||||||
|
| 329 | RPL_CREATIONTIME | Channel creation timestamp |
|
||||||
| 331 | RPL_NOTOPIC | Channel has no topic (on JOIN) |
|
| 331 | RPL_NOTOPIC | Channel has no topic (on JOIN) |
|
||||||
| 332 | RPL_TOPIC | Channel topic (on JOIN, TOPIC set) |
|
| 332 | RPL_TOPIC | Channel topic (on JOIN, TOPIC set) |
|
||||||
| 353 | RPL_NAMREPLY | Channel member list (on JOIN) |
|
| 352 | RPL_WHOREPLY | User in WHO response |
|
||||||
| 366 | RPL_ENDOFNAMES | End of NAMES list (on JOIN) |
|
| 353 | RPL_NAMREPLY | Channel member list (on JOIN, NAMES) |
|
||||||
|
| 366 | RPL_ENDOFNAMES | End of NAMES list |
|
||||||
| 375 | RPL_MOTDSTART | Start of MOTD |
|
| 375 | RPL_MOTDSTART | Start of MOTD |
|
||||||
| 372 | RPL_MOTD | MOTD line |
|
| 372 | RPL_MOTD | MOTD line |
|
||||||
| 376 | RPL_ENDOFMOTD | End of MOTD |
|
| 376 | RPL_ENDOFMOTD | End of MOTD |
|
||||||
@@ -2104,10 +2224,18 @@ GET /api/v1/challenge
|
|||||||
- [ ] **Message rotation** — enforce `MAX_HISTORY` per channel
|
- [ ] **Message rotation** — enforce `MAX_HISTORY` per channel
|
||||||
- [ ] **Channel modes** — enforce `+i`, `+m`, `+s`, `+t`, `+n`
|
- [ ] **Channel modes** — enforce `+i`, `+m`, `+s`, `+t`, `+n`
|
||||||
- [ ] **User channel modes** — `+o` (operator), `+v` (voice)
|
- [ ] **User channel modes** — `+o` (operator), `+v` (voice)
|
||||||
- [ ] **MODE command** — set/query channel and user modes
|
- [x] **MODE command** — query channel and user modes (set not yet implemented)
|
||||||
|
- [x] **NAMES command** — query channel member list
|
||||||
|
- [x] **LIST command** — list all channels with member counts
|
||||||
|
- [x] **WHOIS command** — query user information and channel membership
|
||||||
|
- [x] **WHO command** — query channel user list
|
||||||
|
- [x] **LUSERS command** — query server statistics
|
||||||
|
- [x] **Connection registration numerics** — 001-005 sent on session creation
|
||||||
|
- [x] **LUSERS numerics** — 251/252/254/255 sent on connect and via /LUSERS
|
||||||
- [ ] **KICK command** — remove users from channels
|
- [ ] **KICK command** — remove users from channels
|
||||||
- [ ] **Numeric replies** — send IRC numeric codes via the message queue
|
- [x] **Numeric replies** — send IRC numeric codes via the message queue
|
||||||
(001 welcome, 353 NAMES, 332 TOPIC, etc.)
|
(001-005 welcome, 251-255 LUSERS, 311-319 WHOIS, 322-329 LIST/MODE,
|
||||||
|
331-332 TOPIC, 352-353 WHO/NAMES, 366, 372-376 MOTD, 401-461 errors)
|
||||||
- [ ] **Max message size enforcement** — reject oversized messages
|
- [ ] **Max message size enforcement** — reject oversized messages
|
||||||
- [ ] **NOTICE command** — distinct from PRIVMSG (no auto-reply flag)
|
- [ ] **NOTICE command** — distinct from PRIVMSG (no auto-reply flag)
|
||||||
- [ ] **Multi-client sessions** — add client to existing session
|
- [ ] **Multi-client sessions** — add client to existing session
|
||||||
@@ -2127,7 +2255,7 @@ GET /api/v1/challenge
|
|||||||
- [ ] **Push notifications** — optional webhook/push for mobile clients
|
- [ ] **Push notifications** — optional webhook/push for mobile clients
|
||||||
when messages arrive during disconnect
|
when messages arrive during disconnect
|
||||||
- [ ] **Message search** — full-text search over channel history
|
- [ ] **Message search** — full-text search over channel history
|
||||||
- [ ] **User info command** — WHOIS-equivalent for querying user metadata
|
- [x] **User info command** — WHOIS for querying user info and channels
|
||||||
- [ ] **Connection flood protection** — per-IP connection limits as a
|
- [ ] **Connection flood protection** — per-IP connection limits as a
|
||||||
complement to hashcash
|
complement to hashcash
|
||||||
- [ ] **Invite system** — `INVITE` command for `+i` channels
|
- [ ] **Invite system** — `INVITE` command for `+i` channels
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/neoirc/internal/irc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -168,7 +170,7 @@ func (client *Client) PollMessages(
|
|||||||
func (client *Client) JoinChannel(channel string) error {
|
func (client *Client) JoinChannel(channel string) error {
|
||||||
return client.SendMessage(
|
return client.SendMessage(
|
||||||
&Message{ //nolint:exhaustruct // only command+to needed
|
&Message{ //nolint:exhaustruct // only command+to needed
|
||||||
Command: "JOIN", To: channel,
|
Command: irc.CmdJoin, To: channel,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -177,7 +179,7 @@ func (client *Client) JoinChannel(channel string) error {
|
|||||||
func (client *Client) PartChannel(channel string) error {
|
func (client *Client) PartChannel(channel string) error {
|
||||||
return client.SendMessage(
|
return client.SendMessage(
|
||||||
&Message{ //nolint:exhaustruct // only command+to needed
|
&Message{ //nolint:exhaustruct // only command+to needed
|
||||||
Command: "PART", To: channel,
|
Command: irc.CmdPart, To: channel,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
api "git.eeqj.de/sneak/neoirc/cmd/neoirc-cli/api"
|
api "git.eeqj.de/sneak/neoirc/cmd/neoirc-cli/api"
|
||||||
|
"git.eeqj.de/sneak/neoirc/internal/irc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -86,7 +87,7 @@ func (a *App) handleInput(text string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||||
Command: "PRIVMSG",
|
Command: irc.CmdPrivmsg,
|
||||||
To: target,
|
To: target,
|
||||||
Body: []string{text},
|
Body: []string{text},
|
||||||
})
|
})
|
||||||
@@ -241,7 +242,7 @@ func (a *App) cmdNick(nick string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||||
Command: "NICK",
|
Command: irc.CmdNick,
|
||||||
Body: []string{nick},
|
Body: []string{nick},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -376,7 +377,7 @@ func (a *App) cmdMsg(args string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||||
Command: "PRIVMSG",
|
Command: irc.CmdPrivmsg,
|
||||||
To: target,
|
To: target,
|
||||||
Body: []string{text},
|
Body: []string{text},
|
||||||
})
|
})
|
||||||
@@ -434,7 +435,7 @@ func (a *App) cmdTopic(args string) {
|
|||||||
|
|
||||||
if args == "" {
|
if args == "" {
|
||||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||||
Command: "TOPIC",
|
Command: irc.CmdTopic,
|
||||||
To: target,
|
To: target,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -447,7 +448,7 @@ func (a *App) cmdTopic(args string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||||
Command: "TOPIC",
|
Command: irc.CmdTopic,
|
||||||
To: target,
|
To: target,
|
||||||
Body: []string{args},
|
Body: []string{args},
|
||||||
})
|
})
|
||||||
@@ -535,7 +536,7 @@ func (a *App) cmdMotd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := a.client.SendMessage(
|
err := a.client.SendMessage(
|
||||||
&api.Message{Command: "MOTD"}, //nolint:exhaustruct
|
&api.Message{Command: irc.CmdMotd}, //nolint:exhaustruct
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.ui.AddStatus(fmt.Sprintf(
|
a.ui.AddStatus(fmt.Sprintf(
|
||||||
@@ -572,7 +573,7 @@ func (a *App) cmdWho(args string) {
|
|||||||
|
|
||||||
err := a.client.SendMessage(
|
err := a.client.SendMessage(
|
||||||
&api.Message{ //nolint:exhaustruct
|
&api.Message{ //nolint:exhaustruct
|
||||||
Command: "WHO", To: channel,
|
Command: irc.CmdWho, To: channel,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -603,7 +604,7 @@ func (a *App) cmdWhois(args string) {
|
|||||||
|
|
||||||
err := a.client.SendMessage(
|
err := a.client.SendMessage(
|
||||||
&api.Message{ //nolint:exhaustruct
|
&api.Message{ //nolint:exhaustruct
|
||||||
Command: "WHOIS", To: args,
|
Command: irc.CmdWhois, To: args,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -653,7 +654,7 @@ func (a *App) cmdQuit() {
|
|||||||
|
|
||||||
if a.connected && a.client != nil {
|
if a.connected && a.client != nil {
|
||||||
_ = a.client.SendMessage(
|
_ = a.client.SendMessage(
|
||||||
&api.Message{Command: "QUIT"}, //nolint:exhaustruct
|
&api.Message{Command: irc.CmdQuit}, //nolint:exhaustruct
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,19 +739,19 @@ func (a *App) handleServerMessage(msg *api.Message) {
|
|||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
|
|
||||||
switch msg.Command {
|
switch msg.Command {
|
||||||
case "PRIVMSG":
|
case irc.CmdPrivmsg:
|
||||||
a.handlePrivmsgEvent(msg, timestamp, myNick)
|
a.handlePrivmsgEvent(msg, timestamp, myNick)
|
||||||
case "JOIN":
|
case irc.CmdJoin:
|
||||||
a.handleJoinEvent(msg, timestamp)
|
a.handleJoinEvent(msg, timestamp)
|
||||||
case "PART":
|
case irc.CmdPart:
|
||||||
a.handlePartEvent(msg, timestamp)
|
a.handlePartEvent(msg, timestamp)
|
||||||
case "QUIT":
|
case irc.CmdQuit:
|
||||||
a.handleQuitEvent(msg, timestamp)
|
a.handleQuitEvent(msg, timestamp)
|
||||||
case "NICK":
|
case irc.CmdNick:
|
||||||
a.handleNickEvent(msg, timestamp, myNick)
|
a.handleNickEvent(msg, timestamp, myNick)
|
||||||
case "NOTICE":
|
case irc.CmdNotice:
|
||||||
a.handleNoticeEvent(msg, timestamp)
|
a.handleNoticeEvent(msg, timestamp)
|
||||||
case "TOPIC":
|
case irc.CmdTopic:
|
||||||
a.handleTopicEvent(msg, timestamp)
|
a.handleTopicEvent(msg, timestamp)
|
||||||
default:
|
default:
|
||||||
a.handleDefaultEvent(msg, timestamp)
|
a.handleDefaultEvent(msg, timestamp)
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/neoirc/internal/irc"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ func generateToken() (string, error) {
|
|||||||
type IRCMessage struct {
|
type IRCMessage struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
|
Code int `json:"code,omitempty"`
|
||||||
From string `json:"from,omitempty"`
|
From string `json:"from,omitempty"`
|
||||||
To string `json:"to,omitempty"`
|
To string `json:"to,omitempty"`
|
||||||
Params json.RawMessage `json:"params,omitempty"`
|
Params json.RawMessage `json:"params,omitempty"`
|
||||||
@@ -42,6 +45,15 @@ type IRCMessage struct {
|
|||||||
DBID int64 `json:"-"`
|
DBID int64 `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNumericCode returns true if s is exactly a 3-digit
|
||||||
|
// IRC numeric reply code.
|
||||||
|
func isNumericCode(s string) bool {
|
||||||
|
return len(s) == 3 &&
|
||||||
|
s[0] >= '0' && s[0] <= '9' &&
|
||||||
|
s[1] >= '0' && s[1] <= '9' &&
|
||||||
|
s[2] >= '0' && s[2] <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
// ChannelInfo is a lightweight channel representation.
|
// ChannelInfo is a lightweight channel representation.
|
||||||
type ChannelInfo struct {
|
type ChannelInfo struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
@@ -717,6 +729,15 @@ func scanMessages(
|
|||||||
msg.DBID = qID
|
msg.DBID = qID
|
||||||
lastQID = qID
|
lastQID = qID
|
||||||
|
|
||||||
|
if isNumericCode(msg.Command) {
|
||||||
|
code, _ := strconv.Atoi(msg.Command)
|
||||||
|
msg.Code = code
|
||||||
|
|
||||||
|
if name := irc.Name(code); name != "" {
|
||||||
|
msg.Command = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
msgs = append(msgs, msg)
|
msgs = append(msgs, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,3 +974,125 @@ func (database *Database) GetSessionChannels(
|
|||||||
|
|
||||||
return scanChannels(rows)
|
return scanChannels(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetChannelCount returns the total number of channels.
|
||||||
|
func (database *Database) GetChannelCount(
|
||||||
|
ctx context.Context,
|
||||||
|
) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err := database.conn.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
"SELECT COUNT(*) FROM channels",
|
||||||
|
).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"get channel count: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelInfoFull contains extended channel information.
|
||||||
|
type ChannelInfoFull struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
MemberCount int64 `json:"memberCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllChannelsWithCounts returns every channel
|
||||||
|
// with its member count.
|
||||||
|
func (database *Database) ListAllChannelsWithCounts(
|
||||||
|
ctx context.Context,
|
||||||
|
) ([]ChannelInfoFull, error) {
|
||||||
|
rows, err := database.conn.QueryContext(ctx,
|
||||||
|
`SELECT c.id, c.name, c.topic,
|
||||||
|
COUNT(cm.session_id) AS member_count
|
||||||
|
FROM channels c
|
||||||
|
LEFT JOIN channel_members cm
|
||||||
|
ON cm.channel_id = c.id
|
||||||
|
GROUP BY c.id
|
||||||
|
ORDER BY c.name`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"list channels with counts: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var out []ChannelInfoFull
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var chanInfo ChannelInfoFull
|
||||||
|
|
||||||
|
err = rows.Scan(
|
||||||
|
&chanInfo.ID, &chanInfo.Name,
|
||||||
|
&chanInfo.Topic, &chanInfo.MemberCount,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"scan channel full: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, chanInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out == nil {
|
||||||
|
out = []ChannelInfoFull{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelCreatedAt returns the creation time of a
|
||||||
|
// channel.
|
||||||
|
func (database *Database) GetChannelCreatedAt(
|
||||||
|
ctx context.Context,
|
||||||
|
channelID int64,
|
||||||
|
) (time.Time, error) {
|
||||||
|
var createdAt time.Time
|
||||||
|
|
||||||
|
err := database.conn.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
"SELECT created_at FROM channels WHERE id = ?",
|
||||||
|
channelID,
|
||||||
|
).Scan(&createdAt)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf(
|
||||||
|
"get channel created_at: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionCreatedAt returns the creation time of a
|
||||||
|
// session.
|
||||||
|
func (database *Database) GetSessionCreatedAt(
|
||||||
|
ctx context.Context,
|
||||||
|
sessionID int64,
|
||||||
|
) (time.Time, error) {
|
||||||
|
var createdAt time.Time
|
||||||
|
|
||||||
|
err := database.conn.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
"SELECT created_at FROM sessions WHERE id = ?",
|
||||||
|
sessionID,
|
||||||
|
).Scan(&createdAt)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf(
|
||||||
|
"get session created_at: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdAt, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
package globals
|
package globals
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,16 +17,18 @@ var (
|
|||||||
|
|
||||||
// Globals holds application-wide metadata.
|
// Globals holds application-wide metadata.
|
||||||
type Globals struct {
|
type Globals struct {
|
||||||
Appname string
|
Appname string
|
||||||
Version string
|
Version string
|
||||||
|
StartTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Globals instance from the global state.
|
// New creates a new Globals instance from the global state.
|
||||||
func New(_ fx.Lifecycle) (*Globals, error) {
|
func New(_ fx.Lifecycle) (*Globals, error) {
|
||||||
n := &Globals{
|
result := &Globals{
|
||||||
Appname: Appname,
|
Appname: Appname,
|
||||||
Version: Version,
|
Version: Version,
|
||||||
|
StartTime: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -115,8 +116,9 @@ func newTestServer(
|
|||||||
|
|
||||||
func newTestGlobals() *globals.Globals {
|
func newTestGlobals() *globals.Globals {
|
||||||
return &globals.Globals{
|
return &globals.Globals{
|
||||||
Appname: "neoirc-test",
|
Appname: "neoirc-test",
|
||||||
Version: "test",
|
Version: "test",
|
||||||
|
StartTime: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,8 +468,11 @@ func findNumeric(
|
|||||||
msgs []map[string]any,
|
msgs []map[string]any,
|
||||||
numeric string,
|
numeric string,
|
||||||
) bool {
|
) bool {
|
||||||
|
want, _ := strconv.Atoi(numeric)
|
||||||
|
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
if msg[commandKey] == numeric {
|
code, ok := msg["code"].(float64)
|
||||||
|
if ok && int(code) == want {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
internal/irc/commands.go
Normal file
21
internal/irc/commands.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
// IRC command names (RFC 1459 / RFC 2812).
|
||||||
|
const (
|
||||||
|
CmdJoin = "JOIN"
|
||||||
|
CmdList = "LIST"
|
||||||
|
CmdLusers = "LUSERS"
|
||||||
|
CmdMode = "MODE"
|
||||||
|
CmdMotd = "MOTD"
|
||||||
|
CmdNames = "NAMES"
|
||||||
|
CmdNick = "NICK"
|
||||||
|
CmdNotice = "NOTICE"
|
||||||
|
CmdPart = "PART"
|
||||||
|
CmdPing = "PING"
|
||||||
|
CmdPong = "PONG"
|
||||||
|
CmdPrivmsg = "PRIVMSG"
|
||||||
|
CmdQuit = "QUIT"
|
||||||
|
CmdTopic = "TOPIC"
|
||||||
|
CmdWho = "WHO"
|
||||||
|
CmdWhois = "WHOIS"
|
||||||
|
)
|
||||||
150
internal/irc/numerics.go
Normal file
150
internal/irc/numerics.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// Package irc provides constants and utilities for the
|
||||||
|
// IRC protocol, including numeric reply codes from
|
||||||
|
// RFC 1459 and RFC 2812, and standard command names.
|
||||||
|
package irc
|
||||||
|
|
||||||
|
// Connection registration replies (001-005).
|
||||||
|
const (
|
||||||
|
RplWelcome = 1
|
||||||
|
RplYourHost = 2
|
||||||
|
RplCreated = 3
|
||||||
|
RplMyInfo = 4
|
||||||
|
RplIsupport = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command responses (200-399).
|
||||||
|
const (
|
||||||
|
RplUmodeIs = 221
|
||||||
|
RplLuserClient = 251
|
||||||
|
RplLuserOp = 252
|
||||||
|
RplLuserUnknown = 253
|
||||||
|
RplLuserChannels = 254
|
||||||
|
RplLuserMe = 255
|
||||||
|
RplAway = 301
|
||||||
|
RplUserHost = 302
|
||||||
|
RplIson = 303
|
||||||
|
RplUnaway = 305
|
||||||
|
RplNowAway = 306
|
||||||
|
RplWhoisUser = 311
|
||||||
|
RplWhoisServer = 312
|
||||||
|
RplWhoisOperator = 313
|
||||||
|
RplEndOfWho = 315
|
||||||
|
RplWhoisIdle = 317
|
||||||
|
RplEndOfWhois = 318
|
||||||
|
RplWhoisChannels = 319
|
||||||
|
RplList = 322
|
||||||
|
RplListEnd = 323
|
||||||
|
RplChannelModeIs = 324
|
||||||
|
RplCreationTime = 329
|
||||||
|
RplNoTopic = 331
|
||||||
|
RplTopic = 332
|
||||||
|
RplTopicWhoTime = 333
|
||||||
|
RplInviting = 341
|
||||||
|
RplWhoReply = 352
|
||||||
|
RplNamReply = 353
|
||||||
|
RplEndOfNames = 366
|
||||||
|
RplBanList = 367
|
||||||
|
RplEndOfBanList = 368
|
||||||
|
RplMotd = 372
|
||||||
|
RplMotdStart = 375
|
||||||
|
RplEndOfMotd = 376
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error replies (400-599).
|
||||||
|
const (
|
||||||
|
ErrNoSuchNick = 401
|
||||||
|
ErrNoSuchServer = 402
|
||||||
|
ErrNoSuchChannel = 403
|
||||||
|
ErrCannotSendToChan = 404
|
||||||
|
ErrTooManyChannels = 405
|
||||||
|
ErrNoRecipient = 411
|
||||||
|
ErrNoTextToSend = 412
|
||||||
|
ErrUnknownCommand = 421
|
||||||
|
ErrNoNicknameGiven = 431
|
||||||
|
ErrErroneusNickname = 432
|
||||||
|
ErrNicknameInUse = 433
|
||||||
|
ErrUserNotInChannel = 441
|
||||||
|
ErrNotOnChannel = 442
|
||||||
|
ErrNotRegistered = 451
|
||||||
|
ErrNeedMoreParams = 461
|
||||||
|
ErrAlreadyRegistered = 462
|
||||||
|
ErrChannelIsFull = 471
|
||||||
|
ErrInviteOnlyChan = 473
|
||||||
|
ErrBannedFromChan = 474
|
||||||
|
ErrBadChannelKey = 475
|
||||||
|
ErrChanOpPrivsNeeded = 482
|
||||||
|
)
|
||||||
|
|
||||||
|
// names maps numeric codes to their standard IRC names.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals
|
||||||
|
var names = map[int]string{
|
||||||
|
RplWelcome: "RPL_WELCOME",
|
||||||
|
RplYourHost: "RPL_YOURHOST",
|
||||||
|
RplCreated: "RPL_CREATED",
|
||||||
|
RplMyInfo: "RPL_MYINFO",
|
||||||
|
RplIsupport: "RPL_ISUPPORT",
|
||||||
|
RplUmodeIs: "RPL_UMODEIS",
|
||||||
|
RplLuserClient: "RPL_LUSERCLIENT",
|
||||||
|
RplLuserOp: "RPL_LUSEROP",
|
||||||
|
RplLuserUnknown: "RPL_LUSERUNKNOWN",
|
||||||
|
RplLuserChannels: "RPL_LUSERCHANNELS",
|
||||||
|
RplLuserMe: "RPL_LUSERME",
|
||||||
|
RplAway: "RPL_AWAY",
|
||||||
|
RplUserHost: "RPL_USERHOST",
|
||||||
|
RplIson: "RPL_ISON",
|
||||||
|
RplUnaway: "RPL_UNAWAY",
|
||||||
|
RplNowAway: "RPL_NOWAWAY",
|
||||||
|
RplWhoisUser: "RPL_WHOISUSER",
|
||||||
|
RplWhoisServer: "RPL_WHOISSERVER",
|
||||||
|
RplWhoisOperator: "RPL_WHOISOPERATOR",
|
||||||
|
RplEndOfWho: "RPL_ENDOFWHO",
|
||||||
|
RplWhoisIdle: "RPL_WHOISIDLE",
|
||||||
|
RplEndOfWhois: "RPL_ENDOFWHOIS",
|
||||||
|
RplWhoisChannels: "RPL_WHOISCHANNELS",
|
||||||
|
RplList: "RPL_LIST",
|
||||||
|
RplListEnd: "RPL_LISTEND", //nolint:misspell
|
||||||
|
RplChannelModeIs: "RPL_CHANNELMODEIS",
|
||||||
|
RplCreationTime: "RPL_CREATIONTIME",
|
||||||
|
RplNoTopic: "RPL_NOTOPIC",
|
||||||
|
RplTopic: "RPL_TOPIC",
|
||||||
|
RplTopicWhoTime: "RPL_TOPICWHOTIME",
|
||||||
|
RplInviting: "RPL_INVITING",
|
||||||
|
RplWhoReply: "RPL_WHOREPLY",
|
||||||
|
RplNamReply: "RPL_NAMREPLY",
|
||||||
|
RplEndOfNames: "RPL_ENDOFNAMES",
|
||||||
|
RplBanList: "RPL_BANLIST",
|
||||||
|
RplEndOfBanList: "RPL_ENDOFBANLIST",
|
||||||
|
RplMotd: "RPL_MOTD",
|
||||||
|
RplMotdStart: "RPL_MOTDSTART",
|
||||||
|
RplEndOfMotd: "RPL_ENDOFMOTD",
|
||||||
|
|
||||||
|
ErrNoSuchNick: "ERR_NOSUCHNICK",
|
||||||
|
ErrNoSuchServer: "ERR_NOSUCHSERVER",
|
||||||
|
ErrNoSuchChannel: "ERR_NOSUCHCHANNEL",
|
||||||
|
ErrCannotSendToChan: "ERR_CANNOTSENDTOCHAN",
|
||||||
|
ErrTooManyChannels: "ERR_TOOMANYCHANNELS",
|
||||||
|
ErrNoRecipient: "ERR_NORECIPIENT",
|
||||||
|
ErrNoTextToSend: "ERR_NOTEXTTOSEND",
|
||||||
|
ErrUnknownCommand: "ERR_UNKNOWNCOMMAND",
|
||||||
|
ErrNoNicknameGiven: "ERR_NONICKNAMEGIVEN",
|
||||||
|
ErrErroneusNickname: "ERR_ERRONEUSNICKNAME",
|
||||||
|
ErrNicknameInUse: "ERR_NICKNAMEINUSE",
|
||||||
|
ErrUserNotInChannel: "ERR_USERNOTINCHANNEL",
|
||||||
|
ErrNotOnChannel: "ERR_NOTONCHANNEL",
|
||||||
|
ErrNotRegistered: "ERR_NOTREGISTERED",
|
||||||
|
ErrNeedMoreParams: "ERR_NEEDMOREPARAMS",
|
||||||
|
ErrAlreadyRegistered: "ERR_ALREADYREGISTERED",
|
||||||
|
ErrChannelIsFull: "ERR_CHANNELISFULL",
|
||||||
|
ErrInviteOnlyChan: "ERR_INVITEONLYCHAN",
|
||||||
|
ErrBannedFromChan: "ERR_BANNEDFROMCHAN",
|
||||||
|
ErrBadChannelKey: "ERR_BADCHANNELKEY",
|
||||||
|
ErrChanOpPrivsNeeded: "ERR_CHANOPRIVSNEEDED",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the standard IRC name for a numeric code
|
||||||
|
// (e.g., Name(2) returns "RPL_YOURHOST"). Returns an
|
||||||
|
// empty string if the code is unknown.
|
||||||
|
func Name(code int) string {
|
||||||
|
return names[code]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user