diff --git a/irc/channel.go b/irc/channel.go index 247e7cba..ff299447 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -371,17 +371,22 @@ func (channel *Channel) CanSpeak(client *Client) bool { return true } +// TagMsg sends a tag message to everyone in this channel who can accept them. +func (channel *Channel) TagMsg(minPrefix *ChannelMode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client) { + channel.sendMessage("TAGMSG", []Capability{MessageTags}, minPrefix, clientOnlyTags, client, nil) +} + // PrivMsg sends a private message to everyone in this channel. func (channel *Channel) PrivMsg(minPrefix *ChannelMode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message string) { - channel.sendMessage("PRIVMSG", minPrefix, clientOnlyTags, client, message) + channel.sendMessage("PRIVMSG", nil, minPrefix, clientOnlyTags, client, &message) } // Notice sends a private message to everyone in this channel. func (channel *Channel) Notice(minPrefix *ChannelMode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message string) { - channel.sendMessage("NOTICE", minPrefix, clientOnlyTags, client, message) + channel.sendMessage("NOTICE", nil, minPrefix, clientOnlyTags, client, &message) } -func (channel *Channel) sendMessage(cmd string, minPrefix *ChannelMode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message string) { +func (channel *Channel) sendMessage(cmd string, requiredCaps []Capability, minPrefix *ChannelMode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *string) { if !channel.CanSpeak(client) { client.Send(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, "Cannot send to channel") return @@ -403,10 +408,29 @@ func (channel *Channel) sendMessage(cmd string, minPrefix *ChannelMode, clientOn if member == client && !client.capabilities[EchoMessage] { continue } + + canReceive := true + for _, capName := range requiredCaps { + if !member.capabilities[capName] { + canReceive = false + } + } + if !canReceive { + continue + } + if member.capabilities[MessageTags] { - member.SendFromClient(client, clientOnlyTags, client.nickMaskString, cmd, channel.name, message) + if message == nil { + member.SendFromClient(client, clientOnlyTags, client.nickMaskString, cmd, channel.name) + } else { + member.SendFromClient(client, clientOnlyTags, client.nickMaskString, cmd, channel.name, *message) + } } else { - member.SendFromClient(client, nil, client.nickMaskString, cmd, channel.name, message) + if message == nil { + member.SendFromClient(client, nil, client.nickMaskString, cmd, channel.name) + } else { + member.SendFromClient(client, nil, client.nickMaskString, cmd, channel.name, *message) + } } } } diff --git a/irc/commands.go b/irc/commands.go index b3bb056c..ee27a5bd 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -191,6 +191,10 @@ var Commands = map[string]Command{ handler: sceneHandler, minParams: 2, }, + "TAGMSG": { + handler: tagmsgHandler, + minParams: 1, + }, "QUIT": { handler: quitHandler, usablePreReg: true, diff --git a/irc/help.go b/irc/help.go index e0d726c8..91ed9b07 100644 --- a/irc/help.go +++ b/irc/help.go @@ -300,6 +300,12 @@ Gives the given user a new nickname.`, text: `SCENE The SCENE command is used to send a scene notification to the given target.`, + }, + "tagmsg": { + text: `@+client-only-tags TAGMSG {,} + +Sends the given client-only tags to the given targets as a TAGMSG. See the IRCv3 +specs for more info: http://ircv3.net/specs/core/message-tags-3.3.html`, }, "quit": { text: `QUIT [reason] diff --git a/irc/message_tags.go b/irc/message_tags.go index 7ef5496e..b45a67ac 100644 --- a/irc/message_tags.go +++ b/irc/message_tags.go @@ -19,5 +19,9 @@ func GetClientOnlyTags(tags map[string]ircmsg.TagValue) *map[string]ircmsg.TagVa } } + if len(clientOnlyTags) < 1 { + return nil + } + return &clientOnlyTags } diff --git a/irc/server.go b/irc/server.go index ed585721..da7860b4 100644 --- a/irc/server.go +++ b/irc/server.go @@ -338,7 +338,7 @@ func (server *Server) setISupport() { 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("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:", maxTargetsString, maxTargetsString, maxTargetsString)) server.isupport.Add("TOPICLEN", strconv.Itoa(server.limits.TopicLen)) // account registration @@ -963,6 +963,63 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool return false } +// TAGMSG {,} +func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + clientOnlyTags := GetClientOnlyTags(msg.Tags) + // no client-only tags, so we can drop it + if clientOnlyTags == nil { + return false + } + + targets := strings.Split(msg.Params[0], ",") + + for i, targetString := range targets { + // max of four targets per privmsg + if i > maxTargets-1 { + break + } + prefixes, targetString := SplitChannelMembershipPrefixes(targetString) + lowestPrefix := GetLowestChannelModePrefix(prefixes) + + // eh, no need to notify them + if len(targetString) < 1 { + continue + } + + target, err := CasefoldChannel(targetString) + if err == nil { + channel := server.channels.Get(target) + if channel == nil { + client.Send(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, targetString, "No such channel") + continue + } + channel.TagMsg(lowestPrefix, clientOnlyTags, client) + } else { + target, err = CasefoldName(targetString) + user := server.clients.Get(target) + if err != nil || user == nil { + if len(target) > 0 { + client.Send(nil, server.name, ERR_NOSUCHNICK, target, "No such nick") + } + continue + } + // end user can't receive tagmsgs + if !user.capabilities[MessageTags] { + continue + } + user.SendFromClient(client, clientOnlyTags, "TAGMSG", user.nick) + if client.capabilities[EchoMessage] { + client.SendFromClient(client, clientOnlyTags, "TAGMSG", user.nick) + } + 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) + } + } + } + return false +} + func (client *Client) WhoisChannelsNames(target *Client) []string { isMultiPrefix := target.capabilities[MultiPrefix] var chstrs []string