package ircserver import ( "context" "encoding/json" "strconv" "strings" "time" "git.eeqj.de/sneak/neoirc/pkg/irc" ) // handleCAP silently acknowledges CAP negotiation. func (c *Conn) handleCAP(msg *Message) { if len(msg.Params) == 0 { return } sub := strings.ToUpper(msg.Params[0]) if sub == "LS" { c.send(FormatMessage( c.serverSfx, "CAP", "*", "LS", "", )) } // CAP END and other subcommands are silently ignored. } // handlePing replies with a PONG. func (c *Conn) handlePing(msg *Message) { token := c.serverSfx if len(msg.Params) > 0 { token = msg.Params[0] } c.sendFromServer("PONG", c.serverSfx, token) } // handleNick changes the user's nickname. func (c *Conn) handleNick(ctx context.Context, msg *Message) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNoNicknameGiven, "No nickname given", ) return } newNick := msg.Params[0] if len(newNick) > maxNickLen { newNick = newNick[:maxNickLen] } oldMask := c.hostmask() oldNick := c.nick err := c.database.ChangeNick( ctx, c.sessionID, newNick, ) if err != nil { if strings.Contains(err.Error(), "UNIQUE") { c.sendNumeric( irc.ErrNicknameInUse, newNick, "Nickname is already in use", ) return } c.sendNumeric( irc.ErrErroneusNickname, newNick, "Erroneous nickname", ) return } c.mu.Lock() c.nick = newNick c.mu.Unlock() // Echo NICK change to the client. c.send(FormatMessage(oldMask, "NICK", newNick)) // Broadcast nick change to shared channels. c.broadcastNickChange(ctx, oldNick, newNick) } // broadcastNickChange notifies channel peers of a nick // change. func (c *Conn) broadcastNickChange( ctx context.Context, oldNick, newNick string, ) { channels, err := c.database.GetSessionChannels( ctx, c.sessionID, ) if err != nil { return } body, _ := json.Marshal([]string{newNick}) //nolint:errchkjson notified := make(map[int64]bool) for _, ch := range channels { chID, getErr := c.database.GetChannelByName( ctx, ch.Name, ) if getErr != nil { continue } memberIDs, memErr := c.database.GetChannelMemberIDs( ctx, chID, ) if memErr != nil { continue } for _, mid := range memberIDs { if mid == c.sessionID || notified[mid] { continue } notified[mid] = true dbID, _, insErr := c.database.InsertMessage( ctx, irc.CmdNick, oldNick, "", nil, body, nil, ) if insErr != nil { continue } _ = c.database.EnqueueToSession( ctx, mid, dbID, ) c.brk.Notify(mid) } } } // handlePrivmsg handles PRIVMSG and NOTICE commands. func (c *Conn) handlePrivmsg( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNoRecipient, "No recipient given ("+msg.Command+")", ) return } if len(msg.Params) < 2 { //nolint:mnd c.sendNumeric( irc.ErrNoTextToSend, "No text to send", ) return } target := msg.Params[0] text := msg.Params[1] body, _ := json.Marshal([]string{text}) //nolint:errchkjson if strings.HasPrefix(target, "#") { c.handleChannelMsg(ctx, msg.Command, target, body) } else { c.handleDirectMsg(ctx, msg.Command, target, body) } } // handleChannelMsg sends a message to a channel. func (c *Conn) handleChannelMsg( ctx context.Context, command, channel string, body json.RawMessage, ) { chID, err := c.database.GetChannelByName(ctx, channel) if err != nil { c.sendNumeric( irc.ErrNoSuchChannel, channel, "No such channel", ) return } isMember, _ := c.database.IsChannelMember( ctx, chID, c.sessionID, ) if !isMember { c.sendNumeric( irc.ErrCannotSendToChan, channel, "Cannot send to channel", ) return } // Check moderated mode. moderated, _ := c.database.IsChannelModerated(ctx, chID) if moderated { isOp, _ := c.database.IsChannelOperator( ctx, chID, c.sessionID, ) isVoiced, _ := c.database.IsChannelVoiced( ctx, chID, c.sessionID, ) if !isOp && !isVoiced { c.sendNumeric( irc.ErrCannotSendToChan, channel, "Cannot send to channel (+m)", ) return } } memberIDs, _ := c.database.GetChannelMemberIDs( ctx, chID, ) // Fan out to all members except sender. for _, mid := range memberIDs { if mid == c.sessionID { continue } dbID, _, insErr := c.database.InsertMessage( ctx, command, c.nick, channel, nil, body, nil, ) if insErr != nil { continue } _ = c.database.EnqueueToSession(ctx, mid, dbID) c.brk.Notify(mid) } } // handleDirectMsg sends a private message to a user. func (c *Conn) handleDirectMsg( ctx context.Context, command, target string, body json.RawMessage, ) { targetID, err := c.database.GetSessionByNick( ctx, target, ) if err != nil { c.sendNumeric( irc.ErrNoSuchNick, target, "No such nick", ) return } // Check AWAY status. away, _ := c.database.GetAway(ctx, targetID) if away != "" { c.sendNumeric( irc.RplAway, target, away, ) } dbID, _, insErr := c.database.InsertMessage( ctx, command, c.nick, target, nil, body, nil, ) if insErr != nil { return } _ = c.database.EnqueueToSession(ctx, targetID, dbID) c.brk.Notify(targetID) } // handleJoin joins one or more channels. func (c *Conn) handleJoin(ctx context.Context, msg *Message) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNeedMoreParams, "JOIN", "Not enough parameters", ) return } channels := strings.Split(msg.Params[0], ",") for _, chanName := range channels { chanName = strings.TrimSpace(chanName) if !strings.HasPrefix(chanName, "#") { chanName = "#" + chanName } c.joinChannel(ctx, chanName) } } // joinChannel joins a single channel. func (c *Conn) joinChannel( ctx context.Context, channel string, ) { chID, err := c.database.GetOrCreateChannel(ctx, channel) if err != nil { c.log.Error( "get/create channel failed", "error", err, ) return } // First joiner becomes operator. memberCount, countErr := c.database.CountChannelMembers( ctx, chID, ) isCreator := countErr == nil && memberCount == 0 if isCreator { err = c.database.JoinChannelAsOperator( ctx, chID, c.sessionID, ) } else { err = c.database.JoinChannel( ctx, chID, c.sessionID, ) } if err != nil { return } // Fan out JOIN to all channel members. memberIDs, _ := c.database.GetChannelMemberIDs( ctx, chID, ) joinBody, _ := json.Marshal([]string{channel}) //nolint:errchkjson for _, mid := range memberIDs { dbID, _, insErr := c.database.InsertMessage( ctx, irc.CmdJoin, c.nick, channel, nil, joinBody, nil, ) if insErr != nil { continue } _ = c.database.EnqueueToSession(ctx, mid, dbID) c.brk.Notify(mid) } // Send JOIN echo to this client directly on wire. c.send(FormatMessage(c.hostmask(), "JOIN", channel)) // Send topic. c.deliverTopic(ctx, channel, chID) // Send NAMES. c.deliverNames(ctx, channel, chID) } // deliverTopic sends RPL_TOPIC or RPL_NOTOPIC. func (c *Conn) deliverTopic( ctx context.Context, channel string, chID int64, ) { channels, err := c.database.ListChannels( ctx, c.sessionID, ) topic := "" if err == nil { for _, ch := range channels { if ch.Name == channel { topic = ch.Topic break } } } if topic == "" { c.sendNumeric( irc.RplNoTopic, channel, "No topic is set", ) return } c.sendNumeric(irc.RplTopic, channel, topic) meta, tmErr := c.database.GetTopicMeta(ctx, chID) if tmErr == nil && meta != nil { c.sendNumeric( irc.RplTopicWhoTime, channel, meta.SetBy, strconv.FormatInt(meta.SetAt.Unix(), 10), ) } } // deliverNames sends RPL_NAMREPLY and RPL_ENDOFNAMES. func (c *Conn) deliverNames( ctx context.Context, channel string, chID int64, ) { members, err := c.database.ChannelMembers(ctx, chID) if err != nil { c.sendNumeric( irc.RplEndOfNames, channel, "End of /NAMES list", ) return } names := make([]string, 0, len(members)) for _, member := range members { prefix := "" if member.IsOperator { prefix = "@" } else if member.IsVoiced { prefix = "+" } names = append(names, prefix+member.Nick) } nameStr := strings.Join(names, " ") c.sendNumeric( irc.RplNamReply, "=", channel, nameStr, ) c.sendNumeric( irc.RplEndOfNames, channel, "End of /NAMES list", ) } // handlePart leaves one or more channels. func (c *Conn) handlePart( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNeedMoreParams, "PART", "Not enough parameters", ) return } reason := "" if len(msg.Params) > 1 { reason = msg.Params[1] } channels := strings.Split(msg.Params[0], ",") for _, ch := range channels { ch = strings.TrimSpace(ch) c.partChannel(ctx, ch, reason) } } // partChannel leaves a single channel. func (c *Conn) partChannel( ctx context.Context, channel, reason string, ) { chID, err := c.database.GetChannelByName(ctx, channel) if err != nil { c.sendNumeric( irc.ErrNoSuchChannel, channel, "No such channel", ) return } isMember, _ := c.database.IsChannelMember( ctx, chID, c.sessionID, ) if !isMember { c.sendNumeric( irc.ErrNotOnChannel, channel, "You're not on that channel", ) return } // Broadcast PART to channel members before leaving. memberIDs, _ := c.database.GetChannelMemberIDs( ctx, chID, ) body, _ := json.Marshal([]string{reason}) //nolint:errchkjson for _, mid := range memberIDs { if mid == c.sessionID { continue } dbID, _, insErr := c.database.InsertMessage( ctx, irc.CmdPart, c.nick, channel, nil, body, nil, ) if insErr != nil { continue } _ = c.database.EnqueueToSession(ctx, mid, dbID) c.brk.Notify(mid) } // Echo PART to the client. if reason != "" { c.send(FormatMessage( c.hostmask(), "PART", channel, reason, )) } else { c.send(FormatMessage( c.hostmask(), "PART", channel, )) } c.database.PartChannel(ctx, chID, c.sessionID) //nolint:errcheck,gosec c.database.DeleteChannelIfEmpty(ctx, chID) //nolint:errcheck,gosec } // handleQuit handles the QUIT command. func (c *Conn) handleQuit(msg *Message) { reason := "Client quit" if len(msg.Params) > 0 { reason = msg.Params[0] } c.send("ERROR :Closing Link: " + c.hostname + " (Quit: " + reason + ")") c.closed = true } // handleTopic gets or sets a channel topic. // //nolint:funlen // coherent flow func (c *Conn) handleTopic( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNeedMoreParams, "TOPIC", "Not enough parameters", ) return } channel := msg.Params[0] chID, err := c.database.GetChannelByName(ctx, channel) if err != nil { c.sendNumeric( irc.ErrNoSuchChannel, channel, "No such channel", ) return } // If no second param, query the topic. if len(msg.Params) < 2 { //nolint:mnd c.deliverTopic(ctx, channel, chID) return } // Set topic — check permissions. isMember, _ := c.database.IsChannelMember( ctx, chID, c.sessionID, ) if !isMember { c.sendNumeric( irc.ErrNotOnChannel, channel, "You're not on that channel", ) return } topicLocked, _ := c.database.IsChannelTopicLocked( ctx, chID, ) if topicLocked { isOp, _ := c.database.IsChannelOperator( ctx, chID, c.sessionID, ) if !isOp { c.sendNumeric( irc.ErrChanOpPrivsNeeded, channel, "You're not channel operator", ) return } } newTopic := msg.Params[1] err = c.database.SetTopic(ctx, channel, newTopic) if err != nil { return } _ = c.database.SetTopicMeta( ctx, channel, newTopic, c.nick, ) // Broadcast TOPIC to all members. memberIDs, _ := c.database.GetChannelMemberIDs( ctx, chID, ) body, _ := json.Marshal([]string{newTopic}) //nolint:errchkjson for _, mid := range memberIDs { dbID, _, insErr := c.database.InsertMessage( ctx, irc.CmdTopic, c.nick, channel, nil, body, nil, ) if insErr != nil { continue } _ = c.database.EnqueueToSession(ctx, mid, dbID) c.brk.Notify(mid) } // Echo to the setting client on wire. c.send(FormatMessage( c.hostmask(), "TOPIC", channel, newTopic, )) } // handleMode handles MODE queries and changes. func (c *Conn) handleMode( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNeedMoreParams, "MODE", "Not enough parameters", ) return } target := msg.Params[0] if strings.HasPrefix(target, "#") { c.handleChannelMode(ctx, msg) } else { c.handleUserMode(ctx, msg) } } // handleChannelMode handles MODE for channels. func (c *Conn) handleChannelMode( ctx context.Context, msg *Message, ) { channel := msg.Params[0] chID, err := c.database.GetChannelByName(ctx, channel) if err != nil { c.sendNumeric( irc.ErrNoSuchChannel, channel, "No such channel", ) return } // Query mode if no mode string given. if len(msg.Params) < 2 { //nolint:mnd modeStr := c.buildChannelModeString(ctx, chID) c.sendNumeric( irc.RplChannelModeIs, channel, modeStr, ) created, _ := c.database.GetChannelCreatedAt( ctx, chID, ) if !created.IsZero() { c.sendNumeric( irc.RplCreationTime, channel, strconv.FormatInt(created.Unix(), 10), ) } return } // Need ops to change modes. isOp, _ := c.database.IsChannelOperator( ctx, chID, c.sessionID, ) if !isOp { c.sendNumeric( irc.ErrChanOpPrivsNeeded, channel, "You're not channel operator", ) return } modeStr := msg.Params[1] modeArgs := msg.Params[2:] c.applyChannelModes(ctx, channel, chID, modeStr, modeArgs) } // buildChannelModeString constructs the mode string for a // channel. func (c *Conn) buildChannelModeString( ctx context.Context, chID int64, ) string { modes := "+" moderated, _ := c.database.IsChannelModerated(ctx, chID) if moderated { modes += "m" } topicLocked, _ := c.database.IsChannelTopicLocked( ctx, chID, ) if topicLocked { modes += "t" } if modes == "+" { modes = "+" } return modes } // applyChannelModes applies mode changes. // //nolint:cyclop,funlen // mode parsing is inherently branchy func (c *Conn) applyChannelModes( ctx context.Context, channel string, chID int64, modeStr string, args []string, ) { adding := true argIdx := 0 applied := "" appliedArgs := "" for _, modeChar := range modeStr { switch modeChar { case '+': adding = true case '-': adding = false case 'm': _ = c.database.SetChannelModerated( ctx, chID, adding, ) if adding { applied += "+m" } else { applied += "-m" } case 't': _ = c.database.SetChannelTopicLocked( ctx, chID, adding, ) if adding { applied += "+t" } else { applied += "-t" } case 'o': if argIdx >= len(args) { break } targetNick := args[argIdx] argIdx++ c.applyMemberMode( ctx, chID, channel, targetNick, 'o', adding, ) if adding { applied += "+o" } else { applied += "-o" } appliedArgs += " " + targetNick case 'v': if argIdx >= len(args) { break } targetNick := args[argIdx] argIdx++ c.applyMemberMode( ctx, chID, channel, targetNick, 'v', adding, ) if adding { applied += "+v" } else { applied += "-v" } appliedArgs += " " + targetNick default: c.sendNumeric( irc.ErrUnknownMode, string(modeChar), "is unknown mode char to me", ) } } if applied != "" { modeReply := applied if appliedArgs != "" { modeReply += appliedArgs } c.send(FormatMessage( c.hostmask(), "MODE", channel, modeReply, )) } } // applyMemberMode applies +o/-o or +v/-v on a member. func (c *Conn) applyMemberMode( ctx context.Context, chID int64, channel, targetNick string, mode rune, adding bool, ) { targetSessionID, err := c.database.GetSessionByNick( ctx, targetNick, ) if err != nil { c.sendNumeric( irc.ErrNoSuchNick, targetNick, "No such nick/channel", ) return } isMember, _ := c.database.IsChannelMember( ctx, chID, targetSessionID, ) if !isMember { c.sendNumeric( irc.ErrUserNotInChannel, targetNick, channel, "They aren't on that channel", ) return } switch mode { case 'o': _ = c.database.SetChannelMemberOperator( ctx, chID, targetSessionID, adding, ) case 'v': _ = c.database.SetChannelMemberVoiced( ctx, chID, targetSessionID, adding, ) } } // handleUserMode handles MODE for users. func (c *Conn) handleUserMode( _ context.Context, msg *Message, ) { target := msg.Params[0] if !strings.EqualFold(target, c.nick) { c.sendNumeric( irc.ErrUsersDoNotMatch, "Can't change mode for other users", ) return } if len(msg.Params) < 2 { //nolint:mnd c.sendNumeric(irc.RplUmodeIs, "+") return } // We don't support user modes beyond the basics. c.sendNumeric(irc.RplUmodeIs, "+") } // handleNames replies with channel member list. func (c *Conn) handleNames( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.RplEndOfNames, "*", "End of /NAMES list", ) return } channel := msg.Params[0] chID, err := c.database.GetChannelByName(ctx, channel) if err != nil { c.sendNumeric( irc.RplEndOfNames, channel, "End of /NAMES list", ) return } c.deliverNames(ctx, channel, chID) } // handleList sends the channel list. func (c *Conn) handleList(ctx context.Context) { channels, err := c.database.ListAllChannelsWithCounts( ctx, ) if err != nil { c.sendNumeric( irc.RplListEnd, "End of /LIST", ) return } c.sendNumeric(irc.RplListStart, "Channel", "Users Name") for idx := range channels { c.sendNumeric( irc.RplList, channels[idx].Name, strconv.FormatInt( channels[idx].MemberCount, 10, ), channels[idx].Topic, ) } c.sendNumeric(irc.RplListEnd, "End of /LIST") } // handleWhois replies with user info. // //nolint:funlen // WHOIS has many reply fields func (c *Conn) handleWhois( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNoNicknameGiven, "No nickname given", ) return } // The target nick may be the second param // (WHOIS server nick). target := msg.Params[0] if len(msg.Params) > 1 { target = msg.Params[1] } targetID, err := c.database.GetSessionByNick( ctx, target, ) if err != nil { c.sendNumeric( irc.ErrNoSuchNick, target, "No such nick", ) c.sendNumeric( irc.RplEndOfWhois, target, "End of /WHOIS list", ) return } // Get host info. hostInfo, _ := c.database.GetSessionHostInfo( ctx, targetID, ) username := target hostname := "*" if hostInfo != nil { username = hostInfo.Username hostname = hostInfo.Hostname } c.sendNumeric( irc.RplWhoisUser, target, username, hostname, "*", target, ) c.sendNumeric( irc.RplWhoisServer, target, c.serverSfx, "neoirc server", ) // Check oper status. isOper, _ := c.database.IsSessionOper(ctx, targetID) if isOper { c.sendNumeric( irc.RplWhoisOperator, target, "is an IRC operator", ) } // Get channels. userChannels, _ := c.database.GetSessionChannels( ctx, targetID, ) if len(userChannels) > 0 { var chanList []string for _, userChan := range userChannels { chID, getErr := c.database.GetChannelByName( ctx, userChan.Name, ) if getErr != nil { chanList = append( chanList, userChan.Name, ) continue } isChOp, _ := c.database.IsChannelOperator( ctx, chID, targetID, ) isVoiced, _ := c.database.IsChannelVoiced( ctx, chID, targetID, ) prefix := "" if isChOp { prefix = "@" } else if isVoiced { prefix = "+" } chanList = append( chanList, prefix+userChan.Name, ) } c.sendNumeric( irc.RplWhoisChannels, target, strings.Join(chanList, " "), ) } // Idle time. lastSeen, _ := c.database.GetSessionLastSeen( ctx, targetID, ) created, _ := c.database.GetSessionCreatedAt( ctx, targetID, ) if !lastSeen.IsZero() { idle := int64(time.Since(lastSeen).Seconds()) signonTS := int64(0) if !created.IsZero() { signonTS = created.Unix() } c.sendNumeric( irc.RplWhoisIdle, target, strconv.FormatInt(idle, 10), strconv.FormatInt(signonTS, 10), "seconds idle, signon time", ) } // Away. away, _ := c.database.GetAway(ctx, targetID) if away != "" { c.sendNumeric(irc.RplAway, target, away) } c.sendNumeric( irc.RplEndOfWhois, target, "End of /WHOIS list", ) } // handleWho sends WHO replies for a channel. func (c *Conn) handleWho( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.RplEndOfWho, "*", "End of /WHO list", ) return } target := msg.Params[0] if !strings.HasPrefix(target, "#") { // WHO for a nick. c.whoNick(ctx, target) return } chID, err := c.database.GetChannelByName(ctx, target) if err != nil { c.sendNumeric( irc.RplEndOfWho, target, "End of /WHO list", ) return } members, err := c.database.ChannelMembers(ctx, chID) if err != nil { c.sendNumeric( irc.RplEndOfWho, target, "End of /WHO list", ) return } for _, member := range members { flags := "H" if member.IsOperator { flags += "@" } else if member.IsVoiced { flags += "+" } c.sendNumeric( irc.RplWhoReply, target, member.Username, member.Hostname, c.serverSfx, member.Nick, flags, "0 "+member.Nick, ) } c.sendNumeric( irc.RplEndOfWho, target, "End of /WHO list", ) } // whoNick sends WHO reply for a single nick. func (c *Conn) whoNick(ctx context.Context, nick string) { targetID, err := c.database.GetSessionByNick(ctx, nick) if err != nil { c.sendNumeric( irc.RplEndOfWho, nick, "End of /WHO list", ) return } hostInfo, _ := c.database.GetSessionHostInfo( ctx, targetID, ) username := nick hostname := "*" if hostInfo != nil { username = hostInfo.Username hostname = hostInfo.Hostname } c.sendNumeric( irc.RplWhoReply, "*", username, hostname, c.serverSfx, nick, "H", "0 "+nick, ) c.sendNumeric( irc.RplEndOfWho, nick, "End of /WHO list", ) } // handleLusers replies with server statistics. func (c *Conn) handleLusers(ctx context.Context) { c.deliverLusers(ctx) } // handleOper handles the OPER command. func (c *Conn) handleOper( ctx context.Context, msg *Message, ) { if len(msg.Params) < 2 { //nolint:mnd c.sendNumeric( irc.ErrNeedMoreParams, "OPER", "Not enough parameters", ) return } name := msg.Params[0] password := msg.Params[1] cfgName := c.cfg.OperName cfgPassword := c.cfg.OperPassword if cfgName == "" || cfgPassword == "" { c.sendNumeric( irc.ErrNoOperHost, "No O-lines for your host", ) return } if name != cfgName || password != cfgPassword { c.sendNumeric( irc.ErrPasswdMismatch, "Password incorrect", ) return } _ = c.database.SetSessionOper(ctx, c.sessionID, true) c.sendNumeric( irc.RplYoureOper, "You are now an IRC operator", ) } // handleAway sets or clears the AWAY status. func (c *Conn) handleAway( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 || msg.Params[0] == "" { _ = c.database.SetAway(ctx, c.sessionID, "") c.sendNumeric( irc.RplUnaway, "You are no longer marked as being away", ) return } _ = c.database.SetAway(ctx, c.sessionID, msg.Params[0]) c.sendNumeric( irc.RplNowAway, "You have been marked as being away", ) } // handleKick kicks a user from a channel. // //nolint:funlen // coherent flow func (c *Conn) handleKick( ctx context.Context, msg *Message, ) { if len(msg.Params) < 2 { //nolint:mnd c.sendNumeric( irc.ErrNeedMoreParams, "KICK", "Not enough parameters", ) return } channel := msg.Params[0] targetNick := msg.Params[1] reason := targetNick if len(msg.Params) > 2 { //nolint:mnd reason = msg.Params[2] } chID, err := c.database.GetChannelByName(ctx, channel) if err != nil { c.sendNumeric( irc.ErrNoSuchChannel, channel, "No such channel", ) return } isOp, _ := c.database.IsChannelOperator( ctx, chID, c.sessionID, ) if !isOp { c.sendNumeric( irc.ErrChanOpPrivsNeeded, channel, "You're not channel operator", ) return } targetSessionID, err := c.database.GetSessionByNick( ctx, targetNick, ) if err != nil { c.sendNumeric( irc.ErrNoSuchNick, targetNick, "No such nick/channel", ) return } isMember, _ := c.database.IsChannelMember( ctx, chID, targetSessionID, ) if !isMember { c.sendNumeric( irc.ErrUserNotInChannel, targetNick, channel, "They aren't on that channel", ) return } // Broadcast KICK to all channel members. memberIDs, _ := c.database.GetChannelMemberIDs( ctx, chID, ) body, _ := json.Marshal([]string{reason}) //nolint:errchkjson for _, mid := range memberIDs { dbID, _, insErr := c.database.InsertMessage( ctx, irc.CmdKick, c.nick, channel, nil, body, nil, ) if insErr != nil { continue } _ = c.database.EnqueueToSession(ctx, mid, dbID) c.brk.Notify(mid) } // Remove from channel. c.database.PartChannel(ctx, chID, targetSessionID) //nolint:errcheck,gosec c.database.DeleteChannelIfEmpty(ctx, chID) //nolint:errcheck,gosec // Echo KICK on wire. c.send(FormatMessage( c.hostmask(), "KICK", channel, targetNick, reason, )) } // handlePassPostReg handles PASS after registration (for // setting a session password). func (c *Conn) handlePassPostReg( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { c.sendNumeric( irc.ErrNeedMoreParams, "PASS", "Not enough parameters", ) return } password := msg.Params[0] if len(password) < minPasswordLen { c.sendFromServer("NOTICE", c.nick, "Password must be at least 8 characters", ) return } c.setPassword(ctx, password) c.sendFromServer("NOTICE", c.nick, "Password set. You can reconnect using "+ "PASS with your nick.", ) } // handleInvite handles the INVITE command. func (c *Conn) handleInvite( ctx context.Context, msg *Message, ) { if len(msg.Params) < 2 { //nolint:mnd c.sendNumeric( irc.ErrNeedMoreParams, "INVITE", "Not enough parameters", ) return } targetNick := msg.Params[0] channel := msg.Params[1] chID, err := c.database.GetChannelByName(ctx, channel) if err != nil { c.sendNumeric( irc.ErrNoSuchChannel, channel, "No such channel", ) return } isMember, _ := c.database.IsChannelMember( ctx, chID, c.sessionID, ) if !isMember { c.sendNumeric( irc.ErrNotOnChannel, channel, "You're not on that channel", ) return } targetID, err := c.database.GetSessionByNick( ctx, targetNick, ) if err != nil { c.sendNumeric( irc.ErrNoSuchNick, targetNick, "No such nick", ) return } c.sendNumeric( irc.RplInviting, targetNick, channel, ) // Send INVITE notice to target. body, _ := json.Marshal( //nolint:errchkjson []string{"You have been invited to " + channel}, ) dbID, _, insErr := c.database.InsertMessage( ctx, "INVITE", c.nick, targetNick, nil, body, nil, ) if insErr == nil { _ = c.database.EnqueueToSession( ctx, targetID, dbID, ) c.brk.Notify(targetID) } } // handleUserhost replies with USERHOST info. func (c *Conn) handleUserhost( ctx context.Context, msg *Message, ) { if len(msg.Params) < 1 { return } replies := make([]string, 0, len(msg.Params)) for _, nick := range msg.Params { sid, err := c.database.GetSessionByNick(ctx, nick) if err != nil { continue } hostInfo, _ := c.database.GetSessionHostInfo( ctx, sid, ) host := "*" if hostInfo != nil { host = hostInfo.Hostname } isOper, _ := c.database.IsSessionOper(ctx, sid) operStar := "" if isOper { operStar = "*" } replies = append( replies, nick+operStar+"=+"+nick+"@"+host, ) } c.sendNumeric( irc.RplUserHost, strings.Join(replies, " "), ) }