Compare commits
2 Commits
feat/chi-v
...
a193831ed4
| Author | SHA1 | Date | |
|---|---|---|---|
| a193831ed4 | |||
|
|
8d26df60db |
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
|
||||
|
||||
#### 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:**
|
||||
```json
|
||||
{"command": "MODE", "to": "#general", "params": ["+m"]}
|
||||
{"command": "MODE", "to": "#general", "params": ["+o", "alice"]}
|
||||
{"command": "MODE", "to": "#general"}
|
||||
{"command": "MODE", "to": "alice"}
|
||||
```
|
||||
|
||||
**Status:** Not yet implemented. See [Channel Modes](#channel-modes) for the
|
||||
planned mode set.
|
||||
**S2C (via message queue):**
|
||||
|
||||
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
|
||||
|
||||
#### 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)
|
||||
|
||||
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 |
|
||||
|------|----------------------|-----------|---------|
|
||||
| `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"]}` |
|
||||
| `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"]}` |
|
||||
| `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!"]}` |
|
||||
| `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"]}` |
|
||||
| `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"]}` |
|
||||
@@ -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"]}` |
|
||||
| `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"]}` |
|
||||
| `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"]}` |
|
||||
| `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"]}` |
|
||||
|
||||
**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 |
|
||||
| `NICK` | `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 |
|
||||
| `PING` | | | 200 OK |
|
||||
|
||||
@@ -1095,10 +1196,29 @@ auth tokens (401), and server errors (500).
|
||||
| Numeric | Name | When |
|
||||
|---------|------|------|
|
||||
| 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) |
|
||||
| 332 | RPL_TOPIC | Channel topic (on JOIN, TOPIC set) |
|
||||
| 353 | RPL_NAMREPLY | Channel member list (on JOIN) |
|
||||
| 366 | RPL_ENDOFNAMES | End of NAMES list (on JOIN) |
|
||||
| 352 | RPL_WHOREPLY | User in WHO response |
|
||||
| 353 | RPL_NAMREPLY | Channel member list (on JOIN, NAMES) |
|
||||
| 366 | RPL_ENDOFNAMES | End of NAMES list |
|
||||
| 375 | RPL_MOTDSTART | Start of MOTD |
|
||||
| 372 | RPL_MOTD | MOTD line |
|
||||
| 376 | RPL_ENDOFMOTD | End of MOTD |
|
||||
@@ -2104,10 +2224,18 @@ GET /api/v1/challenge
|
||||
- [ ] **Message rotation** — enforce `MAX_HISTORY` per channel
|
||||
- [ ] **Channel modes** — enforce `+i`, `+m`, `+s`, `+t`, `+n`
|
||||
- [ ] **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
|
||||
- [ ] **Numeric replies** — send IRC numeric codes via the message queue
|
||||
(001 welcome, 353 NAMES, 332 TOPIC, etc.)
|
||||
- [x] **Numeric replies** — send IRC numeric codes via the message queue
|
||||
(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
|
||||
- [ ] **NOTICE command** — distinct from PRIVMSG (no auto-reply flag)
|
||||
- [ ] **Multi-client sessions** — add client to existing session
|
||||
@@ -2127,7 +2255,7 @@ GET /api/v1/challenge
|
||||
- [ ] **Push notifications** — optional webhook/push for mobile clients
|
||||
when messages arrive during disconnect
|
||||
- [ ] **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
|
||||
complement to hashcash
|
||||
- [ ] **Invite system** — `INVITE` command for `+i` channels
|
||||
|
||||
@@ -953,3 +953,125 @@ func (database *Database) GetSessionChannels(
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
@@ -15,16 +17,18 @@ var (
|
||||
|
||||
// Globals holds application-wide metadata.
|
||||
type Globals struct {
|
||||
Appname string
|
||||
Version string
|
||||
Appname string
|
||||
Version string
|
||||
StartTime time.Time
|
||||
}
|
||||
|
||||
// New creates a new Globals instance from the global state.
|
||||
func New(_ fx.Lifecycle) (*Globals, error) {
|
||||
n := &Globals{
|
||||
Appname: Appname,
|
||||
Version: Version,
|
||||
result := &Globals{
|
||||
Appname: Appname,
|
||||
Version: Version,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
return n, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -27,7 +27,15 @@ const (
|
||||
defaultMaxBodySize = 4096
|
||||
defaultHistLimit = 50
|
||||
maxHistLimit = 500
|
||||
cmdJoin = "JOIN"
|
||||
cmdList = "LIST"
|
||||
cmdNick = "NICK"
|
||||
cmdPart = "PART"
|
||||
cmdPrivmsg = "PRIVMSG"
|
||||
cmdQuit = "QUIT"
|
||||
cmdTopic = "TOPIC"
|
||||
cmdWho = "WHO"
|
||||
cmdWhois = "WHOIS"
|
||||
)
|
||||
|
||||
func (hdlr *Handlers) maxBodySize() int64 {
|
||||
@@ -219,19 +227,130 @@ func (hdlr *Handlers) handleCreateSessionError(
|
||||
)
|
||||
}
|
||||
|
||||
// deliverWelcome sends the RPL_WELCOME (001) numeric to a
|
||||
// new client.
|
||||
// deliverWelcome sends connection registration numerics
|
||||
// (001-005) to a new client.
|
||||
func (hdlr *Handlers) deliverWelcome(
|
||||
request *http.Request,
|
||||
clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
version := hdlr.serverVersion()
|
||||
|
||||
// 001 RPL_WELCOME
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "001", nick, nil,
|
||||
"Welcome to the network, "+nick,
|
||||
)
|
||||
|
||||
// 002 RPL_YOURHOST
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "002", nick, nil,
|
||||
"Your host is "+srvName+
|
||||
", running version "+version,
|
||||
)
|
||||
|
||||
// 003 RPL_CREATED
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "003", nick, nil,
|
||||
"This server was created "+
|
||||
hdlr.params.Globals.StartTime.
|
||||
Format("2006-01-02"),
|
||||
)
|
||||
|
||||
// 004 RPL_MYINFO
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "004", nick,
|
||||
[]string{srvName, version, "", "imnst"},
|
||||
"",
|
||||
)
|
||||
|
||||
// 005 RPL_ISUPPORT
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "005", nick,
|
||||
[]string{
|
||||
"CHANTYPES=#",
|
||||
"NICKLEN=32",
|
||||
"CHANMODES=,,," + "imnst",
|
||||
"NETWORK=neoirc",
|
||||
"CASEMAPPING=ascii",
|
||||
},
|
||||
"are supported by this server",
|
||||
)
|
||||
|
||||
// LUSERS
|
||||
hdlr.deliverLusers(ctx, clientID, nick)
|
||||
}
|
||||
|
||||
// deliverLusers sends RPL_LUSERCLIENT (251),
|
||||
// RPL_LUSEROP (252), RPL_LUSERCHANNELS (254), and
|
||||
// RPL_LUSERME (255) to the client.
|
||||
func (hdlr *Handlers) deliverLusers(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
userCount, err := hdlr.params.Database.GetUserCount(ctx)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"lusers user count", "error", err,
|
||||
)
|
||||
|
||||
userCount = 0
|
||||
}
|
||||
|
||||
chanCount, err := hdlr.params.Database.GetChannelCount(
|
||||
ctx,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"lusers channel count", "error", err,
|
||||
)
|
||||
|
||||
chanCount = 0
|
||||
}
|
||||
|
||||
// 251 RPL_LUSERCLIENT
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "251", nick, nil,
|
||||
fmt.Sprintf(
|
||||
"There are %d users and 0 invisible on 1 servers",
|
||||
userCount,
|
||||
),
|
||||
)
|
||||
|
||||
// 252 RPL_LUSEROP
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "252", nick,
|
||||
[]string{"0"},
|
||||
"operator(s) online",
|
||||
)
|
||||
|
||||
// 254 RPL_LUSERCHANNELS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "254", nick,
|
||||
[]string{strconv.FormatInt(chanCount, 10)},
|
||||
"channels formed",
|
||||
)
|
||||
|
||||
// 255 RPL_LUSERME
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "255", nick, nil,
|
||||
fmt.Sprintf(
|
||||
"I have %d clients and 1 servers",
|
||||
userCount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) serverVersion() string {
|
||||
ver := hdlr.params.Globals.Version
|
||||
if ver == "" {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
return ver
|
||||
}
|
||||
|
||||
// deliverMOTD sends the MOTD as IRC numeric messages to a
|
||||
@@ -645,37 +764,86 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
sessionID, clientID, nick,
|
||||
command, target, body, bodyLines,
|
||||
)
|
||||
case "JOIN":
|
||||
case cmdJoin:
|
||||
hdlr.handleJoin(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target,
|
||||
)
|
||||
case "PART":
|
||||
case cmdPart:
|
||||
hdlr.handlePart(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target, body,
|
||||
)
|
||||
case "NICK":
|
||||
case cmdNick:
|
||||
hdlr.handleNick(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case "TOPIC":
|
||||
case cmdTopic:
|
||||
hdlr.handleTopic(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
target, body, bodyLines,
|
||||
)
|
||||
case "QUIT":
|
||||
case cmdQuit:
|
||||
hdlr.handleQuit(
|
||||
writer, request, sessionID, nick, body,
|
||||
)
|
||||
case "MOTD", "LIST", "WHO", "WHOIS", "PING":
|
||||
case "MOTD", cmdList, cmdWho, cmdWhois, "PING":
|
||||
hdlr.dispatchInfoCommand(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
command, target, bodyLines,
|
||||
)
|
||||
default:
|
||||
hdlr.dispatchQueryCommand(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
command, target, bodyLines,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) dispatchQueryCommand(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, command, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
switch command {
|
||||
case "MODE":
|
||||
hdlr.handleMode(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
target, bodyLines,
|
||||
)
|
||||
case "NAMES":
|
||||
hdlr.handleNames(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target,
|
||||
)
|
||||
case cmdList:
|
||||
hdlr.handleList(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
)
|
||||
case cmdWhois:
|
||||
hdlr.handleWhois(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
target, bodyLines,
|
||||
)
|
||||
case cmdWho:
|
||||
hdlr.handleWho(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target,
|
||||
)
|
||||
case "LUSERS":
|
||||
hdlr.handleLusers(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
)
|
||||
default:
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
@@ -913,7 +1081,7 @@ func (hdlr *Handlers) handleJoin(
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"JOIN"},
|
||||
"461", nick, []string{cmdJoin},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
@@ -984,7 +1152,7 @@ func (hdlr *Handlers) executeJoin(
|
||||
)
|
||||
|
||||
_ = hdlr.fanOutSilent(
|
||||
request, "JOIN", nick, channel, nil, memberIDs,
|
||||
request, cmdJoin, nick, channel, nil, memberIDs,
|
||||
)
|
||||
|
||||
hdlr.deliverJoinNumerics(
|
||||
@@ -1082,7 +1250,7 @@ func (hdlr *Handlers) handlePart(
|
||||
if target == "" {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"461", nick, []string{"PART"},
|
||||
"461", nick, []string{cmdPart},
|
||||
"Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
@@ -1120,7 +1288,7 @@ func (hdlr *Handlers) handlePart(
|
||||
)
|
||||
|
||||
_ = hdlr.fanOutSilent(
|
||||
request, "PART", nick, channel, body, memberIDs,
|
||||
request, cmdPart, nick, channel, body, memberIDs,
|
||||
)
|
||||
|
||||
err = hdlr.params.Database.PartChannel(
|
||||
@@ -1162,7 +1330,7 @@ func (hdlr *Handlers) handleNick(
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"NICK"},
|
||||
"461", nick, []string{cmdNick},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
@@ -1260,7 +1428,7 @@ func (hdlr *Handlers) broadcastNick(
|
||||
}
|
||||
|
||||
dbID, _, _ := hdlr.params.Database.InsertMessage(
|
||||
request.Context(), "NICK", oldNick, "",
|
||||
request.Context(), cmdNick, oldNick, "",
|
||||
nil, json.RawMessage(nickBody), nil,
|
||||
)
|
||||
|
||||
@@ -1301,7 +1469,7 @@ func (hdlr *Handlers) handleTopic(
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"TOPIC"},
|
||||
"461", nick, []string{cmdTopic},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
@@ -1312,7 +1480,7 @@ func (hdlr *Handlers) handleTopic(
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"TOPIC"},
|
||||
"461", nick, []string{cmdTopic},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
@@ -1374,7 +1542,7 @@ func (hdlr *Handlers) executeTopic(
|
||||
)
|
||||
|
||||
_ = hdlr.fanOutSilent(
|
||||
request, "TOPIC", nick, channel, body, memberIDs,
|
||||
request, cmdTopic, nick, channel, body, memberIDs,
|
||||
)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
@@ -1407,16 +1575,16 @@ func (hdlr *Handlers) dispatchInfoCommand(
|
||||
hdlr.deliverMOTD(
|
||||
request, clientID, sessionID, nick,
|
||||
)
|
||||
case "LIST":
|
||||
case cmdList:
|
||||
hdlr.handleListCmd(
|
||||
request, clientID, sessionID, nick,
|
||||
)
|
||||
case "WHO":
|
||||
case cmdWho:
|
||||
hdlr.handleWhoCmd(
|
||||
request, clientID, sessionID, nick,
|
||||
target,
|
||||
)
|
||||
case "WHOIS":
|
||||
case cmdWhois:
|
||||
hdlr.handleWhoisCmd(
|
||||
request, clientID, sessionID, nick,
|
||||
target, bodyLines,
|
||||
@@ -1497,7 +1665,7 @@ func (hdlr *Handlers) handleWhoCmd(
|
||||
if target == "" {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "461", nick,
|
||||
[]string{"WHO"}, "Not enough parameters",
|
||||
[]string{cmdWho}, "Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
@@ -1568,7 +1736,7 @@ func (hdlr *Handlers) handleWhoisCmd(
|
||||
if whoisNick == "" {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "461", nick,
|
||||
[]string{"WHOIS"}, "Not enough parameters",
|
||||
[]string{cmdWhois}, "Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
@@ -1671,7 +1839,7 @@ func (hdlr *Handlers) handleQuit(
|
||||
|
||||
if len(channels) > 0 {
|
||||
dbID, _, _ = hdlr.params.Database.InsertMessage(
|
||||
request.Context(), "QUIT", nick, "",
|
||||
request.Context(), cmdQuit, nick, "",
|
||||
nil, body, nil,
|
||||
)
|
||||
}
|
||||
@@ -1712,6 +1880,424 @@ func (hdlr *Handlers) handleQuit(
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleMode handles the MODE command for channels and
|
||||
// users. Currently supports query-only (no mode changes).
|
||||
func (hdlr *Handlers) handleMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"MODE"},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
// User mode query — return empty modes.
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"221", nick, nil, "+",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = bodyLines
|
||||
|
||||
hdlr.handleChannelMode(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, channel,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handleChannelMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, channel string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"403", nick, []string{channel},
|
||||
"No such channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 324 RPL_CHANNELMODEIS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "324", nick,
|
||||
[]string{channel, "+n"}, "",
|
||||
)
|
||||
|
||||
// 329 RPL_CREATIONTIME
|
||||
createdAt, timeErr := hdlr.params.Database.
|
||||
GetChannelCreatedAt(ctx, chID)
|
||||
if timeErr == nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "329", nick,
|
||||
[]string{
|
||||
channel,
|
||||
strconv.FormatInt(
|
||||
createdAt.Unix(), 10,
|
||||
),
|
||||
},
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleNames sends NAMES reply for a channel.
|
||||
func (hdlr *Handlers) handleNames(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"NAMES"},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
channel = "#" + channel
|
||||
}
|
||||
|
||||
ctx := request.Context()
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"403", nick, []string{channel},
|
||||
"No such channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
members, memErr := hdlr.params.Database.ChannelMembers(
|
||||
ctx, chID,
|
||||
)
|
||||
if memErr == nil && len(members) > 0 {
|
||||
nicks := make([]string, 0, len(members))
|
||||
|
||||
for _, mem := range members {
|
||||
nicks = append(nicks, mem.Nick)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "353", nick,
|
||||
[]string{"=", channel},
|
||||
strings.Join(nicks, " "),
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "366", nick,
|
||||
[]string{channel}, "End of /NAMES list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleList sends the LIST response with 322/323
|
||||
// numerics.
|
||||
func (hdlr *Handlers) handleList(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
channels, err := hdlr.params.Database.
|
||||
ListAllChannelsWithCounts(ctx)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"list channels failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, chanInfo := range channels {
|
||||
// 322 RPL_LIST
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "322", nick,
|
||||
[]string{
|
||||
chanInfo.Name,
|
||||
strconv.FormatInt(
|
||||
chanInfo.MemberCount, 10,
|
||||
),
|
||||
},
|
||||
chanInfo.Topic,
|
||||
)
|
||||
}
|
||||
|
||||
// 323 — end of channel list.
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "323", nick, nil,
|
||||
"End of /LIST",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleWhois handles the WHOIS command.
|
||||
func (hdlr *Handlers) handleWhois(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
queryNick := target
|
||||
|
||||
// If target is empty, check body for the nick.
|
||||
if queryNick == "" {
|
||||
lines := bodyLines()
|
||||
if len(lines) > 0 {
|
||||
queryNick = strings.TrimSpace(lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
if queryNick == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{cmdWhois},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.executeWhois(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, queryNick,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) executeWhois(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, queryNick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
targetSID, err := hdlr.params.Database.GetSessionByNick(
|
||||
ctx, queryNick,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "401", nick,
|
||||
[]string{queryNick},
|
||||
"No such nick/channel",
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "318", nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 311 RPL_WHOISUSER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "311", nick,
|
||||
[]string{queryNick, queryNick, srvName, "*"},
|
||||
queryNick,
|
||||
)
|
||||
|
||||
// 312 RPL_WHOISSERVER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "312", nick,
|
||||
[]string{queryNick, srvName},
|
||||
"neoirc server",
|
||||
)
|
||||
|
||||
// 319 RPL_WHOISCHANNELS
|
||||
hdlr.deliverWhoisChannels(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
// 318 RPL_ENDOFWHOIS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "318", nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) deliverWhoisChannels(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick, queryNick string,
|
||||
targetSID int64,
|
||||
) {
|
||||
channels, chanErr := hdlr.params.Database.
|
||||
GetSessionChannels(ctx, targetSID)
|
||||
if chanErr != nil || len(channels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
chanNames := make([]string, 0, len(channels))
|
||||
|
||||
for _, chanInfo := range channels {
|
||||
chanNames = append(chanNames, chanInfo.Name)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "319", nick,
|
||||
[]string{queryNick},
|
||||
strings.Join(chanNames, " "),
|
||||
)
|
||||
}
|
||||
|
||||
// handleWho handles the WHO command.
|
||||
func (hdlr *Handlers) handleWho(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{cmdWho},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
channel = "#" + channel
|
||||
}
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err != nil {
|
||||
// 315 RPL_ENDOFWHO (empty result)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "315", nick,
|
||||
[]string{target},
|
||||
"End of /WHO list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
members, memErr := hdlr.params.Database.ChannelMembers(
|
||||
ctx, chID,
|
||||
)
|
||||
if memErr == nil {
|
||||
for _, mem := range members {
|
||||
// 352 RPL_WHOREPLY
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "352", nick,
|
||||
[]string{
|
||||
channel, mem.Nick, srvName,
|
||||
srvName, mem.Nick, "H",
|
||||
},
|
||||
"0 "+mem.Nick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 315 RPL_ENDOFWHO
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "315", nick,
|
||||
[]string{channel},
|
||||
"End of /WHO list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleLusers handles the LUSERS command.
|
||||
func (hdlr *Handlers) handleLusers(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
hdlr.deliverLusers(
|
||||
request.Context(), clientID, nick,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGetHistory returns message history for a target.
|
||||
func (hdlr *Handlers) HandleGetHistory() http.HandlerFunc {
|
||||
return func(
|
||||
@@ -1921,7 +2507,7 @@ func (hdlr *Handlers) cleanupUser(
|
||||
|
||||
if len(channels) > 0 {
|
||||
quitDBID, _, _ = hdlr.params.Database.InsertMessage(
|
||||
ctx, "QUIT", nick, "",
|
||||
ctx, cmdQuit, nick, "",
|
||||
nil, nil, nil,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,8 +115,9 @@ func newTestServer(
|
||||
|
||||
func newTestGlobals() *globals.Globals {
|
||||
return &globals.Globals{
|
||||
Appname: "neoirc-test",
|
||||
Version: "test",
|
||||
Appname: "neoirc-test",
|
||||
Version: "test",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user