Compare commits
10 Commits
main
...
cdb808bc4f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdb808bc4f | ||
|
|
6fece9a78c | ||
|
|
b47a05a4d7 | ||
|
|
5b4ac75b23 | ||
|
|
ba304e322e | ||
|
|
f5d1594150 | ||
|
|
09a1dddbd0 | ||
|
|
db4cd9f055 | ||
|
|
2f905bda9f | ||
|
|
f53b8f13eb |
2
go.mod
2
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8
|
||||
github.com/gdamore/tcell/v2 v2.13.8
|
||||
github.com/getsentry/sentry-go v0.42.0
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/chi v1.5.5
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -18,8 +18,8 @@ github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3Rl
|
||||
github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||
github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7RLEt8=
|
||||
github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
|
||||
@@ -1110,121 +1110,6 @@ func (database *Database) GetSessionCreatedAt(
|
||||
return createdAt, nil
|
||||
}
|
||||
|
||||
// SetAway sets the away message for a session.
|
||||
// An empty message clears the away status.
|
||||
func (database *Database) SetAway(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
message string,
|
||||
) error {
|
||||
_, err := database.conn.ExecContext(ctx,
|
||||
"UPDATE sessions SET away_message = ? WHERE id = ?",
|
||||
message, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set away: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAway returns the away message for a session.
|
||||
// Returns an empty string if the user is not away.
|
||||
func (database *Database) GetAway(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
) (string, error) {
|
||||
var msg string
|
||||
|
||||
err := database.conn.QueryRowContext(ctx,
|
||||
"SELECT away_message FROM sessions WHERE id = ?",
|
||||
sessionID,
|
||||
).Scan(&msg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get away: %w", err)
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// SetTopicMeta sets the topic along with who set it and
|
||||
// when.
|
||||
func (database *Database) SetTopicMeta(
|
||||
ctx context.Context,
|
||||
channelName, topic, setBy string,
|
||||
) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := database.conn.ExecContext(ctx,
|
||||
`UPDATE channels
|
||||
SET topic = ?, topic_set_by = ?,
|
||||
topic_set_at = ?, updated_at = ?
|
||||
WHERE name = ?`,
|
||||
topic, setBy, now, now, channelName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set topic meta: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TopicMeta holds topic metadata for a channel.
|
||||
type TopicMeta struct {
|
||||
SetBy string
|
||||
SetAt time.Time
|
||||
}
|
||||
|
||||
// GetTopicMeta returns who set the topic and when.
|
||||
func (database *Database) GetTopicMeta(
|
||||
ctx context.Context,
|
||||
channelID int64,
|
||||
) (*TopicMeta, error) {
|
||||
var (
|
||||
setBy string
|
||||
setAt sql.NullTime
|
||||
)
|
||||
|
||||
err := database.conn.QueryRowContext(ctx,
|
||||
`SELECT topic_set_by, topic_set_at
|
||||
FROM channels WHERE id = ?`,
|
||||
channelID,
|
||||
).Scan(&setBy, &setAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"get topic meta: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
if setBy == "" || !setAt.Valid {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
return &TopicMeta{
|
||||
SetBy: setBy,
|
||||
SetAt: setAt.Time,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSessionLastSeen returns the last_seen time for a
|
||||
// session.
|
||||
func (database *Database) GetSessionLastSeen(
|
||||
ctx context.Context,
|
||||
sessionID int64,
|
||||
) (time.Time, error) {
|
||||
var lastSeen time.Time
|
||||
|
||||
err := database.conn.QueryRowContext(ctx,
|
||||
"SELECT last_seen FROM sessions WHERE id = ?",
|
||||
sessionID,
|
||||
).Scan(&lastSeen)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf(
|
||||
"get session last_seen: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return lastSeen, nil
|
||||
}
|
||||
|
||||
// PruneOldQueueEntries deletes client output queue entries
|
||||
// older than cutoff and returns the number of rows removed.
|
||||
func (database *Database) PruneOldQueueEntries(
|
||||
|
||||
@@ -8,7 +8,6 @@ CREATE TABLE IF NOT EXISTS sessions (
|
||||
nick TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL DEFAULT '',
|
||||
signing_key TEXT NOT NULL DEFAULT '',
|
||||
away_message TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -31,8 +30,6 @@ CREATE TABLE IF NOT EXISTS channels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
topic TEXT NOT NULL DEFAULT '',
|
||||
topic_set_by TEXT NOT NULL DEFAULT '',
|
||||
topic_set_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
var validNickRe = regexp.MustCompile(
|
||||
@@ -71,10 +71,11 @@ func (hdlr *Handlers) requireAuth(
|
||||
sessionID, clientID, nick, err :=
|
||||
hdlr.authSession(request)
|
||||
if err != nil {
|
||||
hdlr.respondJSON(writer, request, map[string]any{
|
||||
"error": "not registered",
|
||||
"numeric": irc.ErrNotRegistered,
|
||||
}, http.StatusUnauthorized)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"unauthorized",
|
||||
http.StatusUnauthorized,
|
||||
)
|
||||
|
||||
return 0, 0, "", false
|
||||
}
|
||||
@@ -836,11 +837,6 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
switch command {
|
||||
case irc.CmdAway:
|
||||
hdlr.handleAway(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case irc.CmdPrivmsg, irc.CmdNotice:
|
||||
hdlr.handlePrivmsg(
|
||||
writer, request,
|
||||
@@ -951,8 +947,8 @@ func (hdlr *Handlers) handlePrivmsg(
|
||||
if target == "" {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
irc.ErrNoRecipient, nick, []string{command},
|
||||
"No recipient given",
|
||||
irc.ErrNeedMoreParams, nick, []string{command},
|
||||
"Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
@@ -966,8 +962,8 @@ func (hdlr *Handlers) handlePrivmsg(
|
||||
if len(lines) == 0 {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
irc.ErrNoTextToSend, nick, []string{command},
|
||||
"No text to send",
|
||||
irc.ErrNeedMoreParams, nick, []string{command},
|
||||
"Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
@@ -1054,8 +1050,8 @@ func (hdlr *Handlers) handleChannelMsg(
|
||||
if !isMember {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrCannotSendToChan, nick, []string{target},
|
||||
"Cannot send to channel",
|
||||
irc.ErrNotOnChannel, nick, []string{target},
|
||||
"You're not on that channel",
|
||||
)
|
||||
|
||||
return
|
||||
@@ -1151,19 +1147,6 @@ func (hdlr *Handlers) handleDirectMsg(
|
||||
return
|
||||
}
|
||||
|
||||
// If the target is away, send RPL_AWAY to the sender.
|
||||
awayMsg, awayErr := hdlr.params.Database.GetAway(
|
||||
request.Context(), targetSID,
|
||||
)
|
||||
if awayErr == nil && awayMsg != "" {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
irc.RplAway, nick,
|
||||
[]string{target}, awayMsg,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
}
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"id": msgUUID, "status": "sent"},
|
||||
http.StatusOK)
|
||||
@@ -1274,25 +1257,14 @@ func (hdlr *Handlers) deliverJoinNumerics(
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
hdlr.deliverTopicNumerics(
|
||||
ctx, clientID, sessionID, nick, channel, chID,
|
||||
chInfo, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err == nil {
|
||||
_ = chInfo // chInfo is the ID; topic comes from DB.
|
||||
}
|
||||
|
||||
hdlr.deliverNamesNumerics(
|
||||
ctx, clientID, nick, channel, chID,
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
}
|
||||
|
||||
// deliverTopicNumerics sends RPL_TOPIC or RPL_NOTOPIC,
|
||||
// plus RPL_TOPICWHOTIME when topic metadata is available.
|
||||
func (hdlr *Handlers) deliverTopicNumerics(
|
||||
ctx context.Context,
|
||||
clientID, sessionID int64,
|
||||
nick, channel string,
|
||||
chID int64,
|
||||
) {
|
||||
// Get topic from channel info.
|
||||
channels, listErr := hdlr.params.Database.ListChannels(
|
||||
ctx, sessionID,
|
||||
)
|
||||
@@ -1314,39 +1286,14 @@ func (hdlr *Handlers) deliverTopicNumerics(
|
||||
ctx, clientID, irc.RplTopic, nick,
|
||||
[]string{channel}, topic,
|
||||
)
|
||||
|
||||
topicMeta, tmErr := hdlr.params.Database.
|
||||
GetTopicMeta(ctx, chID)
|
||||
if tmErr == nil && topicMeta != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID,
|
||||
irc.RplTopicWhoTime, nick,
|
||||
[]string{
|
||||
channel,
|
||||
topicMeta.SetBy,
|
||||
strconv.FormatInt(
|
||||
topicMeta.SetAt.Unix(), 10,
|
||||
),
|
||||
},
|
||||
"",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplNoTopic, nick,
|
||||
[]string{channel}, "No topic is set",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// deliverNamesNumerics sends RPL_NAMREPLY and
|
||||
// RPL_ENDOFNAMES for a channel.
|
||||
func (hdlr *Handlers) deliverNamesNumerics(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick, channel string,
|
||||
chID int64,
|
||||
) {
|
||||
// Get member list for NAMES reply.
|
||||
members, memErr := hdlr.params.Database.ChannelMembers(
|
||||
ctx, chID,
|
||||
)
|
||||
@@ -1369,6 +1316,8 @@ func (hdlr *Handlers) deliverNamesNumerics(
|
||||
ctx, clientID, irc.RplEndOfNames, nick,
|
||||
[]string{channel}, "End of /NAMES list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handlePart(
|
||||
@@ -1652,8 +1601,8 @@ func (hdlr *Handlers) executeTopic(
|
||||
body json.RawMessage,
|
||||
chID int64,
|
||||
) {
|
||||
setErr := hdlr.params.Database.SetTopicMeta(
|
||||
request.Context(), channel, topic, nick,
|
||||
setErr := hdlr.params.Database.SetTopic(
|
||||
request.Context(), channel, topic,
|
||||
)
|
||||
if setErr != nil {
|
||||
hdlr.log.Error(
|
||||
@@ -1680,25 +1629,6 @@ func (hdlr *Handlers) executeTopic(
|
||||
request.Context(), clientID,
|
||||
irc.RplTopic, nick, []string{channel}, topic,
|
||||
)
|
||||
|
||||
// 333 RPL_TOPICWHOTIME
|
||||
topicMeta, tmErr := hdlr.params.Database.
|
||||
GetTopicMeta(request.Context(), chID)
|
||||
if tmErr == nil && topicMeta != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
irc.RplTopicWhoTime, nick,
|
||||
[]string{
|
||||
channel,
|
||||
topicMeta.SetBy,
|
||||
strconv.FormatInt(
|
||||
topicMeta.SetAt.Unix(), 10,
|
||||
),
|
||||
},
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
@@ -2088,11 +2018,6 @@ func (hdlr *Handlers) executeWhois(
|
||||
"neoirc server",
|
||||
)
|
||||
|
||||
// 317 RPL_WHOISIDLE
|
||||
hdlr.deliverWhoisIdle(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
// 319 RPL_WHOISCHANNELS
|
||||
hdlr.deliverWhoisChannels(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
@@ -2510,95 +2435,3 @@ func (hdlr *Handlers) HandleServerInfo() http.HandlerFunc {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAway handles the AWAY command. An empty body
|
||||
// clears the away status; a non-empty body sets it.
|
||||
func (hdlr *Handlers) handleAway(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
lines := bodyLines()
|
||||
|
||||
awayMsg := ""
|
||||
if len(lines) > 0 {
|
||||
awayMsg = strings.Join(lines, " ")
|
||||
}
|
||||
|
||||
err := hdlr.params.Database.SetAway(
|
||||
ctx, sessionID, awayMsg,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error("set away failed", "error", err)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if awayMsg == "" {
|
||||
// 305 RPL_UNAWAY
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplUnaway, nick, nil,
|
||||
"You are no longer marked as being away",
|
||||
)
|
||||
} else {
|
||||
// 306 RPL_NOWAWAY
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplNowAway, nick, nil,
|
||||
"You have been marked as being away",
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// deliverWhoisIdle sends RPL_WHOISIDLE (317) with idle
|
||||
// time and signon time.
|
||||
func (hdlr *Handlers) deliverWhoisIdle(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick, queryNick string,
|
||||
targetSID int64,
|
||||
) {
|
||||
lastSeen, lsErr := hdlr.params.Database.
|
||||
GetSessionLastSeen(ctx, targetSID)
|
||||
if lsErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
createdAt, caErr := hdlr.params.Database.
|
||||
GetSessionCreatedAt(ctx, targetSID)
|
||||
if caErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
idleSeconds := int64(time.Since(lastSeen).Seconds())
|
||||
if idleSeconds < 0 {
|
||||
idleSeconds = 0
|
||||
}
|
||||
|
||||
signonUnix := strconv.FormatInt(
|
||||
createdAt.Unix(), 10,
|
||||
)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplWhoisIdle, nick,
|
||||
[]string{
|
||||
queryNick,
|
||||
strconv.FormatInt(idleSeconds, 10),
|
||||
signonUnix,
|
||||
},
|
||||
"seconds idle, signon time",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -811,9 +811,9 @@ func TestMessageMissingBody(t *testing.T) {
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "412") {
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOTEXTTOSEND (412), got %v",
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
@@ -835,9 +835,9 @@ func TestMessageMissingTo(t *testing.T) {
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "411") {
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NORECIPIENT (411), got %v",
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
@@ -870,9 +870,9 @@ func TestNonMemberCannotSend(t *testing.T) {
|
||||
|
||||
msgs, _ := tserver.pollMessages(aliceToken, lastID)
|
||||
|
||||
if !findNumeric(msgs, "404") {
|
||||
if !findNumeric(msgs, "442") {
|
||||
t.Fatalf(
|
||||
"expected ERR_CANNOTSENDTOCHAN (404), got %v",
|
||||
"expected ERR_NOTONCHANNEL (442), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
basicauth "github.com/99designs/basicauth-go"
|
||||
chimw "github.com/go-chi/chi/v5/middleware"
|
||||
chimw "github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
metrics "github.com/slok/go-http-metrics/metrics/prometheus"
|
||||
ghmm "github.com/slok/go-http-metrics/middleware"
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"git.eeqj.de/sneak/neoirc/web"
|
||||
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload" // loads .env file
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ package irc
|
||||
|
||||
// IRC command names (RFC 1459 / RFC 2812).
|
||||
const (
|
||||
CmdAway = "AWAY"
|
||||
CmdJoin = "JOIN"
|
||||
CmdList = "LIST"
|
||||
CmdLusers = "LUSERS"
|
||||
|
||||
Reference in New Issue
Block a user