fix: rebase onto main, add IRC wire handlers and integration tests for Tier 3 commands
Some checks failed
check / check (push) Failing after 2m10s
Some checks failed
check / check (push) Failing after 2m10s
Rebase onto main to resolve conflicts from module path rename (sneak.berlin/go/neoirc) and integration test addition. - Update import paths in utility.go to new module path - Add IRC wire protocol handlers for VERSION, ADMIN, INFO, TIME, KILL, and WALLOPS to ircserver/commands.go - Register all 6 new commands in the IRC command dispatch map - Implement proper user MODE +w/-w support for WALLOPS - Add WALLOPS relay delivery in relay.go - Add integration tests for all 7 Tier 3 commands: USERHOST, VERSION, ADMIN, INFO, TIME, KILL, WALLOPS - Add newTestEnvWithOper helper for oper-dependent tests
This commit is contained in:
@@ -431,7 +431,7 @@ func (c *Conn) handleMode(
|
||||
if strings.HasPrefix(target, "#") {
|
||||
c.handleChannelMode(ctx, msg)
|
||||
} else {
|
||||
c.handleUserMode(msg)
|
||||
c.handleUserMode(ctx, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +694,10 @@ func (c *Conn) applyChannelModes(
|
||||
}
|
||||
|
||||
// handleUserMode handles MODE for users.
|
||||
func (c *Conn) handleUserMode(msg *Message) {
|
||||
func (c *Conn) handleUserMode(
|
||||
ctx context.Context,
|
||||
msg *Message,
|
||||
) {
|
||||
target := msg.Params[0]
|
||||
|
||||
if !strings.EqualFold(target, c.nick) {
|
||||
@@ -706,8 +709,85 @@ func (c *Conn) handleUserMode(msg *Message) {
|
||||
return
|
||||
}
|
||||
|
||||
// We don't support user modes beyond the basics.
|
||||
c.sendNumeric(irc.RplUmodeIs, "+")
|
||||
// Mode query (no mode string).
|
||||
if len(msg.Params) < 2 { //nolint:mnd
|
||||
c.sendNumeric(
|
||||
irc.RplUmodeIs,
|
||||
c.buildUmodeString(ctx),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
modeStr := msg.Params[1]
|
||||
|
||||
if len(modeStr) < 2 { //nolint:mnd
|
||||
c.sendNumeric(
|
||||
irc.ErrUmodeUnknownFlag,
|
||||
"Unknown MODE flag",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
adding := modeStr[0] == '+'
|
||||
|
||||
for _, ch := range modeStr[1:] {
|
||||
switch ch {
|
||||
case 'w':
|
||||
_ = c.database.SetSessionWallops(
|
||||
ctx, c.sessionID, adding,
|
||||
)
|
||||
case 'o':
|
||||
if adding {
|
||||
c.sendNumeric(
|
||||
irc.ErrUmodeUnknownFlag,
|
||||
"Unknown MODE flag",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.database.SetSessionOper(
|
||||
ctx, c.sessionID, false,
|
||||
)
|
||||
default:
|
||||
c.sendNumeric(
|
||||
irc.ErrUmodeUnknownFlag,
|
||||
"Unknown MODE flag",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplUmodeIs,
|
||||
c.buildUmodeString(ctx),
|
||||
)
|
||||
}
|
||||
|
||||
// buildUmodeString returns the current user mode string.
|
||||
func (c *Conn) buildUmodeString(
|
||||
ctx context.Context,
|
||||
) string {
|
||||
modes := "+"
|
||||
|
||||
isOper, err := c.database.IsSessionOper(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err == nil && isOper {
|
||||
modes += "o"
|
||||
}
|
||||
|
||||
isWallops, err := c.database.IsSessionWallops(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err == nil && isWallops {
|
||||
modes += "w"
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
|
||||
// handleNames replies with channel member list.
|
||||
@@ -1299,3 +1379,191 @@ func (c *Conn) handleUserhost(
|
||||
strings.Join(replies, " "),
|
||||
)
|
||||
}
|
||||
|
||||
// handleVersion replies with the server version string.
|
||||
func (c *Conn) handleVersion(ctx context.Context) {
|
||||
_ = ctx
|
||||
|
||||
version := "neoirc-0.1"
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplVersion,
|
||||
version+".", c.cfg.ServerName,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
// handleAdmin replies with server admin info.
|
||||
func (c *Conn) handleAdmin(ctx context.Context) {
|
||||
_ = ctx
|
||||
|
||||
srvName := c.cfg.ServerName
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplAdminMe,
|
||||
srvName, "Administrative info",
|
||||
)
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplAdminLoc1,
|
||||
"neoirc server",
|
||||
)
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplAdminLoc2,
|
||||
"IRC over HTTP",
|
||||
)
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplAdminEmail,
|
||||
"admin@"+srvName,
|
||||
)
|
||||
}
|
||||
|
||||
// handleInfo replies with server software info.
|
||||
func (c *Conn) handleInfo(ctx context.Context) {
|
||||
_ = ctx
|
||||
|
||||
infoLines := []string{
|
||||
"neoirc — IRC semantics over HTTP",
|
||||
"Version: neoirc-0.1",
|
||||
"Written in Go",
|
||||
}
|
||||
|
||||
for _, line := range infoLines {
|
||||
c.sendNumeric(irc.RplInfo, line)
|
||||
}
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplEndOfInfo,
|
||||
"End of /INFO list",
|
||||
)
|
||||
}
|
||||
|
||||
// handleTime replies with the server's current time.
|
||||
func (c *Conn) handleTime(ctx context.Context) {
|
||||
_ = ctx
|
||||
|
||||
srvName := c.cfg.ServerName
|
||||
|
||||
c.sendNumeric(
|
||||
irc.RplTime,
|
||||
srvName, time.Now().Format(time.RFC1123),
|
||||
)
|
||||
}
|
||||
|
||||
// handleKillCmd forcibly disconnects a target user (oper
|
||||
// only).
|
||||
func (c *Conn) handleKillCmd(
|
||||
ctx context.Context,
|
||||
msg *Message,
|
||||
) {
|
||||
isOper, err := c.database.IsSessionOper(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err != nil || !isOper {
|
||||
c.sendNumeric(
|
||||
irc.ErrNoPrivileges,
|
||||
"Permission Denied- "+
|
||||
"You're not an IRC operator",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(msg.Params) < 1 {
|
||||
c.sendNumeric(
|
||||
irc.ErrNeedMoreParams,
|
||||
"KILL", "Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
targetNick := msg.Params[0]
|
||||
|
||||
reason := "KILLed"
|
||||
if len(msg.Params) > 1 {
|
||||
reason = msg.Params[1]
|
||||
}
|
||||
|
||||
if targetNick == c.nick {
|
||||
c.sendNumeric(
|
||||
irc.ErrCantKillServer,
|
||||
"You cannot KILL yourself",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
targetSID, lookupErr := c.database.GetSessionByNick(
|
||||
ctx, targetNick,
|
||||
)
|
||||
if lookupErr != nil {
|
||||
c.sendNumeric(
|
||||
irc.ErrNoSuchNick,
|
||||
targetNick, "No such nick/channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
quitReason := "Killed (" + c.nick + " (" + reason + "))"
|
||||
|
||||
c.svc.BroadcastQuit(
|
||||
ctx, targetSID, targetNick, quitReason,
|
||||
)
|
||||
}
|
||||
|
||||
// handleWallopsCmd broadcasts to all +w users (oper only).
|
||||
func (c *Conn) handleWallopsCmd(
|
||||
ctx context.Context,
|
||||
msg *Message,
|
||||
) {
|
||||
isOper, err := c.database.IsSessionOper(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err != nil || !isOper {
|
||||
c.sendNumeric(
|
||||
irc.ErrNoPrivileges,
|
||||
"Permission Denied- "+
|
||||
"You're not an IRC operator",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(msg.Params) < 1 {
|
||||
c.sendNumeric(
|
||||
irc.ErrNeedMoreParams,
|
||||
"WALLOPS", "Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
message := msg.Params[0]
|
||||
|
||||
wallopsSIDs, wallErr := c.database.
|
||||
GetWallopsSessionIDs(ctx)
|
||||
if wallErr != nil {
|
||||
c.log.Error(
|
||||
"get wallops sessions failed",
|
||||
"error", wallErr,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(wallopsSIDs) > 0 {
|
||||
body, mErr := json.Marshal([]string{message})
|
||||
if mErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _, _ = c.svc.FanOut(
|
||||
ctx, irc.CmdWallops, c.nick, "*",
|
||||
nil, body, nil, wallopsSIDs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user