mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-11 06:29:29 +01:00
Merge pull request #1120 from oragono/master+relaymsg
Initial RELAYMSG implementation
This commit is contained in:
commit
2d277566b5
@ -143,6 +143,19 @@ server:
|
|||||||
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
||||||
motd-formatting: true
|
motd-formatting: true
|
||||||
|
|
||||||
|
# relaying using the RELAYMSG command
|
||||||
|
relaymsg:
|
||||||
|
# is relaymsg enabled at all?
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# which character(s) are reserved for relayed nicks?
|
||||||
|
separators: "/"
|
||||||
|
|
||||||
|
# can channel operators use RELAYMSG in their channels?
|
||||||
|
# our implementation of RELAYMSG makes it safe for chanops to use without the
|
||||||
|
# possibility of real users being silently spoofed
|
||||||
|
available-to-chanops: true
|
||||||
|
|
||||||
# addresses/CIDRs the PROXY command can be used from
|
# addresses/CIDRs the PROXY command can be used from
|
||||||
# this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
|
# this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
|
||||||
# unless you have a good reason. you should also add these addresses to the
|
# unless you have a good reason. you should also add these addresses to the
|
||||||
@ -515,6 +528,7 @@ oper-classes:
|
|||||||
- "local_unban"
|
- "local_unban"
|
||||||
- "nofakelag"
|
- "nofakelag"
|
||||||
- "roleplay"
|
- "roleplay"
|
||||||
|
- "relaymsg-anywhere"
|
||||||
|
|
||||||
# network operator
|
# network operator
|
||||||
"network-oper":
|
"network-oper":
|
||||||
@ -734,7 +748,7 @@ fakelag:
|
|||||||
roleplay:
|
roleplay:
|
||||||
# are roleplay commands enabled at all? (channels and clients still have to
|
# are roleplay commands enabled at all? (channels and clients still have to
|
||||||
# opt in individually with the +E mode)
|
# opt in individually with the +E mode)
|
||||||
enabled: true
|
enabled: false
|
||||||
|
|
||||||
# require the "roleplay" oper capability to send roleplay messages?
|
# require the "roleplay" oper capability to send roleplay messages?
|
||||||
require-oper: false
|
require-oper: false
|
||||||
|
16
default.yaml
16
default.yaml
@ -170,6 +170,19 @@ server:
|
|||||||
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
||||||
motd-formatting: true
|
motd-formatting: true
|
||||||
|
|
||||||
|
# relaying using the RELAYMSG command
|
||||||
|
relaymsg:
|
||||||
|
# is relaymsg enabled at all?
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# which character(s) are reserved for relayed nicks?
|
||||||
|
separators: "/"
|
||||||
|
|
||||||
|
# can channel operators use RELAYMSG in their channels?
|
||||||
|
# our implementation of RELAYMSG makes it safe for chanops to use without the
|
||||||
|
# possibility of real users being silently spoofed
|
||||||
|
available-to-chanops: true
|
||||||
|
|
||||||
# addresses/CIDRs the PROXY command can be used from
|
# addresses/CIDRs the PROXY command can be used from
|
||||||
# this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
|
# this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
|
||||||
# unless you have a good reason. you should also add these addresses to the
|
# unless you have a good reason. you should also add these addresses to the
|
||||||
@ -543,6 +556,7 @@ oper-classes:
|
|||||||
- "local_unban"
|
- "local_unban"
|
||||||
- "nofakelag"
|
- "nofakelag"
|
||||||
- "roleplay"
|
- "roleplay"
|
||||||
|
- "relaymsg-anywhere"
|
||||||
|
|
||||||
# network operator
|
# network operator
|
||||||
"network-oper":
|
"network-oper":
|
||||||
@ -762,7 +776,7 @@ fakelag:
|
|||||||
roleplay:
|
roleplay:
|
||||||
# are roleplay commands enabled at all? (channels and clients still have to
|
# are roleplay commands enabled at all? (channels and clients still have to
|
||||||
# opt in individually with the +E mode)
|
# opt in individually with the +E mode)
|
||||||
enabled: true
|
enabled: false
|
||||||
|
|
||||||
# require the "roleplay" oper capability to send roleplay messages?
|
# require the "roleplay" oper capability to send roleplay messages?
|
||||||
require-oper: false
|
require-oper: false
|
||||||
|
@ -93,6 +93,12 @@ CAPDEFS = [
|
|||||||
url="https://ircv3.net/specs/extensions/multi-prefix-3.1.html",
|
url="https://ircv3.net/specs/extensions/multi-prefix-3.1.html",
|
||||||
standard="IRCv3",
|
standard="IRCv3",
|
||||||
),
|
),
|
||||||
|
CapDef(
|
||||||
|
identifier="Relaymsg",
|
||||||
|
name="draft/relaymsg",
|
||||||
|
url="https://github.com/ircv3/ircv3-specifications/pull/417",
|
||||||
|
standard="proposed IRCv3",
|
||||||
|
),
|
||||||
CapDef(
|
CapDef(
|
||||||
identifier="Rename",
|
identifier="Rename",
|
||||||
name="draft/rename",
|
name="draft/rename",
|
||||||
|
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 26
|
numCapabs = 27
|
||||||
// length of the uint64 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
@ -53,6 +53,10 @@ const (
|
|||||||
// https://github.com/ircv3/ircv3-specifications/pull/398
|
// https://github.com/ircv3/ircv3-specifications/pull/398
|
||||||
Multiline Capability = iota
|
Multiline Capability = iota
|
||||||
|
|
||||||
|
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
|
||||||
|
// https://github.com/ircv3/ircv3-specifications/pull/417
|
||||||
|
Relaymsg Capability = iota
|
||||||
|
|
||||||
// Rename is the proposed IRCv3 capability named "draft/rename":
|
// Rename is the proposed IRCv3 capability named "draft/rename":
|
||||||
// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
|
// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
|
||||||
Rename Capability = iota
|
Rename Capability = iota
|
||||||
@ -131,6 +135,7 @@ var (
|
|||||||
"draft/event-playback",
|
"draft/event-playback",
|
||||||
"draft/languages",
|
"draft/languages",
|
||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
|
"draft/relaymsg",
|
||||||
"draft/rename",
|
"draft/rename",
|
||||||
"draft/resume-0.5",
|
"draft/resume-0.5",
|
||||||
"echo-message",
|
"echo-message",
|
||||||
|
@ -165,6 +165,10 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
|||||||
return "", errNicknameInvalid, false
|
return "", errNicknameInvalid, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.isRelaymsgIdentifier(newNick) {
|
||||||
|
return "", errNicknameInvalid, false
|
||||||
|
}
|
||||||
|
|
||||||
if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
|
if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
|
||||||
return "", errNicknameInvalid, false
|
return "", errNicknameInvalid, false
|
||||||
}
|
}
|
||||||
|
@ -252,6 +252,10 @@ func init() {
|
|||||||
minParams: 2,
|
minParams: 2,
|
||||||
allowedInBatch: true,
|
allowedInBatch: true,
|
||||||
},
|
},
|
||||||
|
"RELAYMSG": {
|
||||||
|
handler: relaymsgHandler,
|
||||||
|
minParams: 3,
|
||||||
|
},
|
||||||
"RENAME": {
|
"RENAME": {
|
||||||
handler: renameHandler,
|
handler: renameHandler,
|
||||||
minParams: 2,
|
minParams: 2,
|
||||||
|
@ -502,6 +502,11 @@ type Config struct {
|
|||||||
MOTD string
|
MOTD string
|
||||||
motdLines []string
|
motdLines []string
|
||||||
MOTDFormatting bool `yaml:"motd-formatting"`
|
MOTDFormatting bool `yaml:"motd-formatting"`
|
||||||
|
Relaymsg struct {
|
||||||
|
Enabled bool
|
||||||
|
Separators string
|
||||||
|
AvailableToChanops bool `yaml:"available-to-chanops"`
|
||||||
|
}
|
||||||
ProxyAllowedFrom []string `yaml:"proxy-allowed-from"`
|
ProxyAllowedFrom []string `yaml:"proxy-allowed-from"`
|
||||||
proxyAllowedFromNets []net.IPNet
|
proxyAllowedFromNets []net.IPNet
|
||||||
WebIRC []webircConfig `yaml:"webirc"`
|
WebIRC []webircConfig `yaml:"webirc"`
|
||||||
@ -1106,6 +1111,17 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
}
|
}
|
||||||
config.Server.capValues[caps.Languages] = config.languageManager.CapValue()
|
config.Server.capValues[caps.Languages] = config.languageManager.CapValue()
|
||||||
|
|
||||||
|
if config.Server.Relaymsg.Enabled {
|
||||||
|
for _, char := range protocolBreakingNameCharacters {
|
||||||
|
if strings.ContainsRune(config.Server.Relaymsg.Separators, char) {
|
||||||
|
return nil, fmt.Errorf("RELAYMSG separators cannot include the characters %s", protocolBreakingNameCharacters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.Server.capValues[caps.Relaymsg] = config.Server.Relaymsg.Separators
|
||||||
|
} else {
|
||||||
|
config.Server.supportedCaps.Disable(caps.Relaymsg)
|
||||||
|
}
|
||||||
|
|
||||||
config.Debug.recoverFromErrors = utils.BoolDefaultTrue(config.Debug.RecoverFromErrors)
|
config.Debug.recoverFromErrors = utils.BoolDefaultTrue(config.Debug.RecoverFromErrors)
|
||||||
|
|
||||||
// process operator definitions, store them to config.operators
|
// process operator definitions, store them to config.operators
|
||||||
@ -1206,6 +1222,19 @@ func (config *Config) getOutputPath(filename string) string {
|
|||||||
return filepath.Join(config.Server.OutputPath, filename)
|
return filepath.Join(config.Server.OutputPath, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) isRelaymsgIdentifier(nick string) bool {
|
||||||
|
if !config.Server.Relaymsg.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, char := range config.Server.Relaymsg.Separators {
|
||||||
|
if strings.ContainsRune(nick, char) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// setISupport sets up our RPL_ISUPPORT reply.
|
// setISupport sets up our RPL_ISUPPORT reply.
|
||||||
func (config *Config) generateISupport() (err error) {
|
func (config *Config) generateISupport() (err error) {
|
||||||
maxTargetsString := strconv.Itoa(maxTargets)
|
maxTargetsString := strconv.Itoa(maxTargets)
|
||||||
|
@ -2013,6 +2013,16 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
if i == maxTargets {
|
if i == maxTargets {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config := server.Config()
|
||||||
|
if config.isRelaymsgIdentifier(targetString) {
|
||||||
|
if histType == history.Privmsg {
|
||||||
|
rb.Add(nil, server.name, ERR_NOSUCHNICK, client.Nick(), targetString, client.t("Relayed users cannot receive private messages"))
|
||||||
|
}
|
||||||
|
// TAGMSG/NOTICEs are intentionally silently dropped
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// each target gets distinct msgids
|
// each target gets distinct msgids
|
||||||
splitMsg := utils.MakeMessage(message)
|
splitMsg := utils.MakeMessage(message)
|
||||||
dispatchMessageToTarget(client, clientOnlyTags, histType, msg.Command, targetString, splitMsg, rb)
|
dispatchMessageToTarget(client, clientOnlyTags, histType, msg.Command, targetString, splitMsg, rb)
|
||||||
@ -2421,6 +2431,72 @@ func rehashHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RELAYMSG <channel> <spoofed nick> :<message>
|
||||||
|
func relaymsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (result bool) {
|
||||||
|
config := server.Config()
|
||||||
|
if !config.Server.Relaymsg.Enabled {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "NOT_ENABLED", client.t("RELAYMSG has been disabled"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := server.channels.Get(msg.Params[0])
|
||||||
|
if channel == nil {
|
||||||
|
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(msg.Params[0]), client.t("No such channel"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedToRelay := client.HasRoleCapabs("relaymsg-anywhere") || (config.Server.Relaymsg.AvailableToChanops && channel.ClientIsAtLeast(client, modes.ChannelOperator))
|
||||||
|
if !allowedToRelay {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "NOT_PRIVED", client.t("You cannot relay messages to this channel"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMessage := msg.Params[2]
|
||||||
|
if strings.TrimSpace(rawMessage) == "" {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "BLANK_MSG", client.t("The message must not be blank"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
message := utils.MakeMessage(rawMessage)
|
||||||
|
|
||||||
|
nick := msg.Params[1]
|
||||||
|
_, err := CasefoldName(nick)
|
||||||
|
if err != nil {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "INVALID_NICK", client.t("Invalid nickname"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !config.isRelaymsgIdentifier(nick) {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "INVALID_NICK", fmt.Sprintf(client.t("Relayed nicknames MUST contain a relaymsg separator from this set: %s"), config.Server.Relaymsg.Separators))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.AddHistoryItem(history.Item{
|
||||||
|
Type: history.Privmsg,
|
||||||
|
Message: message,
|
||||||
|
Nick: nick,
|
||||||
|
}, "")
|
||||||
|
|
||||||
|
// send msg
|
||||||
|
channelName := channel.Name()
|
||||||
|
relayTags := map[string]string{
|
||||||
|
"relaymsg": client.Nick(),
|
||||||
|
}
|
||||||
|
for _, member := range channel.Members() {
|
||||||
|
for _, session := range member.Sessions() {
|
||||||
|
var tagsToUse map[string]string
|
||||||
|
if session.capabilities.Has(caps.Relaymsg) {
|
||||||
|
tagsToUse = relayTags
|
||||||
|
}
|
||||||
|
|
||||||
|
if session == rb.session {
|
||||||
|
rb.AddSplitMessageFromClient(nick, "*", tagsToUse, "PRIVMSG", channelName, message)
|
||||||
|
} else {
|
||||||
|
session.sendSplitMsgFromClientInternal(false, nick, "*", tagsToUse, "PRIVMSG", channelName, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// RENAME <oldchan> <newchan> [<reason>]
|
// RENAME <oldchan> <newchan> [<reason>]
|
||||||
func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
oldName, newName := msg.Params[0], msg.Params[1]
|
oldName, newName := msg.Params[0], msg.Params[1]
|
||||||
|
10
irc/help.go
10
irc/help.go
@ -417,6 +417,16 @@ Replies to a PING. Used to check link connectivity.`,
|
|||||||
text: `PRIVMSG <target>{,<target>} <text to be sent>
|
text: `PRIVMSG <target>{,<target>} <text to be sent>
|
||||||
|
|
||||||
Sends the text to the given targets as a PRIVMSG.`,
|
Sends the text to the given targets as a PRIVMSG.`,
|
||||||
|
},
|
||||||
|
"relaymsg": {
|
||||||
|
text: `RELAYMSG <channel> <spoofed nick> :<message>
|
||||||
|
|
||||||
|
This command lets channel operators relay messages to their
|
||||||
|
channel from other messaging systems using relay bots. The
|
||||||
|
spoofed nickname MUST contain a forwardslash.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
RELAYMSG #ircv3 Mallory/D :Welp, we linked Discord...`,
|
||||||
},
|
},
|
||||||
"rename": {
|
"rename": {
|
||||||
text: `RENAME <channel> <newname> [<reason>]
|
text: `RENAME <channel> <newname> [<reason>]
|
||||||
|
@ -82,7 +82,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
|||||||
if rb.session == session {
|
if rb.session == session {
|
||||||
rb.AddSplitMessageFromClient(source, "", nil, "PRIVMSG", targetString, splitMessage)
|
rb.AddSplitMessageFromClient(source, "", nil, "PRIVMSG", targetString, splitMessage)
|
||||||
} else {
|
} else {
|
||||||
session.sendSplitMsgFromClientInternal(false, source, "", nil, "PRIVMSG", targetString, splitMessage)
|
session.sendSplitMsgFromClientInternal(false, source, "*", nil, "PRIVMSG", targetString, splitMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,7 +108,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
|||||||
cnick := client.Nick()
|
cnick := client.Nick()
|
||||||
tnick := user.Nick()
|
tnick := user.Nick()
|
||||||
for _, session := range user.Sessions() {
|
for _, session := range user.Sessions() {
|
||||||
session.sendSplitMsgFromClientInternal(false, source, "", nil, "PRIVMSG", tnick, splitMessage)
|
session.sendSplitMsgFromClientInternal(false, source, "*", nil, "PRIVMSG", tnick, splitMessage)
|
||||||
}
|
}
|
||||||
if away, awayMessage := user.Away(); away {
|
if away, awayMessage := user.Away(); away {
|
||||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||||
|
@ -485,6 +485,10 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
|||||||
return fmt.Errorf("UTF-8 enforcement cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("UTF-8 enforcement cannot be changed after launching the server, rehash aborted")
|
||||||
} else if oldConfig.Accounts.Multiclient.AlwaysOn != config.Accounts.Multiclient.AlwaysOn {
|
} else if oldConfig.Accounts.Multiclient.AlwaysOn != config.Accounts.Multiclient.AlwaysOn {
|
||||||
return fmt.Errorf("Default always-on setting cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Default always-on setting cannot be changed after launching the server, rehash aborted")
|
||||||
|
} else if oldConfig.Server.Relaymsg.Enabled != config.Server.Relaymsg.Enabled {
|
||||||
|
return fmt.Errorf("Cannot enable or disable relaying after launching the server, rehash aborted")
|
||||||
|
} else if oldConfig.Server.Relaymsg.Separators != config.Server.Relaymsg.Separators {
|
||||||
|
return fmt.Errorf("Cannot change relaying separators after launching the server, rehash aborted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,16 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
precisUTF8MappingToken = "rfc8265"
|
precisUTF8MappingToken = "rfc8265"
|
||||||
|
|
||||||
|
// space can't be used
|
||||||
|
// , is used as a separator
|
||||||
|
// * is used in mask matching
|
||||||
|
// ? is used in mask matching
|
||||||
|
// . denotes a server name
|
||||||
|
// ! separates nickname from username
|
||||||
|
// @ separates username from hostname
|
||||||
|
// : means trailing
|
||||||
|
protocolBreakingNameCharacters = " ,*?.!@:"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -138,18 +148,10 @@ func CasefoldName(name string) (string, error) {
|
|||||||
return "", errStringIsEmpty
|
return "", errStringIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// space can't be used
|
|
||||||
// , is used as a separator
|
|
||||||
// * is used in mask matching
|
|
||||||
// ? is used in mask matching
|
|
||||||
// . denotes a server name
|
|
||||||
// ! separates nickname from username
|
|
||||||
// @ separates username from hostname
|
|
||||||
// : means trailing
|
|
||||||
// # is a channel prefix
|
// # is a channel prefix
|
||||||
// ~&@%+ are channel membership prefixes
|
// ~&@%+ are channel membership prefixes
|
||||||
// - I feel like disallowing
|
// - I feel like disallowing
|
||||||
if strings.ContainsAny(lowered, " ,*?.!@:") || strings.ContainsAny(string(lowered[0]), "#~&@%+-") {
|
if strings.ContainsAny(lowered, protocolBreakingNameCharacters) || strings.ContainsAny(string(lowered[0]), "#~&@%+-") {
|
||||||
return "", errInvalidCharacter
|
return "", errInvalidCharacter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user