diff --git a/CHANGELOG.md b/CHANGELOG.md index c447cef4..375e8968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ New release of Oragono! * Length of channel mode lists (ban / ban-except / invite-except) is now restricted to the limit in config. * Support `MAXLIST`, `MAXTARGETS`, `MODES`, `TARGMAX` in `RPL_ISUPPORT`. * Added support for IRCv3 capability [`chghost`](http://ircv3.net/specs/extensions/chghost-3.2.html). +* Roleplaying commands, both inside channels and between clients. ### Changed * In the config file, "operator" changed to "opers", and new oper class is required. diff --git a/irc/client.go b/irc/client.go index 22963788..c0d019f7 100644 --- a/irc/client.go +++ b/irc/client.go @@ -461,6 +461,8 @@ func (client *Client) Send(tags *map[string]ircmsg.TagValue, prefix string, comm line, err := message.Line() if err != nil { // try not to fail quietly - especially useful when running tests, as a note to dig deeper + // log.Println("Error assembling message:") + // spew.Dump(message) message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending") line, _ := message.Line() client.socket.Write(line) diff --git a/irc/commands.go b/irc/commands.go index b59b176d..7ee7b63b 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -54,6 +54,10 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b // Commands holds all commands executable by a client connected to us. var Commands = map[string]Command{ + "AMBIANCE": { + handler: sceneHandler, + minParams: 2, + }, "AUTHENTICATE": { handler: authenticateHandler, usablePreReg: true, @@ -127,6 +131,14 @@ var Commands = map[string]Command{ handler: noticeHandler, minParams: 2, }, + "NPC": { + handler: npcHandler, + minParams: 3, + }, + "NPCA": { + handler: npcaHandler, + minParams: 3, + }, "OPER": { handler: operHandler, minParams: 2, @@ -161,6 +173,10 @@ var Commands = map[string]Command{ minParams: 2, oper: true, }, + "SCENE": { + handler: sceneHandler, + minParams: 2, + }, "QUIT": { handler: quitHandler, usablePreReg: true, diff --git a/irc/help.go b/irc/help.go index a0a1b27c..d96b5405 100644 --- a/irc/help.go +++ b/irc/help.go @@ -60,6 +60,11 @@ Oragono supports the following user modes: // Help contains the help strings distributed with the IRCd. var Help = map[string]HelpEntry{ // Commands + "ambiance": { + text: `AMBIANCE + +The AMBIANCE command is used to send a scene notification to the given target.`, + }, "authenticate": { text: `AUTHENTICATE @@ -182,6 +187,16 @@ Sets your nickname to the new given one.`, text: `NOTICE {,} Sends the text to the given targets as a NOTICE.`, + }, + "npc": { + text: `NPC + +The NPC command is used to send a message to the target as the source.`, + }, + "npca": { + text: `NPCA + +The NPC command is used to send an action to the target as the source.`, }, "oper": { text: `OPER @@ -219,6 +234,11 @@ Sends the text to the given targets as a PRIVMSG.`, text: `SANICK Gives the given user a new nickname.`, + }, + "scene": { + text: `SCENE + +The SCENE command is used to send a scene notification to the given target.`, }, "quit": { text: `QUIT [reason] diff --git a/irc/modes.go b/irc/modes.go index aa1bdf5f..8baf51af 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -137,35 +137,37 @@ const ( ) const ( - Away UserMode = 'a' - Invisible UserMode = 'i' - LocalOperator UserMode = 'O' - Operator UserMode = 'o' - Restricted UserMode = 'r' - ServerNotice UserMode = 's' // deprecated - TLS UserMode = 'Z' - WallOps UserMode = 'w' + Away UserMode = 'a' + Invisible UserMode = 'i' + LocalOperator UserMode = 'O' + Operator UserMode = 'o' + Restricted UserMode = 'r' + ServerNotice UserMode = 's' // deprecated + TLS UserMode = 'Z' + UserRoleplaying UserMode = 'E' + WallOps UserMode = 'w' ) var ( SupportedUserModes = UserModes{ - Away, Invisible, Operator, + Away, Invisible, Operator, UserRoleplaying, } // supportedUserModesString acts as a cache for when we introduce users supportedUserModesString = SupportedUserModes.String() ) const ( - BanMask ChannelMode = 'b' // arg - ExceptMask ChannelMode = 'e' // arg - InviteMask ChannelMode = 'I' // arg - InviteOnly ChannelMode = 'i' // flag - Key ChannelMode = 'k' // flag arg - Moderated ChannelMode = 'm' // flag - NoOutside ChannelMode = 'n' // flag - OpOnlyTopic ChannelMode = 't' // flag - Secret ChannelMode = 's' // flag - UserLimit ChannelMode = 'l' // flag arg + BanMask ChannelMode = 'b' // arg + ChanRoleplaying ChannelMode = 'E' // flag + ExceptMask ChannelMode = 'e' // arg + InviteMask ChannelMode = 'I' // arg + InviteOnly ChannelMode = 'i' // flag + Key ChannelMode = 'k' // flag arg + Moderated ChannelMode = 'm' // flag + NoOutside ChannelMode = 'n' // flag + OpOnlyTopic ChannelMode = 't' // flag + Secret ChannelMode = 's' // flag + UserLimit ChannelMode = 'l' // flag arg ) var ( @@ -177,7 +179,7 @@ var ( SupportedChannelModes = ChannelModes{ BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside, - OpOnlyTopic, Secret, UserLimit, + OpOnlyTopic, Secret, UserLimit, ChanRoleplaying, } // supportedChannelModesString acts as a cache for when we introduce users supportedChannelModesString = SupportedChannelModes.String() @@ -297,7 +299,7 @@ func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { for _, change := range changes { switch change.mode { - case Invisible, ServerNotice, WallOps: + case Invisible, ServerNotice, WallOps, UserRoleplaying: switch change.op { case Add: if target.flags[change.mode] { @@ -471,7 +473,7 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } applied = append(applied, change) - case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Secret: + case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Secret, ChanRoleplaying: switch change.op { case Add: if channel.flags[change.mode] { diff --git a/irc/nickname.go b/irc/nickname.go index a53d9545..6a34b9ef 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -25,7 +25,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return false } - if err != nil || len(nicknameRaw) > server.limits.NickLen { + if err != nil || len(nicknameRaw) > server.limits.NickLen || nickname == "=scene=" { client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, nicknameRaw, "Erroneous nickname") return false } @@ -59,14 +59,14 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } oldnick, oerr := CasefoldName(msg.Params[0]) - casefoldedNickname, err := CasefoldName(msg.Params[1]) + nickname, err := CasefoldName(msg.Params[1]) - if len(casefoldedNickname) < 1 { + if len(nickname) < 1 { client.Send(nil, server.name, ERR_NONICKNAMEGIVEN, client.nick, "No nickname given") return false } - if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen { + if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen || nickname == "=scene=" { client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, msg.Params[0], "Erroneous nickname") return false } @@ -82,7 +82,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } //TODO(dan): There's probably some races here, we should be changing this in the primary server thread - if server.clients.Get(casefoldedNickname) != nil || server.clients.Get(casefoldedNickname) != target { + if server.clients.Get(nickname) != nil || server.clients.Get(nickname) != target { client.Send(nil, server.name, ERR_NICKNAMEINUSE, client.nick, msg.Params[0], "Nickname is already in use") return false } diff --git a/irc/roleplay.go b/irc/roleplay.go new file mode 100644 index 00000000..8d23d891 --- /dev/null +++ b/irc/roleplay.go @@ -0,0 +1,94 @@ +// Copyright (c) 2016- Daniel Oaks +// released under the MIT license + +package irc + +import ( + "fmt" + + "github.com/DanielOaks/girc-go/ircmsg" +) + +const ( + npcNickMask = "%s!%s@npc.fakeuser.invalid" + sceneNickMask = "=Scene=!%s@npc.fakeuser.invalid" +) + +// SCENE +func sceneHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + target := msg.Params[0] + message := msg.Params[1] + sourceString := fmt.Sprintf(sceneNickMask, client.nick) + + sendRoleplayMessage(server, client, sourceString, target, false, message) + + return false +} + +// NPC +func npcHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + target := msg.Params[0] + fakeSource := msg.Params[1] + message := msg.Params[2] + sourceString := fmt.Sprintf(npcNickMask, fakeSource, client.nick) + + sendRoleplayMessage(server, client, sourceString, target, false, message) + + return false +} + +// NPCA +func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + target := msg.Params[0] + fakeSource := msg.Params[1] + message := msg.Params[2] + sourceString := fmt.Sprintf(npcNickMask, fakeSource, client.nick) + + sendRoleplayMessage(server, client, sourceString, target, true, message) + + return false +} + +func sendRoleplayMessage(server *Server, client *Client, source string, targetString string, isAction bool, message string) { + if isAction { + message = fmt.Sprintf("\x01ACTION %s (%s)\x01", message, client.nick) + } else { + message = fmt.Sprintf("%s (%s)", message, client.nick) + } + + target, cerr := CasefoldChannel(targetString) + if cerr == nil { + channel := server.channels.Get(target) + if channel == nil { + client.Send(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, targetString, "No such channel") + return + } + + if !channel.CanSpeak(client) { + client.Send(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, "Cannot send to channel") + return + } + + for member := range channel.members { + if member == client && !client.capabilities[EchoMessage] { + continue + } + member.Send(nil, source, "PRIVMSG", channel.name, message) + } + } else { + target, err := CasefoldName(targetString) + user := server.clients.Get(target) + if err != nil || user == nil { + client.Send(nil, server.name, ERR_NOSUCHNICK, target, "No such nick") + return + } + user.Send(nil, source, "PRIVMSG", user.nick, message) + if client.capabilities[EchoMessage] { + client.Send(nil, source, "PRIVMSG", user.nick, message) + } + if user.flags[Away] { + //TODO(dan): possibly implement cooldown of away notifications to users + client.Send(nil, server.name, RPL_AWAY, user.nick, user.awayMessage) + } + } +} diff --git a/irc/server.go b/irc/server.go index 541add62..4d3f9023 100644 --- a/irc/server.go +++ b/irc/server.go @@ -264,7 +264,7 @@ func (server *Server) setISupport() { server.isupport = NewISupportList() server.isupport.Add("AWAYLEN", strconv.Itoa(server.limits.AwayLen)) server.isupport.Add("CASEMAPPING", "rfc7700") - server.isupport.Add("CHANMODES", strings.Join([]string{ChannelModes{BanMask, ExceptMask, InviteMask}.String(), "", ChannelModes{UserLimit, Key}.String(), ChannelModes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, Secret}.String()}, ",")) + server.isupport.Add("CHANMODES", strings.Join([]string{ChannelModes{BanMask, ExceptMask, InviteMask}.String(), "", ChannelModes{UserLimit, Key}.String(), ChannelModes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, ChanRoleplaying, Secret}.String()}, ",")) server.isupport.Add("CHANNELLEN", strconv.Itoa(server.limits.ChannelLen)) server.isupport.Add("CHANTYPES", "#") server.isupport.Add("EXCEPTS", "") @@ -277,6 +277,8 @@ func (server *Server) setISupport() { server.isupport.Add("NETWORK", server.networkName) server.isupport.Add("NICKLEN", strconv.Itoa(server.limits.NickLen)) server.isupport.Add("PREFIX", "(qaohv)~&@%+") + server.isupport.Add("RPCHAN", "E") + server.isupport.Add("RPUSER", "E") server.isupport.Add("STATUSMSG", "~&@%+") server.isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:%s,NOTICE:%s,MONITOR:", maxTargetsString, maxTargetsString)) server.isupport.Add("TOPICLEN", strconv.Itoa(server.limits.TopicLen)) @@ -1436,7 +1438,7 @@ func versionHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool target = msg.Params[0] } casefoldedTarget, err := Casefold(target) - if (target != "") && err != nil || (casefoldedTarget != server.nameCasefolded) { + if target != "" && (err != nil || casefoldedTarget != server.nameCasefolded) { client.Send(nil, server.name, ERR_NOSUCHSERVER, client.nick, target, "No such server") return false }