diff --git a/irc/capability.go b/irc/capability.go index 3214fc03..d37fb94c 100644 --- a/irc/capability.go +++ b/irc/capability.go @@ -14,8 +14,8 @@ import ( type Capability string const ( - AccountTag Capability = "account-tag" AccountNotify Capability = "account-notify" + AccountTag Capability = "account-tag" AwayNotify Capability = "away-notify" CapNotify Capability = "cap-notify" ChgHost Capability = "chghost" @@ -26,6 +26,7 @@ const ( MessageIDs Capability = "draft/message-ids" MessageTags Capability = "draft/message-tags-0.2" MultiPrefix Capability = "multi-prefix" + Rename Capability = "draft/rename" SASL Capability = "sasl" ServerTime Capability = "server-time" STS Capability = "draft/sts" @@ -47,6 +48,7 @@ var ( // MaxLine is set during server startup MessageTags: true, MultiPrefix: true, + Rename: true, // SASL is set during server startup ServerTime: true, // STS is set during server startup diff --git a/irc/channelreg.go b/irc/channelreg.go index 8fad7908..ab772a0a 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -53,6 +53,12 @@ type RegisteredChannel struct { Invitelist []string } +// deleteChannelNoMutex deletes a given channel from our store. +func (server *Server) deleteChannelNoMutex(tx *buntdb.Tx, channelKey string) { + tx.Delete(fmt.Sprintf(keyChannelExists, channelKey)) + server.registeredChannels[channelKey] = nil +} + // loadChannelNoMutex loads a channel from the store. func (server *Server) loadChannelNoMutex(tx *buntdb.Tx, channelKey string) *RegisteredChannel { // return loaded chan if it already exists diff --git a/irc/commands.go b/irc/commands.go index d9de1668..21a50147 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -209,6 +209,10 @@ var Commands = map[string]Command{ handler: privmsgHandler, minParams: 2, }, + "RENAME": { + handler: renameHandler, + minParams: 2, + }, "SANICK": { handler: sanickHandler, minParams: 2, diff --git a/irc/help.go b/irc/help.go index fc44d8c5..6f682c26 100644 --- a/irc/help.go +++ b/irc/help.go @@ -354,6 +354,14 @@ Replies to a PING. Used to check link connectivity.`, text: `PRIVMSG {,} Sends the text to the given targets as a PRIVMSG.`, + }, + "rename": { + text: `RENAME [] + +Renames the given channel with the given reason, if possible. + +For example: + RENAME #ircv2 #ircv3 :Protocol upgrades!`, }, "sanick": { oper: true, diff --git a/irc/server.go b/irc/server.go index 591c11fc..e8937c72 100644 --- a/irc/server.go +++ b/irc/server.go @@ -789,6 +789,115 @@ func pongHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return false } +// RENAME [] +//TODO(dan): Clean up this function so it doesn't look like an eldrich horror... prolly by putting it into a server.renameChannel function. +func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + // get lots of locks... make sure nobody touches anything while we're doing this + server.registeredChannelsMutex.Lock() + defer server.registeredChannelsMutex.Unlock() + server.channels.ChansLock.Lock() + defer server.channels.ChansLock.Unlock() + + oldName := strings.TrimSpace(msg.Params[0]) + newName := strings.TrimSpace(msg.Params[1]) + reason := "No reason" + if 2 < len(msg.Params) { + reason = msg.Params[2] + } + + // check for all the reasons why the rename couldn't happen + casefoldedOldName, err := CasefoldChannel(oldName) + if err != nil { + //TODO(dan): Change this to ERR_CANNOTRENAME + client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", oldName, "Old channel name is invalid") + return false + } + + channel := server.channels.Chans[casefoldedOldName] + if channel == nil { + client.Send(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, oldName, "No such channel") + return false + } + + channel.membersMutex.Lock() + defer channel.membersMutex.Unlock() + + casefoldedNewName, err := CasefoldChannel(newName) + if err != nil { + //TODO(dan): Change this to ERR_CANNOTRENAME + client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", newName, "New channel name is invalid") + return false + } + + newChannel := server.channels.Chans[casefoldedNewName] + if newChannel != nil { + //TODO(dan): Change this to ERR_CHANNAMEINUSE + client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", newName, "New channel name is in use") + return false + } + + var canEdit bool + server.store.Update(func(tx *buntdb.Tx) error { + chanReg := server.loadChannelNoMutex(tx, casefoldedOldName) + if chanReg == nil || client.account == nil || client.account.Name == chanReg.Founder { + canEdit = true + } + + chanReg = server.loadChannelNoMutex(tx, casefoldedNewName) + if chanReg != nil { + canEdit = false + } + return nil + }) + if !canEdit { + //TODO(dan): Change this to ERR_CANNOTRENAME + client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", oldName, "Only channel founders can change registered channels") + return false + } + + // perform the channel rename + server.channels.Chans[casefoldedOldName] = nil + server.channels.Chans[casefoldedNewName] = channel + + channel.name = strings.TrimSpace(msg.Params[1]) + channel.nameCasefolded = casefoldedNewName + + // rename stored channel info if any exists + server.store.Update(func(tx *buntdb.Tx) error { + chanReg := server.loadChannelNoMutex(tx, casefoldedOldName) + if chanReg == nil { + return nil + } + + server.deleteChannelNoMutex(tx, casefoldedOldName) + + chanReg.Name = newName + + server.saveChannelNoMutex(tx, casefoldedNewName, *chanReg) + return nil + }) + + // send RENAME messages + for mcl := range channel.members { + if mcl.capabilities[Rename] { + mcl.Send(nil, client.nickMaskString, "RENAME", oldName, newName, reason) + } else { + mcl.Send(nil, mcl.nickMaskString, "PART", oldName, fmt.Sprintf("Channel renamed: %s", reason)) + if mcl.capabilities[ExtendedJoin] { + accountName := "*" + if mcl.account != nil { + accountName = mcl.account.Name + } + mcl.Send(nil, mcl.nickMaskString, "JOIN", newName, accountName, mcl.realname) + } else { + mcl.Send(nil, mcl.nickMaskString, "JOIN", newName) + } + } + } + + return false +} + // JOIN {,} [{,}] // JOIN 0 func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {