diff --git a/irc/caps/constants.go b/irc/caps/constants.go index f8a3d5d1a..2383e5c07 100644 --- a/irc/caps/constants.go +++ b/irc/caps/constants.go @@ -60,6 +60,8 @@ const ( MultilineConcatTag = "draft/multiline-concat" // draft/relaymsg: RelaymsgTagName = "draft/relaymsg" + // BOT mode: https://github.com/ircv3/ircv3-specifications/pull/439 + BotTagName = "draft/bot" ) func init() { diff --git a/irc/channel.go b/irc/channel.go index 99dde88ef..c32785dd1 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -729,6 +729,7 @@ func (channel *Channel) AddHistoryItem(item history.Item, account string) (err e // Join joins the given client to this channel (if they can be joined). func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) (joinErr error, forward string) { details := client.Details() + isBot := client.HasMode(modes.Bot) channel.stateMutex.RLock() chname := channel.name @@ -824,6 +825,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp Nick: details.nickMask, AccountName: details.accountName, Message: message, + IsBot: isBot, } histItem.Params[0] = details.realname channel.AddHistoryItem(histItem, details.account) @@ -840,7 +842,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp // cache the most common case (JOIN without extended-join) var cache MessageCache - cache.Initialize(channel.server, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname) + cache.Initialize(channel.server, message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname) isAway, awayMessage := client.Away() for _, member := range channel.Members() { if respectAuditorium { @@ -859,7 +861,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp continue } if session.capabilities.Has(caps.ExtendedJoin) { - session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname) + session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname, details.accountName, details.realname) } else { cache.Send(session) } @@ -867,15 +869,15 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick) } if isAway && session.capabilities.Has(caps.AwayNotify) { - session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, nil, "AWAY", awayMessage) + session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage) } } } if rb.session.capabilities.Has(caps.ExtendedJoin) { - rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname) + rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname, details.accountName, details.realname) } else { - rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname) + rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname) } if rb.session.client == client { @@ -988,6 +990,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) splitMessage := utils.MakeMessage(message) details := client.Details() + isBot := client.HasMode(modes.Bot) params := make([]string, 1, 2) params[0] = chname if message != "" { @@ -996,7 +999,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) respectAuditorium := channel.flags.HasMode(modes.Auditorium) && clientData.modes.HighestChannelUserMode() == modes.Mode(0) var cache MessageCache - cache.Initialize(channel.server, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...) + cache.Initialize(channel.server, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, isBot, nil, "PART", params...) for _, member := range channel.Members() { if respectAuditorium { channel.stateMutex.RLock() @@ -1010,10 +1013,10 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) cache.Send(session) } } - rb.AddFromClient(splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...) + rb.AddFromClient(splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, isBot, nil, "PART", params...) for _, session := range client.Sessions() { if session != rb.session { - session.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...) + session.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, isBot, nil, "PART", params...) } } @@ -1023,6 +1026,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) Nick: details.nickMask, AccountName: details.accountName, Message: splitMessage, + IsBot: isBot, }, details.account) } @@ -1133,19 +1137,19 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I nick := NUHToNick(item.Nick) switch item.Type { case history.Privmsg: - rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "PRIVMSG", chname, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "PRIVMSG", chname, item.Message) case history.Notice: - rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "NOTICE", chname, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "NOTICE", chname, item.Message) case history.Tagmsg: if eventPlayback { - rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "TAGMSG", chname, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "TAGMSG", chname, item.Message) } case history.Join: if eventPlayback { if extendedJoin { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname, item.AccountName, item.Params[0]) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "JOIN", chname, item.AccountName, item.Params[0]) } else { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "JOIN", chname) } } else { if !playJoinsAsPrivmsg { @@ -1157,48 +1161,48 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I } else { message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) } - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message) } case history.Part: if eventPlayback { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "PART", chname, item.Message.Message) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "PART", chname, item.Message.Message) } else { if !playJoinsAsPrivmsg { continue // #474 } message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message) } case history.Kick: if eventPlayback { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "KICK", chname, item.Params[0], item.Message.Message) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "KICK", chname, item.Params[0], item.Message.Message) } else { message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message) } case history.Quit: if eventPlayback { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "QUIT", item.Message.Message) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "QUIT", item.Message.Message) } else { if !playJoinsAsPrivmsg { continue // #474 } message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message) } case history.Nick: if eventPlayback { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "NICK", item.Params[0]) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "NICK", item.Params[0]) } else { message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0]) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message) } case history.Topic: if eventPlayback { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "TOPIC", chname, item.Message.Message) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "TOPIC", chname, item.Message.Message) } else { message := fmt.Sprintf(client.t("%[1]s set the channel topic to: %[2]s"), nick, item.Message.Message) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message) } case history.Mode: params := make([]string, len(item.Message.Split)+1) @@ -1207,10 +1211,10 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I params[i+1] = pair.Message } if eventPlayback { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "MODE", params...) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "MODE", params...) } else { message := fmt.Sprintf(client.t("%[1]s set channel modes: %[2]s"), nick, strings.Join(params[1:], " ")) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message) } } } @@ -1268,12 +1272,13 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe channel.stateMutex.Unlock() details := client.Details() + isBot := client.HasMode(modes.Bot) message := utils.MakeMessage(topic) - rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "TOPIC", chname, topic) + rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "TOPIC", chname, topic) for _, member := range channel.Members() { for _, session := range member.Sessions() { if session != rb.session { - session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "TOPIC", chname, topic) + session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "TOPIC", chname, topic) } } } @@ -1397,7 +1402,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod rb.addEchoMessage(clientOnlyTags, details.nickMask, details.accountName, command, chname, message) var cache MessageCache - cache.InitializeSplitMessage(channel.server, details.nickMask, details.accountName, clientOnlyTags, command, chname, message) + cache.InitializeSplitMessage(channel.server, details.nickMask, details.accountName, client.HasMode(modes.Bot), clientOnlyTags, command, chname, message) for _, member := range channel.Members() { if minPrefixMode != modes.Mode(0) && !channel.ClientIsAtLeast(member, minPrefixMode) { // STATUSMSG or OpModerated @@ -1519,23 +1524,25 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb message := utils.MakeMessage(comment) details := client.Details() + isBot := client.HasMode(modes.Bot) targetNick := target.Nick() chname := channel.Name() for _, member := range channel.Members() { for _, session := range member.Sessions() { if session != rb.session { - session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "KICK", chname, targetNick, comment) + session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "KICK", chname, targetNick, comment) } } } - rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "KICK", chname, targetNick, comment) + rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "KICK", chname, targetNick, comment) histItem := history.Item{ Type: history.Kick, Nick: details.nickMask, AccountName: details.accountName, Message: message, + IsBot: isBot, } histItem.Params[0] = targetNick channel.AddHistoryItem(histItem, details.account) @@ -1563,7 +1570,7 @@ func (channel *Channel) Purge(source string) { tnick := member.Nick() msgid := utils.GenerateSecretToken() for _, session := range member.Sessions() { - session.sendFromClientInternal(false, now, msgid, source, "*", nil, "KICK", chname, tnick, member.t("This channel has been purged by the server administrators and cannot be used")) + session.sendFromClientInternal(false, now, msgid, source, "*", false, nil, "KICK", chname, tnick, member.t("This channel has been purged by the server administrators and cannot be used")) } member.removeChannel(channel) } @@ -1600,6 +1607,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf } details := inviter.Details() + isBot := inviter.HasMode(modes.Bot) tDetails := invitee.Details() tnick := invitee.Nick() message := utils.MakeMessage(chname) @@ -1614,13 +1622,15 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf } for _, session := range member.Sessions() { if session.capabilities.Has(caps.InviteNotify) { - session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "INVITE", tnick, chname) + session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "INVITE", tnick, chname) } } } rb.Add(nil, inviter.server.name, RPL_INVITING, details.nick, tnick, chname) - invitee.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "INVITE", tnick, chname) + for _, iSession := range invitee.Sessions() { + iSession.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "INVITE", tnick, chname) + } if away, awayMessage := invitee.Away(); away { rb.Add(nil, inviter.server.name, RPL_AWAY, details.nick, tnick, awayMessage) } diff --git a/irc/chanserv.go b/irc/chanserv.go index 5dfcf44e9..13d9e9bf5 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -271,7 +271,7 @@ func csAmodeHandler(service *ircService, server *Server, client *Client, command if member.Account() == change.Arg { applied, change := channel.applyModeToMember(client, change, rb) if applied { - announceCmodeChanges(channel, modes.ModeChanges{change}, server.name, "*", "", rb) + announceCmodeChanges(channel, modes.ModeChanges{change}, server.name, "*", "", false, rb) } } } @@ -334,7 +334,7 @@ func csOpHandler(service *ircService, server *Server, client *Client, command st }, rb) if applied { - announceCmodeChanges(channelInfo, modes.ModeChanges{change}, server.name, "*", "", rb) + announceCmodeChanges(channelInfo, modes.ModeChanges{change}, server.name, "*", "", false, rb) } service.Notice(rb, client.t("Successfully granted operator privileges")) @@ -386,7 +386,8 @@ func csDeopHandler(service *ircService, server *Server, client *Client, command // the changes as coming from chanserv applied := channel.ApplyChannelModeChanges(client, false, modeChanges, rb) details := client.Details() - announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, rb) + isBot := client.HasMode(modes.Bot) + announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, isBot, rb) if len(applied) == 0 { return @@ -437,7 +438,7 @@ func csRegisterHandler(service *ircService, server *Server, client *Client, comm }, rb) if applied { - announceCmodeChanges(channelInfo, modes.ModeChanges{change}, service.prefix, "*", "", rb) + announceCmodeChanges(channelInfo, modes.ModeChanges{change}, service.prefix, "*", "", false, rb) } } diff --git a/irc/client.go b/irc/client.go index 0d275dcd7..f88d4a837 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1095,9 +1095,9 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I continue } if hasEventPlayback { - rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "INVITE", nick, item.Message.Message) + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "INVITE", nick, item.Message.Message) } else { - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", nil, "PRIVMSG", fmt.Sprintf(client.t("%[1]s invited you to channel %[2]s"), NUHToNick(item.Nick), item.Message.Message)) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", fmt.Sprintf(client.t("%[1]s invited you to channel %[2]s"), NUHToNick(item.Nick), item.Message.Message)) } continue case history.Privmsg: @@ -1118,11 +1118,11 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I tags = item.Tags } if !isSelfMessage(&item) { - rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, tags, command, nick, item.Message) } else { // this message was sent *from* the client to another nick; the target is item.Params[0] // substitute client's current nickmask in case client changed nick - rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message) + rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, item.IsBot, tags, command, item.Params[0], item.Message) } } @@ -1244,8 +1244,9 @@ func (client *Client) SetOper(oper *Oper) { // this is annoying to do correctly func (client *Client) sendChghost(oldNickMask string, vhost string) { details := client.Details() + isBot := client.HasMode(modes.Bot) for fClient := range client.Friends(caps.ChgHost) { - fClient.sendFromClientInternal(false, time.Time{}, "", oldNickMask, details.accountName, nil, "CHGHOST", details.username, vhost) + fClient.sendFromClientInternal(false, time.Time{}, "", oldNickMask, details.accountName, isBot, nil, "CHGHOST", details.username, vhost) } } @@ -1594,14 +1595,16 @@ func (client *Client) destroy(session *Session) { quitMessage = "Exited" } splitQuitMessage := utils.MakeMessage(quitMessage) + isBot := client.HasMode(modes.Bot) quitItem = history.Item{ Type: history.Quit, Nick: details.nickMask, AccountName: details.accountName, Message: splitQuitMessage, + IsBot: isBot, } var cache MessageCache - cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage) + cache.Initialize(client.server, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, isBot, nil, "QUIT", quitMessage) for friend := range friends { for _, session := range friend.Sessions() { cache.Send(session) @@ -1615,12 +1618,12 @@ func (client *Client) destroy(session *Session) { // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client. // Adds account-tag to the line as well. -func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) { +func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, isBot bool, tags map[string]string, command, target string, message utils.SplitMessage) { if message.Is512() { - session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message) + session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, isBot, tags, command, target, message.Message) } else { if session.capabilities.Has(caps.Multiline) { - for _, msg := range composeMultilineBatch(session.generateBatchID(), nickmask, accountName, tags, command, target, message) { + for _, msg := range composeMultilineBatch(session.generateBatchID(), nickmask, accountName, isBot, tags, command, target, message) { session.SendRawMessage(msg, blocking) } } else { @@ -1634,24 +1637,13 @@ func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, msgidSent = true msgid = message.Msgid } - session.sendFromClientInternal(blocking, message.Time, msgid, nickmask, accountName, tags, command, target, messagePair.Message) + session.sendFromClientInternal(blocking, message.Time, msgid, nickmask, accountName, isBot, tags, command, target, messagePair.Message) } } } } -// Sends a line with `nickmask` as the prefix, adding `time` and `account` tags if supported -func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) { - for _, session := range client.Sessions() { - err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...) - if err_ != nil { - err = err_ - } - } - return -} - -func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) { +func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, isBot bool, tags map[string]string, command string, params ...string) (err error) { msg := ircmsg.MakeMessage(tags, nickmask, command, params...) // attach account-tag if session.capabilities.Has(caps.AccountTag) && accountName != "*" { @@ -1663,17 +1655,24 @@ func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Ti } // attach server-time session.setTimeTag(&msg, serverTime) + // attach bot tag + if isBot && session.capabilities.Has(caps.MessageTags) { + msg.SetTag(caps.BotTagName, "") + } return session.SendRawMessage(msg, blocking) } -func composeMultilineBatch(batchID, fromNickMask, fromAccount string, tags map[string]string, command, target string, message utils.SplitMessage) (result []ircmsg.Message) { +func composeMultilineBatch(batchID, fromNickMask, fromAccount string, isBot bool, tags map[string]string, command, target string, message utils.SplitMessage) (result []ircmsg.Message) { batchStart := ircmsg.MakeMessage(tags, fromNickMask, "BATCH", "+"+batchID, caps.MultilineBatchType, target) batchStart.SetTag("time", message.Time.Format(IRCv3TimestampFormat)) batchStart.SetTag("msgid", message.Msgid) if fromAccount != "*" { batchStart.SetTag("account", fromAccount) } + if isBot { + batchStart.SetTag(caps.BotTagName, "") + } result = append(result, batchStart) for _, msg := range message.Split { diff --git a/irc/handlers.go b/irc/handlers.go index ac417a6da..ddd4d9c14 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -367,11 +367,12 @@ func awayHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons func dispatchAwayNotify(client *Client, isAway bool, awayMessage string) { // dispatch away-notify details := client.Details() + isBot := client.HasMode(modes.Bot) for session := range client.Friends(caps.AwayNotify) { if isAway { - session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, nil, "AWAY", awayMessage) + session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage) } else { - session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, nil, "AWAY") + session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY") } } } @@ -1689,12 +1690,13 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon // process mode changes, include list operations (an empty set of changes does a list) applied := channel.ApplyChannelModeChanges(client, msg.Command == "SAMODE", changes, rb) details := client.Details() - announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, rb) + isBot := client.HasMode(modes.Bot) + announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, isBot, rb) return false } -func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source, accountName, account string, rb *ResponseBuffer) { +func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source, accountName, account string, isBot bool, rb *ResponseBuffer) { // send out changes if len(applied) > 0 { message := utils.MakeMessage("") @@ -1703,11 +1705,11 @@ func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source, a message.Split = append(message.Split, utils.MessagePair{Message: changeString}) } args := append([]string{channel.name}, changeStrings...) - rb.AddFromClient(message.Time, message.Msgid, source, accountName, nil, "MODE", args...) + rb.AddFromClient(message.Time, message.Msgid, source, accountName, isBot, nil, "MODE", args...) for _, member := range channel.Members() { for _, session := range member.Sessions() { if session != rb.session { - session.sendFromClientInternal(false, message.Time, message.Msgid, source, accountName, nil, "MODE", args...) + session.sendFromClientInternal(false, message.Time, message.Msgid, source, accountName, isBot, nil, "MODE", args...) } } } @@ -1716,6 +1718,7 @@ func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source, a Nick: source, AccountName: accountName, Message: message, + IsBot: isBot, }, account) } } @@ -2204,17 +2207,18 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi } } + isBot := client.HasMode(modes.Bot) for _, session := range deliverySessions { hasTagsCap := session.capabilities.Has(caps.MessageTags) // don't send TAGMSG at all if they don't have the tags cap if histType == history.Tagmsg && hasTagsCap { - session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick) + session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, isBot, tags, command, tnick) } else if histType != history.Tagmsg && !(session.isTor && message.IsRestrictedCTCPMessage()) { tagsToSend := tags if !hasTagsCap { tagsToSend = nil } - session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tagsToSend, command, tnick, message) + session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, isBot, tagsToSend, command, tnick, message) } } @@ -2674,9 +2678,9 @@ func relaymsgHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res } if session == rb.session { - rb.AddSplitMessageFromClient(nick, "*", tagsToUse, "PRIVMSG", channelName, message) + rb.AddSplitMessageFromClient(nick, "*", false, tagsToUse, "PRIVMSG", channelName, message) } else { - session.sendSplitMsgFromClientInternal(false, nick, "*", tagsToUse, "PRIVMSG", channelName, message) + session.sendSplitMsgFromClientInternal(false, nick, "*", false, tagsToUse, "PRIVMSG", channelName, message) } } } @@ -2835,11 +2839,12 @@ func setnameHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp now := time.Now().UTC() friends := client.Friends(caps.SetName) delete(friends, rb.session) + isBot := client.HasMode(modes.Bot) for session := range friends { - session.sendFromClientInternal(false, now, "", details.nickMask, details.accountName, nil, "SETNAME", details.realname) + session.sendFromClientInternal(false, now, "", details.nickMask, details.accountName, isBot, nil, "SETNAME", details.realname) } // respond to the user unconditionally, even if they don't have the cap - rb.AddFromClient(now, "", details.nickMask, details.accountName, nil, "SETNAME", details.realname) + rb.AddFromClient(now, "", details.nickMask, details.accountName, isBot, nil, "SETNAME", details.realname) return false } diff --git a/irc/history/history.go b/irc/history/history.go index 990f7302e..981108d86 100644 --- a/irc/history/history.go +++ b/irc/history/history.go @@ -45,6 +45,7 @@ type Item struct { // an incoming or outgoing message). this lets us emulate the "query buffer" functionality // required by CHATHISTORY: CfCorrespondent string + IsBot bool `json:"IsBot,omitempty"` } // HasMsgid tests whether a message has the message id `msgid`. diff --git a/irc/message_cache.go b/irc/message_cache.go index 0bb8e2bd8..4efe8234b 100644 --- a/irc/message_cache.go +++ b/irc/message_cache.go @@ -35,6 +35,7 @@ type MessageCache struct { tags map[string]string source string command string + isBot bool params []string @@ -42,7 +43,7 @@ type MessageCache struct { splitMessage utils.SplitMessage } -func addAllTags(msg *ircmsg.Message, tags map[string]string, serverTime time.Time, msgid, accountName string) { +func addAllTags(msg *ircmsg.Message, tags map[string]string, serverTime time.Time, msgid, accountName string, isBot bool) { msg.UpdateTags(tags) msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat)) if accountName != "*" { @@ -51,6 +52,9 @@ func addAllTags(msg *ircmsg.Message, tags map[string]string, serverTime time.Tim if msgid != "" { msg.SetTag("msgid", msgid) } + if isBot { + msg.SetTag(caps.BotTagName, "") + } } func (m *MessageCache) handleErr(server *Server, err error) bool { @@ -64,11 +68,12 @@ func (m *MessageCache) handleErr(server *Server, err error) bool { return false } -func (m *MessageCache) Initialize(server *Server, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) { +func (m *MessageCache) Initialize(server *Server, serverTime time.Time, msgid string, nickmask, accountName string, isBot bool, tags map[string]string, command string, params ...string) (err error) { m.time = serverTime m.msgid = msgid m.source = nickmask m.accountName = accountName + m.isBot = isBot m.tags = tags m.command = command m.params = params @@ -87,7 +92,7 @@ func (m *MessageCache) Initialize(server *Server, serverTime time.Time, msgid st return } - addAllTags(&msg, tags, serverTime, msgid, accountName) + addAllTags(&msg, tags, serverTime, msgid, accountName, isBot) m.fullTags, err = msg.LineBytesStrict(false, MaxLineLen) if m.handleErr(server, err) { return @@ -95,11 +100,12 @@ func (m *MessageCache) Initialize(server *Server, serverTime time.Time, msgid st return } -func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) (err error) { +func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountName string, isBot bool, tags map[string]string, command, target string, message utils.SplitMessage) (err error) { m.time = message.Time m.msgid = message.Msgid m.source = nickmask m.accountName = accountName + m.isBot = isBot m.tags = tags m.command = command m.target = target @@ -130,7 +136,7 @@ func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountN } } - addAllTags(&msg, tags, message.Time, message.Msgid, accountName) + addAllTags(&msg, tags, message.Time, message.Msgid, accountName, isBot) m.fullTags, err = msg.LineBytesStrict(false, MaxLineLen) if m.handleErr(server, err) { return @@ -158,7 +164,7 @@ func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountN // so a collision isn't expected until there are on the order of 2**32 // concurrent batches being relayed: batchID := utils.GenerateSecretToken()[:utils.SecretTokenLength/2] - batch := composeMultilineBatch(batchID, nickmask, accountName, tags, command, target, message) + batch := composeMultilineBatch(batchID, nickmask, accountName, isBot, tags, command, target, message) m.fullTagsMultiline = make([][]byte, len(batch)) for i, msg := range batch { if forceTrailing { @@ -184,7 +190,7 @@ func (m *MessageCache) Send(session *Session) { session.sendBytes(m.plain, false) } else { // slowpath - session.sendFromClientInternal(false, m.time, m.msgid, m.source, m.accountName, nil, m.command, m.params...) + session.sendFromClientInternal(false, m.time, m.msgid, m.source, m.accountName, m.isBot, nil, m.command, m.params...) } } } else if m.fullTagsMultiline != nil { @@ -199,7 +205,7 @@ func (m *MessageCache) Send(session *Session) { } } else { // slowpath - session.sendSplitMsgFromClientInternal(false, m.source, m.accountName, m.tags, m.command, m.target, m.splitMessage) + session.sendSplitMsgFromClientInternal(false, m.source, m.accountName, m.isBot, m.tags, m.command, m.target, m.splitMessage) } } } diff --git a/irc/nickname.go b/irc/nickname.go index 180f99533..ef2d353d2 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -11,6 +11,7 @@ import ( "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/history" + "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/utils" ) @@ -101,10 +102,11 @@ func performNickChange(server *Server, client *Client, target *Client, session * target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, assignedNickname)) } target.server.whoWas.Append(details.WhoWas) - rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname) + isBot := !isSanick && client.HasMode(modes.Bot) + rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, isBot, nil, "NICK", assignedNickname) for session := range target.Friends() { if session != rb.session { - session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname) + session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, isBot, nil, "NICK", assignedNickname) } } } diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index f7c7d2287..7e071f99a 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -96,7 +96,7 @@ func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, comma } // AddFromClient adds a new message from a specific client to our queue. -func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) { +func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, isBot bool, tags map[string]string, command string, params ...string) { msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...) if rb.session.capabilities.Has(caps.MessageTags) { msg.UpdateTags(tags) @@ -107,8 +107,13 @@ func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMa msg.SetTag("account", fromAccount) } // attach message-id - if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) { - msg.SetTag("msgid", msgid) + if rb.session.capabilities.Has(caps.MessageTags) { + if len(msgid) != 0 { + msg.SetTag("msgid", msgid) + } + if isBot { + msg.SetTag(caps.BotTagName, "") + } } // attach server-time rb.session.setTimeTag(&msg, time) @@ -117,17 +122,17 @@ func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMa } // AddSplitMessageFromClient adds a new split message from a specific client to our queue. -func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) { +func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, isBot bool, tags map[string]string, command string, target string, message utils.SplitMessage) { if message.Is512() { if message.Message == "" { // XXX this is a TAGMSG - rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target) + rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, isBot, tags, command, target) } else { - rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message) + rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, isBot, tags, command, target, message.Message) } } else { if rb.session.capabilities.Has(caps.Multiline) { - batch := composeMultilineBatch(rb.session.generateBatchID(), fromNickMask, fromAccount, tags, command, target, message) + batch := composeMultilineBatch(rb.session.generateBatchID(), fromNickMask, fromAccount, isBot, tags, command, target, message) rb.setNestedBatchTag(&batch[0]) rb.setNestedBatchTag(&batch[len(batch)-1]) rb.messages = append(rb.messages, batch...) @@ -137,25 +142,26 @@ func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAcc if i == 0 { msgid = message.Msgid } - rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message) + rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, isBot, tags, command, target, messagePair.Message) } } } } func (rb *ResponseBuffer) addEchoMessage(tags map[string]string, nickMask, accountName, command, target string, message utils.SplitMessage) { + // TODO fix isBot here if rb.session.capabilities.Has(caps.EchoMessage) { hasTagsCap := rb.session.capabilities.Has(caps.MessageTags) if command == "TAGMSG" { if hasTagsCap { - rb.AddFromClient(message.Time, message.Msgid, nickMask, accountName, tags, command, target) + rb.AddFromClient(message.Time, message.Msgid, nickMask, accountName, false, tags, command, target) } } else { tagsToSend := tags if !hasTagsCap { tagsToSend = nil } - rb.AddSplitMessageFromClient(nickMask, accountName, tagsToSend, command, target, message) + rb.AddSplitMessageFromClient(nickMask, accountName, false, tagsToSend, command, target, message) } } } diff --git a/irc/roleplay.go b/irc/roleplay.go index ab22d28df..246112ab5 100644 --- a/irc/roleplay.go +++ b/irc/roleplay.go @@ -92,15 +92,16 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt return } + isBot := client.HasMode(modes.Bot) for _, member := range channel.Members() { for _, session := range member.Sessions() { // see discussion on #865: clients do not understand how to do local echo // of roleplay commands, so send them a copy whether they have echo-message // or not if rb.session == session { - rb.AddSplitMessageFromClient(sourceMask, "", nil, "PRIVMSG", targetString, splitMessage) + rb.AddSplitMessageFromClient(sourceMask, "*", isBot, nil, "PRIVMSG", targetString, splitMessage) } else { - session.sendSplitMsgFromClientInternal(false, sourceMask, "*", nil, "PRIVMSG", targetString, splitMessage) + session.sendSplitMsgFromClientInternal(false, sourceMask, "*", isBot, nil, "PRIVMSG", targetString, splitMessage) } } } @@ -125,8 +126,9 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt cnick := client.Nick() tnick := user.Nick() + isBot := client.HasMode(modes.Bot) for _, session := range user.Sessions() { - session.sendSplitMsgFromClientInternal(false, sourceMask, "*", nil, "PRIVMSG", tnick, splitMessage) + session.sendSplitMsgFromClientInternal(false, sourceMask, "*", isBot, nil, "PRIVMSG", tnick, splitMessage) } if away, awayMessage := user.Away(); away { //TODO(dan): possibly implement cooldown of away notifications to users