From 2428acab955826b0b354c093e703ae539ed3dc03 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 19 Mar 2020 17:09:52 -0400 Subject: [PATCH] enhancements to roleplay --- irc/config.go | 39 +++++++++++++------------ irc/handlers.go | 8 +++--- irc/roleplay.go | 72 ++++++++++++++++++++++++++++++++++------------- irc/utils/args.go | 7 +++++ oragono.yaml | 18 ++++++++++++ 5 files changed, 101 insertions(+), 43 deletions(-) diff --git a/irc/config.go b/irc/config.go index 6912df7c..94689f71 100644 --- a/irc/config.go +++ b/irc/config.go @@ -507,6 +507,15 @@ type Config struct { Casemapping Casemapping } + Roleplay struct { + Enabled *bool + enabled bool + RequireChanops bool `yaml:"require-chanops"` + RequireOper bool `yaml:"require-oper"` + AddSuffix *bool `yaml:"add-suffix"` + addSuffix bool + } + Languages struct { Enabled bool Path string @@ -844,12 +853,7 @@ func LoadConfig(filename string) (config *Config, err error) { // set this even if STS is disabled config.Server.capValues[caps.STS] = config.Server.STS.Value() - // lookup-hostnames defaults to true if unset - if config.Server.LookupHostnames != nil { - config.Server.lookupHostnames = *config.Server.LookupHostnames - } else { - config.Server.lookupHostnames = true - } + config.Server.lookupHostnames = utils.BoolDefaultTrue(config.Server.LookupHostnames) // process webirc blocks var newWebIRC []webircConfig @@ -1014,12 +1018,7 @@ func LoadConfig(filename string) (config *Config, err error) { } config.Server.capValues[caps.Languages] = config.languageManager.CapValue() - // RecoverFromErrors defaults to true - if config.Debug.RecoverFromErrors != nil { - config.Debug.recoverFromErrors = *config.Debug.RecoverFromErrors - } else { - config.Debug.recoverFromErrors = true - } + config.Debug.recoverFromErrors = utils.BoolDefaultTrue(config.Debug.RecoverFromErrors) // process operator definitions, store them to config.operators operclasses, err := config.OperatorClasses() @@ -1053,12 +1052,7 @@ func LoadConfig(filename string) (config *Config, err error) { config.Channels.Registration.MaxChannelsPerAccount = 15 } - forceTrailingPtr := config.Server.Compatibility.ForceTrailing - if forceTrailingPtr != nil { - config.Server.Compatibility.forceTrailing = *forceTrailingPtr - } else { - config.Server.Compatibility.forceTrailing = true - } + config.Server.Compatibility.forceTrailing = utils.BoolDefaultTrue(config.Server.Compatibility.ForceTrailing) config.loadMOTD() @@ -1080,6 +1074,9 @@ func LoadConfig(filename string) (config *Config, err error) { config.History.ZNCMax = config.History.ChathistoryMax } + config.Roleplay.enabled = utils.BoolDefaultTrue(config.Roleplay.Enabled) + config.Roleplay.addSuffix = utils.BoolDefaultTrue(config.Roleplay.AddSuffix) + config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime) config.Server.Cloaks.Initialize() @@ -1133,8 +1130,10 @@ func (config *Config) generateISupport() (err error) { isupport.Add("NETWORK", config.Network.Name) isupport.Add("NICKLEN", strconv.Itoa(config.Limits.NickLen)) isupport.Add("PREFIX", "(qaohv)~&@%+") - isupport.Add("RPCHAN", "E") - isupport.Add("RPUSER", "E") + if config.Roleplay.enabled { + isupport.Add("RPCHAN", "E") + isupport.Add("RPUSER", "E") + } isupport.Add("STATUSMSG", "~&@%+") isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:", maxTargetsString, maxTargetsString, maxTargetsString)) isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen)) diff --git a/irc/handlers.go b/irc/handlers.go index 3a78261e..b49b5159 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2027,7 +2027,7 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi func npcHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { target := msg.Params[0] fakeSource := msg.Params[1] - message := msg.Params[2] + message := msg.Params[2:] _, err := CasefoldName(fakeSource) if err != nil { @@ -2046,7 +2046,7 @@ func npcHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { target := msg.Params[0] fakeSource := msg.Params[1] - message := msg.Params[2] + message := msg.Params[2:] sourceString := fmt.Sprintf(npcNickMask, fakeSource, client.nick) _, err := CasefoldName(fakeSource) @@ -2231,7 +2231,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(oldName), client.t("No such channel")) return false } - if !(channel.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("chanreg")) { + if !(channel.ClientIsAtLeast(client, modes.ChannelOperator) || client.HasRoleCapabs("chanreg")) { rb.Add(nil, server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), oldName, client.t("You're not a channel operator")) return false } @@ -2334,7 +2334,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re // SCENE func sceneHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { target := msg.Params[0] - message := msg.Params[1] + message := msg.Params[1:] sourceString := fmt.Sprintf(sceneNickMask, client.nick) sendRoleplayMessage(server, client, sourceString, target, false, message, rb) diff --git a/irc/roleplay.go b/irc/roleplay.go index 37790225..3e89d11c 100644 --- a/irc/roleplay.go +++ b/irc/roleplay.go @@ -4,10 +4,11 @@ package irc import ( - "fmt" + "bytes" - "github.com/oragono/oragono/irc/caps" + "github.com/oragono/oragono/irc/history" "github.com/oragono/oragono/irc/modes" + "github.com/oragono/oragono/irc/utils" ) const ( @@ -15,17 +16,39 @@ const ( sceneNickMask = "=Scene=!%s@npc.fakeuser.invalid" ) -func sendRoleplayMessage(server *Server, client *Client, source string, targetString string, isAction bool, message string, rb *ResponseBuffer) { - if isAction { - message = fmt.Sprintf("\x01ACTION %s (%s)\x01", message, client.nick) - } else { - // block attempts to send CTCP messages to Tor clients - // TODO(#395) clean this up - if len(message) != 0 && message[0] == '\x01' { - return - } - message = fmt.Sprintf("%s (%s)", message, client.nick) +func sendRoleplayMessage(server *Server, client *Client, source string, targetString string, isAction bool, messageParts []string, rb *ResponseBuffer) { + config := server.Config() + if !config.Roleplay.enabled { + rb.Add(nil, client.server.name, ERR_CANNOTSENDRP, targetString, client.t("Roleplaying has been disabled by the server administrators")) + return } + if config.Roleplay.RequireOper && !client.HasRoleCapabs("roleplay") { + rb.Add(nil, client.server.name, ERR_CANNOTSENDRP, targetString, client.t("Insufficient privileges")) + return + } + + // block attempts to send CTCP messages to Tor clients + if len(messageParts) > 0 && len(messageParts[0]) > 0 && messageParts[0][0] == '\x01' { + return + } + + var buf bytes.Buffer + if isAction { + buf.WriteString("\x01ACTION ") + } + for i, part := range messageParts { + buf.WriteString(part) + if i != len(messageParts)-1 { + buf.WriteByte(' ') + } + } + if config.Roleplay.addSuffix { + buf.WriteString(" (") + buf.WriteString(client.Nick()) + buf.WriteString(")") + } + + splitMessage := utils.MakeMessage(buf.String()) target, cerr := CasefoldChannel(targetString) if cerr == nil { @@ -35,13 +58,19 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt return } + targetString = channel.Name() if !channel.CanSpeak(client) { - rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) + rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, targetString, client.t("Cannot send to channel")) return } if !channel.flags.HasMode(modes.ChanRoleplaying) { - rb.Add(nil, client.server.name, ERR_CANNOTSENDRP, channel.name, client.t("Channel doesn't have roleplaying mode available")) + rb.Add(nil, client.server.name, ERR_CANNOTSENDRP, targetString, client.t("Channel doesn't have roleplaying mode available")) + return + } + + if config.Roleplay.RequireChanops && !channel.ClientIsAtLeast(client, modes.ChannelOperator) { + rb.Add(nil, client.server.name, ERR_CANNOTSENDRP, targetString, client.t("Insufficient privileges")) return } @@ -51,12 +80,18 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt // of roleplay commands, so send them a copy whether they have echo-message // or not if rb.session == session { - rb.Add(nil, source, "PRIVMSG", channel.name, message) + rb.AddSplitMessageFromClient(source, "", nil, "PRIVMSG", targetString, splitMessage) } else { - session.Send(nil, source, "PRIVMSG", channel.name, message) + session.sendSplitMsgFromClientInternal(false, source, "", nil, "PRIVMSG", targetString, splitMessage) } } } + + channel.AddHistoryItem(history.Item{ + Type: history.Privmsg, + Message: splitMessage, + Nick: source, + }) } else { target, err := CasefoldName(targetString) user := server.clients.Get(target) @@ -72,9 +107,8 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt cnick := client.Nick() tnick := user.Nick() - user.Send(nil, source, "PRIVMSG", tnick, message) - if rb.session.capabilities.Has(caps.EchoMessage) { - rb.Add(nil, source, "PRIVMSG", tnick, message) + for _, session := range user.Sessions() { + session.sendSplitMsgFromClientInternal(false, source, "", nil, "PRIVMSG", tnick, splitMessage) } if user.Away() { //TODO(dan): possibly implement cooldown of away notifications to users diff --git a/irc/utils/args.go b/irc/utils/args.go index c33cd566..64475da1 100644 --- a/irc/utils/args.go +++ b/irc/utils/args.go @@ -82,3 +82,10 @@ func (err *IncompatibleSchemaError) Error() string { func NanoToTimestamp(nanotime int64) string { return time.Unix(0, nanotime).UTC().Format(IRCv3TimestampFormat) } + +func BoolDefaultTrue(value *bool) bool { + if value != nil { + return *value + } + return true +} diff --git a/oragono.yaml b/oragono.yaml index f57151e3..ed9f4201 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -492,6 +492,7 @@ oper-classes: - "local_ban" - "local_unban" - "nofakelag" + - "roleplay" # network operator "network-oper": @@ -702,6 +703,23 @@ fakelag: # sending any commands: cooldown: 2s +# the roleplay commands are semi-standardized extensions to IRC that allow +# sending and receiving messages from pseudo-nicknames. this can be used either +# for actual roleplaying, or for bridging IRC with other protocols. +roleplay: + # are roleplay commands enabled at all? (channels and clients still have to + # opt in individually with the +E mode) + enabled: true + + # require the "roleplay" oper capability to send roleplay messages? + require-oper: false + + # require channel operator permissions to send roleplay messages? + require-chanops: false + + # add the real nickname, in parentheses, to the end of every roleplay message? + add-suffix: true + # message history tracking, for the RESUME extension and possibly other uses in future history: # should we store messages for later playback?