|
@@ -509,6 +509,59 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
509
|
509
|
return false
|
510
|
510
|
}
|
511
|
511
|
|
|
512
|
+// BATCH {+,-}reference-tag type [params...]
|
|
513
|
+func batchHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
|
514
|
+ tag := msg.Params[0]
|
|
515
|
+ fail := false
|
|
516
|
+ sendErrors := rb.session.batch.command != "NOTICE"
|
|
517
|
+ if len(tag) == 0 {
|
|
518
|
+ fail = true
|
|
519
|
+ } else if tag[0] == '+' {
|
|
520
|
+ if rb.session.batch.label != "" || msg.Params[1] != caps.MultilineBatchType {
|
|
521
|
+ fail = true
|
|
522
|
+ } else {
|
|
523
|
+ rb.session.batch.label = tag[1:]
|
|
524
|
+ rb.session.batch.tags = msg.ClientOnlyTags()
|
|
525
|
+ if len(msg.Params) == 2 {
|
|
526
|
+ fail = true
|
|
527
|
+ } else {
|
|
528
|
+ rb.session.batch.target = msg.Params[2]
|
|
529
|
+ // save the response label for later
|
|
530
|
+ // XXX changing the label inside a handler is a bit dodgy, but it works here
|
|
531
|
+ // because there's no way we could have triggered a flush up to this point
|
|
532
|
+ rb.session.batch.responseLabel = rb.Label
|
|
533
|
+ rb.Label = ""
|
|
534
|
+ }
|
|
535
|
+ }
|
|
536
|
+ } else if tag[0] == '-' {
|
|
537
|
+ if rb.session.batch.label == "" || rb.session.batch.label != tag[1:] {
|
|
538
|
+ fail = true
|
|
539
|
+ } else if rb.session.batch.message.LenLines() == 0 {
|
|
540
|
+ fail = true
|
|
541
|
+ } else {
|
|
542
|
+ batch := rb.session.batch
|
|
543
|
+ rb.session.batch = MultilineBatch{}
|
|
544
|
+ batch.message.Time = time.Now().UTC()
|
|
545
|
+ histType, err := msgCommandToHistType(batch.command)
|
|
546
|
+ if err != nil {
|
|
547
|
+ histType = history.Privmsg
|
|
548
|
+ }
|
|
549
|
+ // see previous caution about modifying ResponseBuffer.Label
|
|
550
|
+ rb.Label = batch.responseLabel
|
|
551
|
+ dispatchMessageToTarget(client, batch.tags, histType, batch.target, batch.message, rb)
|
|
552
|
+ }
|
|
553
|
+ }
|
|
554
|
+
|
|
555
|
+ if fail {
|
|
556
|
+ rb.session.batch = MultilineBatch{}
|
|
557
|
+ if sendErrors {
|
|
558
|
+ rb.Add(nil, server.name, "FAIL", "BATCH", "MULTILINE_INVALID", client.t("Invalid multiline batch"))
|
|
559
|
+ }
|
|
560
|
+ }
|
|
561
|
+
|
|
562
|
+ return false
|
|
563
|
+}
|
|
564
|
+
|
512
|
565
|
// BRB [message]
|
513
|
566
|
func brbHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
514
|
567
|
success, duration := client.brbTimer.Enable()
|
|
@@ -665,11 +718,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
665
|
718
|
defer func() {
|
666
|
719
|
// successful responses are sent as a chathistory or history batch
|
667
|
720
|
if success && 0 < len(items) {
|
668
|
|
- batchType := "chathistory"
|
669
|
|
- if rb.session.capabilities.Has(caps.EventPlayback) {
|
670
|
|
- batchType = "history"
|
671
|
|
- }
|
672
|
|
- rb.ForceBatchStart(batchType, true)
|
673
|
721
|
if channel == nil {
|
674
|
722
|
client.replayPrivmsgHistory(rb, items, true)
|
675
|
723
|
} else {
|
|
@@ -2019,15 +2067,44 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
2019
|
2067
|
return false
|
2020
|
2068
|
}
|
2021
|
2069
|
|
|
2070
|
+// helper to store a batched PRIVMSG in the session object
|
|
2071
|
+func absorbBatchedMessage(server *Server, client *Client, msg ircmsg.IrcMessage, batchTag string, histType history.ItemType, rb *ResponseBuffer) {
|
|
2072
|
+ // sanity checks. batch tag correctness was already checked and is redundant here
|
|
2073
|
+ // as a defensive measure. TAGMSG is checked without an error message: "don't eat paste"
|
|
2074
|
+ if batchTag != rb.session.batch.label || histType == history.Tagmsg || len(msg.Params) == 1 || msg.Params[1] == "" {
|
|
2075
|
+ return
|
|
2076
|
+ }
|
|
2077
|
+ rb.session.batch.command = msg.Command
|
|
2078
|
+ isConcat, _ := msg.GetTag(caps.MultilineConcatTag)
|
|
2079
|
+ rb.session.batch.message.Append(msg.Params[1], isConcat)
|
|
2080
|
+ config := server.Config()
|
|
2081
|
+ if config.Limits.Multiline.MaxBytes < rb.session.batch.message.LenBytes() {
|
|
2082
|
+ if histType != history.Notice {
|
|
2083
|
+ rb.Add(nil, server.name, "FAIL", "BATCH", "MULTILINE_MAX_BYTES", strconv.Itoa(config.Limits.Multiline.MaxBytes))
|
|
2084
|
+ }
|
|
2085
|
+ rb.session.batch = MultilineBatch{}
|
|
2086
|
+ } else if config.Limits.Multiline.MaxLines != 0 && config.Limits.Multiline.MaxLines < rb.session.batch.message.LenLines() {
|
|
2087
|
+ if histType != history.Notice {
|
|
2088
|
+ rb.Add(nil, server.name, "FAIL", "BATCH", "MULTILINE_MAX_LINES", strconv.Itoa(config.Limits.Multiline.MaxLines))
|
|
2089
|
+ }
|
|
2090
|
+ rb.session.batch = MultilineBatch{}
|
|
2091
|
+ }
|
|
2092
|
+}
|
|
2093
|
+
|
2022
|
2094
|
// NOTICE <target>{,<target>} <message>
|
2023
|
2095
|
// PRIVMSG <target>{,<target>} <message>
|
2024
|
2096
|
// TAGMSG <target>{,<target>}
|
2025
|
2097
|
func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
2026
|
|
- histType, err := msgCommandToHistType(server, msg.Command)
|
|
2098
|
+ histType, err := msgCommandToHistType(msg.Command)
|
2027
|
2099
|
if err != nil {
|
2028
|
2100
|
return false
|
2029
|
2101
|
}
|
2030
|
2102
|
|
|
2103
|
+ if isBatched, batchTag := msg.GetTag("batch"); isBatched {
|
|
2104
|
+ absorbBatchedMessage(server, client, msg, batchTag, histType, rb)
|
|
2105
|
+ return false
|
|
2106
|
+ }
|
|
2107
|
+
|
2031
|
2108
|
cnick := client.Nick()
|
2032
|
2109
|
clientOnlyTags := msg.ClientOnlyTags()
|
2033
|
2110
|
if histType == history.Tagmsg && len(clientOnlyTags) == 0 {
|
|
@@ -2040,116 +2117,125 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
2040
|
2117
|
if len(msg.Params) > 1 {
|
2041
|
2118
|
message = msg.Params[1]
|
2042
|
2119
|
}
|
|
2120
|
+ if histType != history.Tagmsg && message == "" {
|
|
2121
|
+ rb.Add(nil, server.name, ERR_NOTEXTTOSEND, cnick, client.t("No text to send"))
|
|
2122
|
+ return false
|
|
2123
|
+ }
|
2043
|
2124
|
|
2044
|
|
- // note that error replies are never sent for NOTICE
|
2045
|
|
-
|
2046
|
|
- if client.isTor && isRestrictedCTCPMessage(message) {
|
|
2125
|
+ if client.isTor && utils.IsRestrictedCTCPMessage(message) {
|
|
2126
|
+ // note that error replies are never sent for NOTICE
|
2047
|
2127
|
if histType != history.Notice {
|
2048
|
|
- rb.Add(nil, server.name, "NOTICE", client.t("CTCP messages are disabled over Tor"))
|
|
2128
|
+ rb.Notice(client.t("CTCP messages are disabled over Tor"))
|
2049
|
2129
|
}
|
2050
|
2130
|
return false
|
2051
|
2131
|
}
|
2052
|
2132
|
|
2053
|
2133
|
for i, targetString := range targets {
|
2054
|
|
- // each target gets distinct msgids
|
2055
|
|
- splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine))
|
2056
|
|
-
|
2057
|
2134
|
// max of four targets per privmsg
|
2058
|
|
- if i > maxTargets-1 {
|
|
2135
|
+ if i == maxTargets {
|
2059
|
2136
|
break
|
2060
|
2137
|
}
|
2061
|
|
- prefixes, targetString := modes.SplitChannelMembershipPrefixes(targetString)
|
2062
|
|
- lowestPrefix := modes.GetLowestChannelModePrefix(prefixes)
|
|
2138
|
+ // each target gets distinct msgids
|
|
2139
|
+ splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine))
|
|
2140
|
+ dispatchMessageToTarget(client, clientOnlyTags, histType, targetString, splitMsg, rb)
|
|
2141
|
+ }
|
|
2142
|
+ return false
|
|
2143
|
+}
|
2063
|
2144
|
|
2064
|
|
- if len(targetString) == 0 {
|
2065
|
|
- continue
|
2066
|
|
- } else if targetString[0] == '#' {
|
2067
|
|
- channel := server.channels.Get(targetString)
|
2068
|
|
- if channel == nil {
|
2069
|
|
- if histType != history.Notice {
|
2070
|
|
- rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, cnick, utils.SafeErrorParam(targetString), client.t("No such channel"))
|
2071
|
|
- }
|
2072
|
|
- continue
|
|
2145
|
+func dispatchMessageToTarget(client *Client, tags map[string]string, histType history.ItemType, target string, message utils.SplitMessage, rb *ResponseBuffer) {
|
|
2146
|
+ server := client.server
|
|
2147
|
+ command := histTypeToMsgCommand(histType)
|
|
2148
|
+
|
|
2149
|
+ prefixes, target := modes.SplitChannelMembershipPrefixes(target)
|
|
2150
|
+ lowestPrefix := modes.GetLowestChannelModePrefix(prefixes)
|
|
2151
|
+
|
|
2152
|
+ if len(target) == 0 {
|
|
2153
|
+ return
|
|
2154
|
+ } else if target[0] == '#' {
|
|
2155
|
+ channel := server.channels.Get(target)
|
|
2156
|
+ if channel == nil {
|
|
2157
|
+ if histType != history.Notice {
|
|
2158
|
+ rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
|
2073
|
2159
|
}
|
2074
|
|
- channel.SendSplitMessage(msg.Command, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
|
2075
|
|
- } else {
|
2076
|
|
- // NOTICE and TAGMSG to services are ignored
|
2077
|
|
- if histType == history.Privmsg {
|
2078
|
|
- lowercaseTarget := strings.ToLower(targetString)
|
2079
|
|
- if service, isService := OragonoServices[lowercaseTarget]; isService {
|
2080
|
|
- servicePrivmsgHandler(service, server, client, message, rb)
|
2081
|
|
- continue
|
2082
|
|
- } else if _, isZNC := zncHandlers[lowercaseTarget]; isZNC {
|
2083
|
|
- zncPrivmsgHandler(client, lowercaseTarget, message, rb)
|
2084
|
|
- continue
|
2085
|
|
- }
|
|
2160
|
+ return
|
|
2161
|
+ }
|
|
2162
|
+ channel.SendSplitMessage(command, lowestPrefix, tags, client, message, rb)
|
|
2163
|
+ } else {
|
|
2164
|
+ // NOTICE and TAGMSG to services are ignored
|
|
2165
|
+ if histType == history.Privmsg {
|
|
2166
|
+ lowercaseTarget := strings.ToLower(target)
|
|
2167
|
+ if service, isService := OragonoServices[lowercaseTarget]; isService {
|
|
2168
|
+ servicePrivmsgHandler(service, server, client, message.Message, rb)
|
|
2169
|
+ return
|
|
2170
|
+ } else if _, isZNC := zncHandlers[lowercaseTarget]; isZNC {
|
|
2171
|
+ zncPrivmsgHandler(client, lowercaseTarget, message.Message, rb)
|
|
2172
|
+ return
|
2086
|
2173
|
}
|
|
2174
|
+ }
|
2087
|
2175
|
|
2088
|
|
- user := server.clients.Get(targetString)
|
2089
|
|
- if user == nil {
|
2090
|
|
- if histType != history.Notice {
|
2091
|
|
- rb.Add(nil, server.name, ERR_NOSUCHNICK, cnick, targetString, "No such nick")
|
2092
|
|
- }
|
2093
|
|
- continue
|
|
2176
|
+ user := server.clients.Get(target)
|
|
2177
|
+ if user == nil {
|
|
2178
|
+ if histType != history.Notice {
|
|
2179
|
+ rb.Add(nil, server.name, ERR_NOSUCHNICK, client.Nick(), target, "No such nick")
|
2094
|
2180
|
}
|
2095
|
|
- tnick := user.Nick()
|
2096
|
|
-
|
2097
|
|
- nickMaskString := client.NickMaskString()
|
2098
|
|
- accountName := client.AccountName()
|
2099
|
|
- // restrict messages appropriately when +R is set
|
2100
|
|
- // intentionally make the sending user think the message went through fine
|
2101
|
|
- allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
|
2102
|
|
- allowedTor := !user.isTor || !isRestrictedCTCPMessage(message)
|
2103
|
|
- if allowedPlusR && allowedTor {
|
2104
|
|
- for _, session := range user.Sessions() {
|
2105
|
|
- if histType == history.Tagmsg {
|
2106
|
|
- // don't send TAGMSG at all if they don't have the tags cap
|
2107
|
|
- if session.capabilities.Has(caps.MessageTags) {
|
2108
|
|
- session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
2109
|
|
- }
|
2110
|
|
- } else {
|
2111
|
|
- session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
|
2181
|
+ return
|
|
2182
|
+ }
|
|
2183
|
+ tnick := user.Nick()
|
|
2184
|
+
|
|
2185
|
+ nickMaskString := client.NickMaskString()
|
|
2186
|
+ accountName := client.AccountName()
|
|
2187
|
+ // restrict messages appropriately when +R is set
|
|
2188
|
+ // intentionally make the sending user think the message went through fine
|
|
2189
|
+ allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
|
|
2190
|
+ allowedTor := !user.isTor || !message.IsRestrictedCTCPMessage()
|
|
2191
|
+ if allowedPlusR && allowedTor {
|
|
2192
|
+ for _, session := range user.Sessions() {
|
|
2193
|
+ if histType == history.Tagmsg {
|
|
2194
|
+ // don't send TAGMSG at all if they don't have the tags cap
|
|
2195
|
+ if session.capabilities.Has(caps.MessageTags) {
|
|
2196
|
+ session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
|
2112
|
2197
|
}
|
2113
|
|
- }
|
2114
|
|
- }
|
2115
|
|
- // an echo-message may need to be included in the response:
|
2116
|
|
- if rb.session.capabilities.Has(caps.EchoMessage) {
|
2117
|
|
- if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
2118
|
|
- rb.AddFromClient(splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
2119
|
2198
|
} else {
|
2120
|
|
- rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
|
2199
|
+ session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tags, command, tnick, message)
|
2121
|
2200
|
}
|
2122
|
2201
|
}
|
2123
|
|
- // an echo-message may need to go out to other client sessions:
|
2124
|
|
- for _, session := range client.Sessions() {
|
2125
|
|
- if session == rb.session {
|
2126
|
|
- continue
|
2127
|
|
- }
|
2128
|
|
- if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
2129
|
|
- session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
2130
|
|
- } else if histType != history.Tagmsg {
|
2131
|
|
- session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
2132
|
|
- }
|
|
2202
|
+ }
|
|
2203
|
+ // an echo-message may need to be included in the response:
|
|
2204
|
+ if rb.session.capabilities.Has(caps.EchoMessage) {
|
|
2205
|
+ if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
|
2206
|
+ rb.AddFromClient(message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
|
|
2207
|
+ } else {
|
|
2208
|
+ rb.AddSplitMessageFromClient(nickMaskString, accountName, tags, command, tnick, message)
|
2133
|
2209
|
}
|
2134
|
|
- if histType != history.Notice && user.Away() {
|
2135
|
|
- //TODO(dan): possibly implement cooldown of away notifications to users
|
2136
|
|
- rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage())
|
|
2210
|
+ }
|
|
2211
|
+ // an echo-message may need to go out to other client sessions:
|
|
2212
|
+ for _, session := range client.Sessions() {
|
|
2213
|
+ if session == rb.session {
|
|
2214
|
+ continue
|
2137
|
2215
|
}
|
2138
|
|
-
|
2139
|
|
- item := history.Item{
|
2140
|
|
- Type: histType,
|
2141
|
|
- Message: splitMsg,
|
2142
|
|
- Nick: nickMaskString,
|
2143
|
|
- AccountName: accountName,
|
|
2216
|
+ if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
|
2217
|
+ session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
|
|
2218
|
+ } else if histType != history.Tagmsg {
|
|
2219
|
+ session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tags, command, tnick, message)
|
2144
|
2220
|
}
|
2145
|
|
- // add to the target's history:
|
2146
|
|
- user.history.Add(item)
|
2147
|
|
- // add this to the client's history as well, recording the target:
|
2148
|
|
- item.Params[0] = tnick
|
2149
|
|
- client.history.Add(item)
|
2150
|
2221
|
}
|
|
2222
|
+ if histType != history.Notice && user.Away() {
|
|
2223
|
+ //TODO(dan): possibly implement cooldown of away notifications to users
|
|
2224
|
+ rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
|
|
2225
|
+ }
|
|
2226
|
+
|
|
2227
|
+ item := history.Item{
|
|
2228
|
+ Type: histType,
|
|
2229
|
+ Message: message,
|
|
2230
|
+ Nick: nickMaskString,
|
|
2231
|
+ AccountName: accountName,
|
|
2232
|
+ }
|
|
2233
|
+ // add to the target's history:
|
|
2234
|
+ user.history.Add(item)
|
|
2235
|
+ // add this to the client's history as well, recording the target:
|
|
2236
|
+ item.Params[0] = tnick
|
|
2237
|
+ client.history.Add(item)
|
2151
|
2238
|
}
|
2152
|
|
- return false
|
2153
|
2239
|
}
|
2154
|
2240
|
|
2155
|
2241
|
// NPC <target> <sourcenick> <message>
|
|
@@ -2308,12 +2394,6 @@ func pongHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
2308
|
2394
|
return false
|
2309
|
2395
|
}
|
2310
|
2396
|
|
2311
|
|
-func isRestrictedCTCPMessage(message string) bool {
|
2312
|
|
- // block all CTCP privmsgs to Tor clients except for ACTION
|
2313
|
|
- // DCC can potentially be used for deanonymization, the others for fingerprinting
|
2314
|
|
- return strings.HasPrefix(message, "\x01") && !strings.HasPrefix(message, "\x01ACTION")
|
2315
|
|
-}
|
2316
|
|
-
|
2317
|
2397
|
// QUIT [<reason>]
|
2318
|
2398
|
func quitHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
2319
|
2399
|
reason := "Quit"
|