diff --git a/hid/main.go b/hid/main.go index 75f4231..4c9176b 100644 --- a/hid/main.go +++ b/hid/main.go @@ -556,14 +556,40 @@ func initiateQuit() { quitting = true } -// TODO: ircChannelCreate - -func ircChannelDestroyIfEmpty(ch *channel) { - // TODO +func ircChannelCreate(name string) *channel { + ch := &channel{ + name: name, + created: time.Now().UnixNano(), + userLimit: -1, + } + channels[ircToCanon(name)] = ch + return ch } +func ircChannelDestroyIfEmpty(ch *channel) { + if len(ch.userModes) == 0 { + delete(channels, ircToCanon(ch.name)) + } +} + +// TODO: Improve the name as it takes mode +q into account. func ircSendToRoommates(c *client, message string) { - // TODO + targets := make(map[*client]bool) + for _, ch := range channels { + _, present := ch.userModes[c] + if !present || 0 != ch.modes&ircChanModeQuiet { + continue + } + for client := range ch.userModes { + targets[client] = true + } + } + + for roommate := range targets { + if roommate != c { + roommate.send(message) + } + } } // --- Clients (continued) ----------------------------------------------------- @@ -1662,13 +1688,248 @@ func ircHandleTOPIC(msg *message, c *client) { ircChannelMulticast(ch, message, nil) } -// TODO: All the various real command handlers. +func ircTryPart(c *client, target string, reason string) { + if reason == "" { + reason = c.nickname + } -func ircHandleX(msg *message, c *client) { + ch := channels[ircToCanon(target)] + if ch == nil { + c.sendReply(ERR_NOSUCHCHANNEL, target) + return + } + + if _, present := ch.userModes[c]; !present { + c.sendReply(ERR_NOTONCHANNEL, target) + return + } + + message := fmt.Sprintf(":%s@%s@%s PART %s :%s", + c.nickname, c.username, c.hostname, target, reason) + if 0 == ch.modes&ircChanModeQuiet { + ircChannelMulticast(ch, message, nil) + } else { + c.send(message) + } + + delete(ch.userModes, c) + ircChannelDestroyIfEmpty(ch) +} + +func ircPartAllChannels(c *client) { + for _, ch := range channels { + if _, present := ch.userModes[c]; present { + ircTryPart(c, ch.name, "") + } + } +} + +func ircHandlePART(msg *message, c *client) { if len(msg.params) < 1 { c.sendReply(ERR_NEEDMOREPARAMS, msg.command) return } + + reason := "" + if len(msg.params) > 1 { + reason = msg.params[1] + } + + for _, target := range splitString(msg.params[0], ",", true) { + ircTryPart(c, target, reason) + } +} + +// TODO: Undo the rename from channelName to target, also in ircTryPart. +func ircTryKick(c *client, target, nick, reason string) { + ch := channels[ircToCanon(target)] + if ch == nil { + c.sendReply(ERR_NOSUCHCHANNEL, target) + return + } + + if modes, present := ch.userModes[c]; !present { + c.sendReply(ERR_NOTONCHANNEL, target) + return + } else if 0 == modes&ircChanModeOperator { + c.sendReply(ERR_CHANOPRIVSNEEDED, target) + return + } + + client := users[ircToCanon(nick)] + if _, present := ch.userModes[client]; client == nil || !present { + c.sendReply(ERR_USERNOTINCHANNEL, nick, target) + return + } + + message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s", + c.nickname, c.username, c.hostname, target, nick, reason) + if 0 == ch.modes&ircChanModeQuiet { + ircChannelMulticast(ch, message, nil) + } else { + c.send(message) + } + + delete(ch.userModes, client) + ircChannelDestroyIfEmpty(ch) +} + +func ircHandleKICK(msg *message, c *client) { + if len(msg.params) < 2 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } + + reason := c.nickname + if len(msg.params) > 2 { + reason = msg.params[2] + } + + targetChannels := splitString(msg.params[0], ",", true) + targetUsers := splitString(msg.params[1], ",", true) + + if len(channels) == 1 { + for i := 0; i < len(targetUsers); i++ { + ircTryKick(c, targetChannels[0], targetUsers[i], reason) + } + } else { + for i := 0; i < len(channels) && i < len(targetUsers); i++ { + ircTryKick(c, targetChannels[i], targetUsers[i], reason) + } + } +} + +func ircSendInviteNotifications(ch *channel, c, target *client) { + for client := range ch.userModes { + if client != target && 0 != client.capsEnabled&ircCapInviteNotify { + client.sendf(":%s!%s@%s INVITE %s %s", + c.nickname, c.username, c.hostname, target.nickname, ch.name) + } + } +} + +func ircHandleINVITE(msg *message, c *client) { + if len(msg.params) < 2 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } + + target, channelName := msg.params[0], msg.params[1] + client := users[ircToCanon(target)] + if client == nil { + c.sendReply(ERR_NOSUCHNICK, target) + return + } + + if ch := channels[ircToCanon(channelName)]; ch != nil { + invitingModes, invitingPresent := ch.userModes[c] + if !invitingPresent { + c.sendReply(ERR_NOTONCHANNEL, channelName) + return + } + if _, present := ch.userModes[client]; present { + c.sendReply(ERR_USERONCHANNEL, target, channelName) + return + } + + if 0 != invitingModes&ircChanModeOperator { + client.invites[ircToCanon(channelName)] = true + } else if 0 != ch.modes&ircChanModeInviteOnly { + c.sendReply(ERR_CHANOPRIVSNEEDED, channelName) + return + } + + // It's not specified when and how we should send out invite-notify. + if 0 != ch.modes&ircChanModeInviteOnly { + ircSendInviteNotifications(ch, c, client) + } + } + + client.sendf(":%s!%s@%s INVITE %s %s", + c.nickname, c.username, c.hostname, client.nickname, channelName) + if client.awayMessage != "" { + c.sendReply(RPL_AWAY, client.nickname, client.awayMessage) + } + c.sendReply(RPL_INVITING, client.nickname, channelName) +} + +func ircTryJoin(c *client, channelName, key string) { + ch := channels[ircToCanon(channelName)] + var userMode uint + if ch == nil { + if !ircIsValidChannelName(channelName) { + c.sendReply(ERR_BADCHANMASK, channelName) + return + } + ch = ircChannelCreate(channelName) + userMode = ircChanModeOperator + } else if _, present := ch.userModes[c]; present { + return + } + + _, invitedByChanop := c.invites[ircToCanon(channelName)] + if 0 != ch.modes&ircChanModeInviteOnly && c.inMaskList(ch.inviteList) && + !invitedByChanop { + c.sendReply(ERR_INVITEONLYCHAN, channelName) + return + } + if ch.key != "" && (key == "" || key != ch.key) { + c.sendReply(ERR_BADCHANNELKEY, channelName) + return + } + if ch.userLimit != -1 && len(ch.userModes) >= ch.userLimit { + c.sendReply(ERR_CHANNELISFULL, channelName) + return + } + if c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList) && + !invitedByChanop { + c.sendReply(ERR_BANNEDFROMCHAN, channelName) + return + } + + // Destroy any invitation as there's no other way to get rid of it. + delete(c.invites, ircToCanon(channelName)) + + ch.userModes[c] = userMode + + message := fmt.Sprintf(":%s!%s@%s JOIN %s", + c.nickname, c.username, c.hostname, channelName) + if 0 == ch.modes&ircChanModeQuiet { + ircChannelMulticast(ch, message, nil) + } else { + c.send(message) + } + + ircSendRPLTOPIC(c, ch) + ircSendRPLNAMREPLY(c, ch, nil) + c.sendReply(RPL_ENDOFNAMES, ch.name) +} + +func ircHandleJOIN(msg *message, c *client) { + if len(msg.params) < 1 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } + + if msg.params[0] == "0" { + ircPartAllChannels(c) + return + } + + targetChannels := splitString(msg.params[0], ",", true) + + var keys []string + if len(msg.params) > 1 { + keys = splitString(msg.params[1], ",", true) + } + + for i, name := range targetChannels { + key := "" + if i < len(keys) { + key = keys[i] + } + ircTryJoin(c, name, key) + } } func ircHandleSUMMON(msg *message, c *client) { @@ -1679,11 +1940,63 @@ func ircHandleUSERS(msg *message, c *client) { c.sendReply(ERR_USERSDISABLED) } +func ircHandleAWAY(msg *message, c *client) { + if len(msg.params) < 1 { + c.awayMessage = "" + c.sendReply(RPL_UNAWAY) + } else { + c.awayMessage = msg.params[0] + c.sendReply(RPL_NOWAWAY) + } +} + +func ircHandleISON(msg *message, c *client) { + if len(msg.params) < 1 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } + + var on []string + for _, nick := range msg.params { + if client := users[ircToCanon(nick)]; client != nil { + on = append(on, nick) + } + } + c.sendReply(RPL_ISON, strings.Join(on, " ")) +} + +func ircHandleADMIN(msg *message, c *client) { + if len(msg.params) > 0 && !isThisMe(msg.params[0]) { + c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) + return + } + c.sendReply(ERR_NOADMININFO, serverName) +} + +// TODO: All the remaining command handlers. + +func ircHandleX(msg *message, c *client) { + if len(msg.params) < 1 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } +} + +func ircHandleDIE(msg *message, c *client) { + if 0 == c.mode&ircUserModeOperator { + c.sendReply(ERR_NOPRIVILEGES) + return + } + if !quitting { + initiateQuit() + } +} + // ----------------------------------------------------------------------------- // TODO: Add an index for IRC_ERR_NOSUCHSERVER validation? // TODO: Add a minimal parameter count? -// TODO: Add a field for oper-only commands? +// TODO: Add a field for oper-only commands? Use flags? var ircHandlers = map[string]*ircCommand{ "CAP": {false, ircHandleCAP, 0, 0}, "PASS": {false, ircHandlePASS, 0, 0}, @@ -1700,16 +2013,25 @@ var ircHandlers = map[string]*ircCommand{ "VERSION": {true, ircHandleVERSION, 0, 0}, "USERS": {true, ircHandleUSERS, 0, 0}, "SUMMON": {true, ircHandleSUMMON, 0, 0}, + "AWAY": {true, ircHandleAWAY, 0, 0}, + "ADMIN": {true, ircHandleADMIN, 0, 0}, "MODE": {true, ircHandleMODE, 0, 0}, "PRIVMSG": {true, ircHandlePRIVMSG, 0, 0}, "NOTICE": {true, ircHandleNOTICE, 0, 0}, + "JOIN": {true, ircHandleJOIN, 0, 0}, + "PART": {true, ircHandlePART, 0, 0}, + "KICK": {true, ircHandleKICK, 0, 0}, + "INVITE": {true, ircHandleINVITE, 0, 0}, "TOPIC": {true, ircHandleTOPIC, 0, 0}, "LIST": {true, ircHandleLIST, 0, 0}, "NAMES": {true, ircHandleNAMES, 0, 0}, "WHO": {true, ircHandleWHO, 0, 0}, "WHOIS": {true, ircHandleWHOIS, 0, 0}, "WHOWAS": {true, ircHandleWHOWAS, 0, 0}, + "ISON": {true, ircHandleISON, 0, 0}, + + "DIE": {true, ircHandleDIE, 0, 0}, } func ircProcessMessage(c *client, msg *message, raw string) { @@ -1739,7 +2061,7 @@ func ircProcessMessage(c *client, msg *message, raw string) { } } -// --- ? ----------------------------------------------------------------------- +// --- Network I/O ------------------------------------------------------------- // Handle the results from initializing the client's connection. func (c *client) onPrepared(host string, isTLS bool) {