1 /++ 2 Awareness mixins, for plugins to mix in to extend behaviour and enjoy a 3 considerable degree of automation. 4 5 These are used for plugins to mix in book-keeping of users and channels. 6 7 Example: 8 --- 9 import kameloso.plugins.common.core; 10 import kameloso.plugins.common.awareness; 11 12 @Settings struct FooSettings { /* ... */ } 13 14 @(IRCEventHandler() 15 .onEvent(IRCEvent.Type.CHAN) 16 .permissionsRequired(Permissions.anyone) 17 .channelPolicy(ChannelPolicy.home) 18 .addCommand( 19 IRCEventHandler.Command() 20 .word("foo") 21 .policy(PrefixPolicy.prefixed) 22 ) 23 ) 24 void onFoo(FooPlugin plugin, const ref IRCEvent event) 25 { 26 // ... 27 } 28 29 mixin UserAwareness; 30 mixin ChannelAwareness; 31 32 final class FooPlugin : IRCPlugin 33 { 34 FooSettings fooSettings; 35 36 // ... 37 38 mixin IRCPluginImpl; 39 } 40 --- 41 42 See_Also: 43 [kameloso.plugins.common.core], 44 [kameloso.plugins.common.misc] 45 46 Copyright: [JR](https://github.com/zorael) 47 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 48 49 Authors: 50 [JR](https://github.com/zorael) 51 +/ 52 module kameloso.plugins.common.awareness; 53 54 private: 55 56 import kameloso.plugins.common.core; 57 import dialect.defs; 58 import std.typecons : Flag, No, Yes; 59 60 public: 61 62 @safe: 63 64 65 // MinimalAuthentication 66 /++ 67 Implements triggering of queued events in a plugin module, based on user details 68 such as account or hostmask. 69 70 Most of the time a plugin doesn't require a full 71 [kameloso.plugins.common.awareness.UserAwareness|UserAwareness]; only 72 those that need looking up users outside of the current event do. The 73 persistency service allows for plugins to just read the information from 74 the [dialect.defs.IRCUser|IRCUser] embedded in the event directly, and that's 75 often enough. 76 77 General rule: if a plugin doesn't access 78 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users], 79 it's probably going to be enough with only 80 [kameloso.plugins.common.awareness.MinimalAuthentication|MinimalAuthentication]. 81 82 Params: 83 debug_ = Whether or not to include debugging output. 84 module_ = String name of the mixing-in module; generally leave as-is. 85 86 See_Also: 87 [kameloso.plugins.common.awareness.UserAwareness|UserAwareness] 88 [kameloso.plugins.common.awareness.ChannelAwareness|ChannelAwareness] 89 +/ 90 mixin template MinimalAuthentication( 91 Flag!"debug_" debug_ = No.debug_, 92 string module_ = __MODULE__) 93 { 94 private import dialect.defs : IRCEvent; 95 private static import kameloso.plugins.common.awareness; 96 97 /++ 98 Flag denoting that 99 [kameloso.plugins.common.awareness.MinimalAuthentication|MinimalAuthentication] 100 has been mixed in. 101 +/ 102 package enum hasMinimalAuthentication = true; 103 104 // onMinimalAuthenticationAccountInfoTargetMixin 105 /++ 106 Proxies to 107 [kameloso.plugins.common.awareness.onMinimalAuthenticationAccountInfoTarget|onMinimalAuthenticationAccountInfoTarget]. 108 109 See_Also: 110 [kameloso.plugins.common.awareness.onMinimalAuthenticationAccountInfoTarget|onMinimalAuthenticationAccountInfoTarget] 111 +/ 112 @(IRCEventHandler() 113 .onEvent(IRCEvent.Type.RPL_WHOISACCOUNT) 114 .onEvent(IRCEvent.Type.RPL_WHOISREGNICK) 115 .onEvent(IRCEvent.Type.RPL_ENDOFWHOIS) 116 .when(Timing.early) 117 .chainable(true) 118 ) 119 void onMinimalAuthenticationAccountInfoTargetMixin(IRCPlugin plugin, const ref IRCEvent event) @system 120 { 121 return kameloso.plugins.common.awareness.onMinimalAuthenticationAccountInfoTarget(plugin, event); 122 } 123 124 // onMinimalAuthenticationUnknownCommandWHOISMixin 125 /++ 126 Proxies to 127 [kameloso.plugins.common.awareness.onMinimalAuthenticationUnknownCommandWHOIS|onMinimalAuthenticationUnknownCommandWHOIS]. 128 129 See_Also: 130 [kameloso.plugins.common.awareness.onMinimalAuthenticationUnknownCommandWHOIS|onMinimalAuthenticationUnknownCommandWHOIS] 131 +/ 132 @(IRCEventHandler() 133 .onEvent(IRCEvent.Type.ERR_UNKNOWNCOMMAND) 134 .when(Timing.early) 135 .chainable(true) 136 ) 137 void onMinimalAuthenticationUnknownCommandWHOIS(IRCPlugin plugin, const ref IRCEvent event) @system 138 { 139 return kameloso.plugins.common.awareness.onMinimalAuthenticationUnknownCommandWHOIS(plugin, event); 140 } 141 } 142 143 144 // onMinimalAuthenticationAccountInfoTarget 145 /++ 146 Replays any queued [kameloso.plugins.common.core.Replay|Replay]s awaiting the result 147 of a WHOIS query. Before that, records the user's services account by 148 saving it to the user's [dialect.defs.IRCClient|IRCClient] in the 149 [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]'s 150 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] associative array. 151 152 [dialect.defs.IRCEvent.Type.RPL_ENDOFWHOIS] is also handled, to 153 cover the case where a user without an account triggering 154 [kameloso.plugins.common.core.Permissions.anyone|Permissions.anyone]- or 155 [kameloso.plugins.common.core.Permissions.ignore|Permissions.ignore]-level commands. 156 157 This function was part of [UserAwareness] but triggering queued replays 158 is too common to conflate with it. 159 +/ 160 void onMinimalAuthenticationAccountInfoTarget(IRCPlugin plugin, const ref IRCEvent event) @system 161 { 162 import kameloso.plugins.common.misc : catchUser; 163 164 // Catch the user here, before replaying anything. 165 catchUser(plugin, event.target); 166 167 // See if there are any queued replays to trigger 168 auto replaysForNickname = event.target.nickname in plugin.state.pendingReplays; 169 if (!replaysForNickname) return; 170 171 scope(exit) 172 { 173 plugin.state.pendingReplays.remove(event.target.nickname); 174 plugin.state.hasPendingReplays = (plugin.state.pendingReplays.length > 0); 175 } 176 177 if (!replaysForNickname.length) return; 178 179 foreach (immutable i, replay; *replaysForNickname) 180 { 181 import kameloso.constants : Timeout; 182 183 if ((event.time - replay.timestamp) >= Timeout.whoisDiscard) 184 { 185 // Stale entry 186 } 187 else 188 { 189 plugin.state.readyReplays ~= replay; 190 } 191 } 192 } 193 194 195 // onMinimalAuthenticationUnknownCommandWHOIS 196 /++ 197 Clears all queued [kameloso.plugins.common.core.Replay|Replay]s if the server 198 says it doesn't support WHOIS at all. 199 200 This is the case with Twitch servers. 201 +/ 202 void onMinimalAuthenticationUnknownCommandWHOIS(IRCPlugin plugin, const ref IRCEvent event) @system 203 { 204 if (event.aux[0] != "WHOIS") return; 205 206 // We're on a server that doesn't support WHOIS 207 // Trigger queued replays of a Permissions.anyone nature, since 208 // they're just Permissions.ignore plus a WHOIS lookup just in case 209 // Then clear everything 210 211 foreach (replaysForNickname; plugin.state.pendingReplays) 212 { 213 foreach (replay; replaysForNickname) 214 { 215 plugin.state.readyReplays ~= replay; 216 } 217 } 218 219 plugin.state.pendingReplays.clear(); 220 plugin.state.hasPendingReplays = false; 221 } 222 223 224 // UserAwareness 225 /++ 226 Implements *user awareness* in a plugin module. 227 228 This maintains a cache of all currently visible users, adding people to it 229 upon discovering them and best-effort culling them when they leave or quit. 230 The cache kept is an associative array, in 231 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users]. 232 233 User awareness implicitly requires 234 [kameloso.plugins.common.awareness.MinimalAuthentication|minimal authentication] 235 and will silently include it if it was not already mixed in. 236 237 Params: 238 channelPolicy = What [kameloso.plugins.common.core.ChannelPolicy|ChannelPolicy] 239 to apply to enwrapped event handlers. 240 debug_ = Whether or not to include debugging output. 241 module_ = String name of the mixing-in module; generally leave as-is. 242 243 See_Also: 244 [kameloso.plugins.common.awareness.MinimalAuthentication|MinimalAuthentication] 245 [kameloso.plugins.common.awareness.ChannelAwareness|ChannelAwareness] 246 +/ 247 mixin template UserAwareness( 248 ChannelPolicy channelPolicy = ChannelPolicy.home, 249 Flag!"debug_" debug_ = No.debug_, 250 string module_ = __MODULE__) 251 { 252 private import dialect.defs : IRCEvent; 253 private static import kameloso.plugins.common.awareness; 254 255 /++ 256 Flag denoting that 257 [kameloso.plugins.common.awareness.UserAwareness|UserAwareness] 258 has been mixed in. 259 +/ 260 package enum hasUserAwareness = true; 261 262 static if (!__traits(compiles, { alias _ = .hasMinimalAuthentication; })) 263 { 264 mixin kameloso.plugins.common.awareness.MinimalAuthentication!(debug_, module_); 265 } 266 267 // onUserAwarenessQuitMixin 268 /++ 269 Proxies to [kameloso.plugins.common.awareness.onUserAwarenessQuit|onUserAwarenessQuit]. 270 271 See_Also: 272 [kameloso.plugins.common.awareness.onUserAwarenessQuit|onUserAwarenessQuit] 273 +/ 274 @(IRCEventHandler() 275 .onEvent(IRCEvent.Type.QUIT) 276 .when(Timing.cleanup) 277 .chainable(true) 278 ) 279 void onUserAwarenessQuitMixin(IRCPlugin plugin, const ref IRCEvent event) @system 280 { 281 return kameloso.plugins.common.awareness.onUserAwarenessQuit(plugin, event); 282 } 283 284 // onUserAwarenessNickMixin 285 /++ 286 Proxies to [kameloso.plugins.common.awareness.onUserAwarenessNick|onUserAwarenessNick]. 287 288 See_Also: 289 [kameloso.plugins.common.awareness.onUserAwarenessNick|onUserAwarenessNick] 290 +/ 291 @(IRCEventHandler() 292 .onEvent(IRCEvent.Type.NICK) 293 .when(Timing.early) 294 .chainable(true) 295 ) 296 void onUserAwarenessNickMixin(IRCPlugin plugin, const ref IRCEvent event) @system 297 { 298 return kameloso.plugins.common.awareness.onUserAwarenessNick(plugin, event); 299 } 300 301 // onUserAwarenessCatchTargetMixin 302 /++ 303 Proxies to [kameloso.plugins.common.awareness.onUserAwarenessCatchTarget|onUserAwarenessCatchTarget]. 304 305 See_Also: 306 [kameloso.plugins.common.awareness.onUserAwarenessCatchTarget|onUserAwarenessCatchTarget] 307 +/ 308 @(IRCEventHandler() 309 .onEvent(IRCEvent.Type.RPL_WHOISUSER) 310 .onEvent(IRCEvent.Type.RPL_WHOREPLY) 311 /*.onEvent(IRCEvent.Type.RPL_WHOISACCOUNT) 312 .onEvent(IRCEvent.Type.RPL_WHOISREGNICK)*/ // Caught in MinimalAuthentication 313 .onEvent(IRCEvent.Type.CHGHOST) 314 .channelPolicy(channelPolicy) 315 .when(Timing.early) 316 .chainable(true) 317 ) 318 void onUserAwarenessCatchTargetMixin(IRCPlugin plugin, const ref IRCEvent event) @system 319 { 320 return kameloso.plugins.common.awareness.onUserAwarenessCatchTarget(plugin, event); 321 } 322 323 // onUserAwarenessCatchSenderMixin 324 /++ 325 Proxies to 326 [kameloso.plugins.common.awareness.onUserAwarenessCatchSender|onUserAwarenessCatchSender]. 327 328 See_Also: 329 [kameloso.plugins.common.awareness.onUserAwarenessCatchSender|onUserAwarenessCatchSender] 330 +/ 331 @(IRCEventHandler() 332 .onEvent(IRCEvent.Type.JOIN) 333 .onEvent(IRCEvent.Type.ACCOUNT) 334 .onEvent(IRCEvent.Type.AWAY) 335 .onEvent(IRCEvent.Type.BACK) 336 /*.onEvent(IRCEvent.Type.CHAN) // Avoid these to be lean; everyone gets indexed by WHO anyway 337 .onEvent(IRCEvent.Type.EMOTE)*/ // ...except on Twitch, but TwitchAwareness has these annotations 338 .channelPolicy(channelPolicy) 339 .when(Timing.setup) 340 .chainable(true) 341 ) 342 void onUserAwarenessCatchSenderMixin(IRCPlugin plugin, const ref IRCEvent event) @system 343 { 344 return kameloso.plugins.common.awareness.onUserAwarenessCatchSender!channelPolicy(plugin, event); 345 } 346 347 // onUserAwarenessNamesReplyMixin 348 /++ 349 Proxies to 350 [kameloso.plugins.common.awareness.onUserAwarenessNamesReply|onUserAwarenessNamesReply]. 351 352 See_Also: 353 [kameloso.plugins.common.awareness.onUserAwarenessNamesReply|onUserAwarenessNamesReply] 354 +/ 355 @(IRCEventHandler() 356 .onEvent(IRCEvent.Type.RPL_NAMREPLY) 357 .channelPolicy(channelPolicy) 358 .when(Timing.early) 359 .chainable(true) 360 ) 361 void onUserAwarenessNamesReplyMixin(IRCPlugin plugin, const ref IRCEvent event) @system 362 { 363 return kameloso.plugins.common.awareness.onUserAwarenessNamesReply(plugin, event); 364 } 365 366 // onUserAwarenessEndOfListMixin 367 /++ 368 Proxies to 369 [kameloso.plugins.common.awareness.onUserAwarenessEndOfList|onUserAwarenessEndOfList]. 370 371 See_Also: 372 [kameloso.plugins.common.awareness.onUserAwarenessEndOfList|onUserAwarenessEndOfList] 373 +/ 374 @(IRCEventHandler() 375 .onEvent(IRCEvent.Type.RPL_ENDOFNAMES) 376 .onEvent(IRCEvent.Type.RPL_ENDOFWHO) 377 .channelPolicy(channelPolicy) 378 .when(Timing.early) 379 .chainable(true) 380 ) 381 void onUserAwarenessEndOfListMixin(IRCPlugin plugin, const ref IRCEvent event) @system 382 { 383 return kameloso.plugins.common.awareness.onUserAwarenessEndOfList(plugin, event); 384 } 385 386 // onUserAwarenessPingMixin 387 /++ 388 Proxies to [kameloso.plugins.common.awareness.onUserAwarenessPing|onUserAwarenessPing]. 389 390 See_Also: 391 [kameloso.plugins.common.awareness.onUserAwarenessPing|onUserAwarenessPing] 392 +/ 393 @(IRCEventHandler() 394 .onEvent(IRCEvent.Type.PING) 395 .when(Timing.early) 396 .chainable(true) 397 ) 398 void onUserAwarenessPingMixin(IRCPlugin plugin, const ref IRCEvent event) @system 399 { 400 return kameloso.plugins.common.awareness.onUserAwarenessPing(plugin, event); 401 } 402 } 403 404 405 // onUserAwarenessQuit 406 /++ 407 Removes a user's [dialect.defs.IRCUser|IRCUser] entry from a plugin's user 408 list upon them disconnecting. 409 +/ 410 void onUserAwarenessQuit(IRCPlugin plugin, const ref IRCEvent event) 411 { 412 plugin.state.users.remove(event.sender.nickname); 413 } 414 415 416 // onUserAwarenessNick 417 /++ 418 Upon someone changing nickname, update their entry in the 419 [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]'s 420 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 421 array to point to the new nickname. 422 423 Removes the old entry after assigning it to the new key. 424 +/ 425 void onUserAwarenessNick(IRCPlugin plugin, const ref IRCEvent event) 426 { 427 if (plugin.state.settings.preferHostmasks) 428 { 429 // Persistence will have set up a complete user with account and everything. 430 // There's no point in copying anything over. 431 } 432 else if (auto oldUser = event.sender.nickname in plugin.state.users) 433 { 434 plugin.state.users[event.target.nickname] = *oldUser; 435 } 436 437 plugin.state.users.remove(event.sender.nickname); 438 } 439 440 441 // onUserAwarenessCatchTarget 442 /++ 443 Catches a user's information and saves it in the plugin's 444 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 445 array of [dialect.defs.IRCUser|IRCUser]s. 446 447 [dialect.defs.IRCEvent.Type.RPL_WHOISUSER] events carry values in 448 the [dialect.defs.IRCUser.updated|IRCUser.updated] field that we want to store. 449 450 [dialect.defs.IRCEvent.Type.CHGHOST] occurs when a user changes host 451 on some servers that allow for custom host addresses. 452 +/ 453 void onUserAwarenessCatchTarget(IRCPlugin plugin, const ref IRCEvent event) 454 { 455 import kameloso.plugins.common.misc : catchUser; 456 catchUser(plugin, event.target); 457 } 458 459 460 // onUserAwarenessCatchSender 461 /++ 462 Adds a user to the [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]'s 463 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] array, 464 potentially including their services account name. 465 466 Servers with the (enabled) capability `extended-join` will include the 467 account name of whoever joins in the event string. If it's there, catch 468 the user into the user array so we don't have to WHOIS them later. 469 +/ 470 void onUserAwarenessCatchSender(ChannelPolicy channelPolicy) 471 (IRCPlugin plugin, const ref IRCEvent event) 472 { 473 import kameloso.plugins.common.misc : catchUser; 474 475 with (IRCEvent.Type) 476 switch (event.type) 477 { 478 case ACCOUNT: 479 case AWAY: 480 case BACK: 481 static if (channelPolicy == ChannelPolicy.home) 482 { 483 // These events don't carry a channel. 484 // Catch if there's already an entry. Trust that it's supposed 485 // to be there if it's there. (RPL_NAMREPLY probably populated it) 486 487 if (event.sender.nickname in plugin.state.users) 488 { 489 catchUser(plugin, event.sender); 490 break; 491 } 492 493 static if (__traits(compiles, { alias _ = .hasChannelAwareness; })) 494 { 495 // Catch the user if it's visible in some channel we're in. 496 497 foreach (const channel; plugin.state.channels) 498 { 499 if (event.sender.nickname in channel.users) 500 { 501 // event is from a user that's in a relevant channel 502 return catchUser(plugin, event.sender); 503 } 504 } 505 } 506 } 507 else /*static if (channelPolicy == ChannelPolicy.any)*/ 508 { 509 // Catch everyone on ChannelPolicy.any 510 catchUser(plugin, event.sender); 511 } 512 break; 513 514 //case JOIN: 515 default: 516 return catchUser(plugin, event.sender); 517 } 518 } 519 520 521 // onUserAwarenessNamesReply 522 /++ 523 Catch users in a reply for the request for a NAMES list of all the 524 participants in a channel. 525 526 Freenode only sends a list of the nicknames but SpotChat sends the full 527 `user!ident@address` information. 528 +/ 529 void onUserAwarenessNamesReply(IRCPlugin plugin, const ref IRCEvent event) 530 { 531 import kameloso.plugins.common.misc : catchUser; 532 import kameloso.irccolours : stripColours; 533 import dialect.common : IRCControlCharacter, stripModesign; 534 import lu.string : contains, nom; 535 import std.algorithm.iteration : splitter; 536 537 if (plugin.state.server.daemon == IRCServer.Daemon.twitch) 538 { 539 // Do nothing actually. Twitch NAMES is unreliable noise. 540 return; 541 } 542 543 auto names = event.content.splitter(' '); 544 545 foreach (immutable userstring; names) 546 { 547 string slice = userstring; // mutable 548 IRCUser user; 549 550 if (!slice.contains('!')) 551 { 552 // No need to check for slice.contains('@')) 553 // Freenode-like, only nicknames with possible modesigns 554 immutable nickname = slice.stripModesign(plugin.state.server); 555 if (nickname == plugin.state.client.nickname) continue; 556 user.nickname = nickname; 557 } 558 else 559 { 560 // SpotChat-like, names are in full nick!ident@address form 561 immutable signed = slice.nom('!'); 562 immutable nickname = signed.stripModesign(plugin.state.server); 563 if (nickname == plugin.state.client.nickname) continue; 564 immutable ident = slice.nom('@'); 565 566 // Do addresses ever contain bold, italics, underlined? 567 immutable address = slice.contains(IRCControlCharacter.colour) ? 568 stripColours(slice) : 569 slice; 570 571 user = IRCUser(nickname, ident, address); 572 } 573 574 catchUser(plugin, user); // this melds with the default conservative strategy 575 } 576 } 577 578 579 // onUserAwarenessEndOfList 580 /++ 581 Rehashes, or optimises, the [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]'s 582 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 583 associative array upon the end of a WHO or a NAMES list. 584 585 These replies can list hundreds of users depending on the size of the 586 channel. Once an associative array has grown sufficiently, it becomes 587 inefficient. Rehashing it makes it take its new size into account and 588 makes lookup faster. 589 +/ 590 void onUserAwarenessEndOfList(IRCPlugin plugin, const ref IRCEvent event) @system 591 { 592 import kameloso.plugins.common.misc : rehashUsers; 593 594 // Pass a channel name so only that channel is rehashed 595 rehashUsers(plugin, event.channel); 596 } 597 598 599 // onUserAwarenessPingMixin 600 /++ 601 Rehash the internal [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 602 associative array of [dialect.defs.IRCUser|IRCUser]s, once every 603 [kameloso.constants.Periodicals.userAARehashMinutes|Periodicals.userAARehashMinutes] minutes. 604 605 We ride the periodicity of [dialect.defs.IRCEvent.Type.PING|PING] to get 606 a natural cadence without having to resort to queued 607 [kameloso.thread.ScheduledFiber|ScheduledFiber]s. 608 609 The number of hours is so far hardcoded but can be made configurable if 610 there's a use-case for it. 611 +/ 612 void onUserAwarenessPing(IRCPlugin plugin, const ref IRCEvent event) @system 613 { 614 import std.datetime.systime : Clock; 615 616 enum minutesBeforeInitialRehash = 5; 617 618 static long pingRehash; 619 620 if (pingRehash == 0L) 621 { 622 // First PING encountered 623 // Delay rehashing to let the client join all channels 624 pingRehash = event.time + (minutesBeforeInitialRehash * 60); 625 } 626 else if (event.time >= pingRehash) 627 { 628 import kameloso.constants : Periodicals; 629 import kameloso.plugins.common.misc : rehashUsers; 630 631 // Once every `userAARehashMinutes` minutes, rehash the `users` array. 632 rehashUsers(plugin); 633 pingRehash = event.time + (Periodicals.userAARehashMinutes * 60); 634 } 635 } 636 637 638 // ChannelAwareness 639 /++ 640 Implements *channel awareness* in a plugin module. 641 642 This maintains a cache of all current channels, their topics and modes, and 643 their participants. The cache kept is an associative array, in 644 [kameloso.plugins.common.core.IRCPluginState.channels|IRCPluginState.channels]. 645 646 Channel awareness explicitly requires 647 [kameloso.plugins.common.awareness.UserAwareness|user awareness] and will 648 halt compilation if it is not also mixed in. 649 650 Params: 651 channelPolicy = What [kameloso.plugins.common.core.ChannelPolicy|ChannelPolicy] 652 to apply to enwrapped event handlers. 653 debug_ = Whether or not to include debugging output. 654 module_ = String name of the mixing-in module; generally leave as-is. 655 656 See_Also: 657 [kameloso.plugins.common.awareness.MinimalAuthentication|MinimalAuthentication] 658 [kameloso.plugins.common.awareness.UserAwareness|UserAwareness] 659 +/ 660 mixin template ChannelAwareness( 661 ChannelPolicy channelPolicy = ChannelPolicy.home, 662 Flag!"debug_" debug_ = No.debug_, 663 string module_ = __MODULE__) 664 { 665 private import dialect.defs : IRCEvent; 666 private static import kameloso.plugins.common.awareness; 667 668 /++ 669 Flag denoting that [kameloso.plugins.common.awareness.ChannelAwareness|ChannelAwareness] 670 has been mixed in. 671 +/ 672 package enum hasChannelAwareness = true; 673 674 static if (!__traits(compiles, { alias _ = .hasUserAwareness; })) 675 { 676 import std.format : format; 677 678 enum pattern = "`%s` is missing a `UserAwareness` mixin " ~ 679 "(needed for `ChannelAwareness`)"; 680 enum message = pattern.format(module_); 681 static assert(0, message); 682 } 683 684 // onChannelAwarenessSelfjoinMixin 685 /++ 686 Proxies to 687 [kameloso.plugins.common.awareness.onChannelAwarenessSelfjoin|onChannelAwarenessSelfjoin]. 688 689 See_Also: 690 [kameloso.plugins.common.awareness.onChannelAwarenessSelfjoin|onChannelAwarenessSelfjoin] 691 +/ 692 @(IRCEventHandler() 693 .onEvent(IRCEvent.Type.SELFJOIN) 694 .channelPolicy(channelPolicy) 695 .when(Timing.setup) 696 .chainable(true) 697 ) 698 void onChannelAwarenessSelfjoinMixin(IRCPlugin plugin, const ref IRCEvent event) @system 699 { 700 return kameloso.plugins.common.awareness.onChannelAwarenessSelfjoin(plugin, event); 701 } 702 703 // onChannelAwarenessSelfpartMixin 704 /++ 705 Proxies to 706 [kameloso.plugins.common.awareness.onChannelAwarenessSelfpart|onChannelAwarenessSelfpart]. 707 708 See_Also: 709 [kameloso.plugins.common.awareness.onChannelAwarenessSelfpart|onChannelAwarenessSelfpart] 710 +/ 711 @(IRCEventHandler() 712 .onEvent(IRCEvent.Type.SELFPART) 713 .onEvent(IRCEvent.Type.SELFKICK) 714 .channelPolicy(channelPolicy) 715 .when(Timing.cleanup) 716 .chainable(true) 717 ) 718 void onChannelAwarenessSelfpartMixin(IRCPlugin plugin, const ref IRCEvent event) @system 719 { 720 return kameloso.plugins.common.awareness.onChannelAwarenessSelfpart(plugin, event); 721 } 722 723 // onChannelAwarenessJoinMixin 724 /++ 725 Proxies to [kameloso.plugins.common.awareness.onChannelAwarenessJoin|onChannelAwarenessJoin]. 726 727 See_Also: 728 [kameloso.plugins.common.awareness.onChannelAwarenessJoin|onChannelAwarenessJoin] 729 +/ 730 @(IRCEventHandler() 731 .onEvent(IRCEvent.Type.JOIN) 732 .channelPolicy(channelPolicy) 733 .when(Timing.setup) 734 .chainable(true) 735 ) 736 void onChannelAwarenessJoinMixin(IRCPlugin plugin, const ref IRCEvent event) @system 737 { 738 return kameloso.plugins.common.awareness.onChannelAwarenessJoin(plugin, event); 739 } 740 741 // onChannelAwarenessPartMixin 742 /++ 743 Proxies to [kameloso.plugins.common.awareness.onChannelAwarenessPart|onChannelAwarenessPart]. 744 745 See_Also: 746 [kameloso.plugins.common.awareness.onChannelAwarenessPart|onChannelAwarenessPart] 747 +/ 748 @(IRCEventHandler() 749 .onEvent(IRCEvent.Type.PART) 750 .channelPolicy(channelPolicy) 751 .when(Timing.late) 752 .chainable(true) 753 ) 754 void onChannelAwarenessPartMixin(IRCPlugin plugin, const ref IRCEvent event) @system 755 { 756 return kameloso.plugins.common.awareness.onChannelAwarenessPart(plugin, event); 757 } 758 759 // onChannelAwarenessNickMixin 760 /++ 761 Proxies to [kameloso.plugins.common.awareness.onChannelAwarenessNick|onChannelAwarenessNick]. 762 763 See_Also: 764 [kameloso.plugins.common.awareness.onChannelAwarenessNick|onChannelAwarenessNick] 765 +/ 766 @(IRCEventHandler() 767 .onEvent(IRCEvent.Type.NICK) 768 .when(Timing.setup) 769 .chainable(true) 770 ) 771 void onChannelAwarenessNickMixin(IRCPlugin plugin, const ref IRCEvent event) @system 772 { 773 return kameloso.plugins.common.awareness.onChannelAwarenessNick(plugin, event); 774 } 775 776 // onChannelAwarenessQuitMixin 777 /++ 778 Proxies to [kameloso.plugins.common.awareness.onChannelAwarenessQuit|onChannelAwarenessQuit]. 779 780 See_Also: 781 [kameloso.plugins.common.awareness.onChannelAwarenessQuit|onChannelAwarenessQuit] 782 +/ 783 @(IRCEventHandler() 784 .onEvent(IRCEvent.Type.QUIT) 785 .when(Timing.late) 786 .chainable(true) 787 ) 788 void onChannelAwarenessQuitMixin(IRCPlugin plugin, const ref IRCEvent event) @system 789 { 790 return kameloso.plugins.common.awareness.onChannelAwarenessQuit(plugin, event); 791 } 792 793 // onChannelAwarenessTopicMixin 794 /++ 795 Proxies to [kameloso.plugins.common.awareness.onChannelAwarenessTopic|onChannelAwarenessTopic]. 796 797 See_Also: 798 [kameloso.plugins.common.awareness.onChannelAwarenessTopic|onChannelAwarenessTopic] 799 +/ 800 @(IRCEventHandler() 801 .onEvent(IRCEvent.Type.TOPIC) 802 .onEvent(IRCEvent.Type.RPL_TOPIC) 803 .channelPolicy(channelPolicy) 804 .when(Timing.early) 805 .chainable(true) 806 ) 807 void onChannelAwarenessTopicMixin(IRCPlugin plugin, const ref IRCEvent event) @system 808 { 809 return kameloso.plugins.common.awareness.onChannelAwarenessTopic(plugin, event); 810 } 811 812 // onChannelAwarenessCreationTimeMixin 813 /++ 814 Proxies to 815 [kameloso.plugins.common.awareness.onChannelAwarenessCreationTime|onChannelAwarenessCreationTime]. 816 817 See_Also: 818 [kameloso.plugins.common.awareness.onChannelAwarenessCreationTime|onChannelAwarenessCreationTime] 819 +/ 820 @(IRCEventHandler() 821 .onEvent(IRCEvent.Type.RPL_CREATIONTIME) 822 .channelPolicy(channelPolicy) 823 .when(Timing.early) 824 .chainable(true) 825 ) 826 void onChannelAwarenessCreationTimeMixin(IRCPlugin plugin, const ref IRCEvent event) @system 827 { 828 return kameloso.plugins.common.awareness.onChannelAwarenessCreationTime(plugin, event); 829 } 830 831 // onChannelAwarenessModeMixin 832 /++ 833 Proxies to [kameloso.plugins.common.awareness.onChannelAwarenessMode|onChannelAwarenessMode]. 834 835 See_Also: 836 [kameloso.plugins.common.awareness.onChannelAwarenessMode|onChannelAwarenessMode] 837 +/ 838 @(IRCEventHandler() 839 .onEvent(IRCEvent.Type.MODE) 840 .channelPolicy(channelPolicy) 841 .when(Timing.early) 842 .chainable(true) 843 ) 844 void onChannelAwarenessModeMixin(IRCPlugin plugin, const ref IRCEvent event) @system 845 { 846 return kameloso.plugins.common.awareness.onChannelAwarenessMode(plugin, event); 847 } 848 849 // onChannelAwarenessWhoReplyMixin 850 /++ 851 Proxies to 852 [kameloso.plugins.common.awareness.onChannelAwarenessWhoReply|onChannelAwarenessWhoReply]. 853 854 See_Also: 855 [kameloso.plugins.common.awareness.onChannelAwarenessWhoReply|onChannelAwarenessWhoReply] 856 +/ 857 @(IRCEventHandler() 858 .onEvent(IRCEvent.Type.RPL_WHOREPLY) 859 .channelPolicy(channelPolicy) 860 .when(Timing.early) 861 .chainable(true) 862 ) 863 void onChannelAwarenessWhoReplyMixin(IRCPlugin plugin, const ref IRCEvent event) @system 864 { 865 return kameloso.plugins.common.awareness.onChannelAwarenessWhoReply(plugin, event); 866 } 867 868 // onChannelAwarenessNamesReplyMixin 869 /++ 870 Proxies to 871 [kameloso.plugins.common.awareness.onChannelAwarenessNamesReply|onChannelAwarenessNamesReply]. 872 873 See_Also: 874 [kameloso.plugins.common.awareness.onChannelAwarenessNamesReply|onChannelAwarenessNamesReply] 875 +/ 876 @(IRCEventHandler() 877 .onEvent(IRCEvent.Type.RPL_NAMREPLY) 878 .channelPolicy(channelPolicy) 879 .when(Timing.early) 880 .chainable(true) 881 ) 882 void onChannelAwarenessNamesReplyMixin(IRCPlugin plugin, const ref IRCEvent event) @system 883 { 884 return kameloso.plugins.common.awareness.onChannelAwarenessNamesReply(plugin, event); 885 } 886 887 // onChannelAwarenessModeListsMixin 888 /++ 889 Proxies to 890 [kameloso.plugins.common.awareness.onChannelAwarenessModeLists|onChannelAwarenessModeLists]. 891 892 See_Also: 893 [kameloso.plugins.common.awareness.onChannelAwarenessModeLists|onChannelAwarenessModeLists] 894 +/ 895 @(IRCEventHandler() 896 .onEvent(IRCEvent.Type.RPL_BANLIST) 897 .onEvent(IRCEvent.Type.RPL_EXCEPTLIST) 898 .onEvent(IRCEvent.Type.RPL_INVITELIST) 899 .onEvent(IRCEvent.Type.RPL_REOPLIST) 900 .onEvent(IRCEvent.Type.RPL_QUIETLIST) 901 .channelPolicy(channelPolicy) 902 .when(Timing.early) 903 .chainable(true) 904 ) 905 void onChannelAwarenessModeListsMixin(IRCPlugin plugin, const ref IRCEvent event) @system 906 { 907 return kameloso.plugins.common.awareness.onChannelAwarenessModeLists(plugin, event); 908 } 909 910 // onChannelAwarenessChannelModeIsMixin 911 /++ 912 Proxies to 913 [kameloso.plugins.common.awareness.onChannelAwarenessChannelModeIs|onChannelAwarenessChannelModeIs]. 914 915 See_Also: 916 [kameloso.plugins.common.awareness.onChannelAwarenessChannelModeIs|onChannelAwarenessChannelModeIs] 917 +/ 918 @(IRCEventHandler() 919 .onEvent(IRCEvent.Type.RPL_CHANNELMODEIS) 920 .channelPolicy(channelPolicy) 921 .when(Timing.early) 922 .chainable(true) 923 ) 924 void onChannelAwarenessChannelModeIsMixin(IRCPlugin plugin, const ref IRCEvent event) @system 925 { 926 return kameloso.plugins.common.awareness.onChannelAwarenessChannelModeIs(plugin, event); 927 } 928 } 929 930 931 // onChannelAwarenessSelfjoin 932 /++ 933 Create a new [dialect.defs.IRCChannel|IRCChannel] in the the 934 [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]'s 935 [kameloso.plugins.common.core.IRCPluginState.channels|IRCPluginState.channels] 936 associative array when the bot joins a channel. 937 +/ 938 void onChannelAwarenessSelfjoin(IRCPlugin plugin, const ref IRCEvent event) 939 { 940 if (event.channel in plugin.state.channels) return; 941 942 plugin.state.channels[event.channel] = IRCChannel.init; 943 plugin.state.channels[event.channel].name = event.channel; 944 } 945 946 947 // onChannelAwarenessSelfpart 948 /++ 949 Removes an [dialect.defs.IRCChannel|IRCChannel] from the internal list when the 950 bot leaves it. 951 952 Remove users from the [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 953 array if, by leaving, it left the last channel we can observe it from, so as 954 not to leak users. It can be argued that this should be part of user awareness, 955 however this would not be possible if it were not for channel-tracking. 956 As such keep the behaviour in channel awareness. 957 +/ 958 void onChannelAwarenessSelfpart(IRCPlugin plugin, const ref IRCEvent event) 959 { 960 // On Twitch SELFPART may occur on untracked channels 961 auto channel = event.channel in plugin.state.channels; 962 if (!channel) return; 963 964 nickloop: 965 foreach (immutable nickname; channel.users.byKey) 966 { 967 foreach (immutable stateChannelName, const stateChannel; plugin.state.channels) 968 { 969 if (stateChannelName == event.channel) continue; 970 if (nickname in stateChannel.users) continue nickloop; 971 } 972 973 // nickname is not in any of our other tracked channels; remove 974 plugin.state.users.remove(nickname); 975 } 976 977 plugin.state.channels.remove(event.channel); 978 } 979 980 981 // onChannelAwarenessJoin 982 /++ 983 Adds a user as being part of a channel when they join it. 984 +/ 985 void onChannelAwarenessJoin(IRCPlugin plugin, const ref IRCEvent event) 986 { 987 auto channel = event.channel in plugin.state.channels; 988 if (!channel) return; 989 990 channel.users[event.sender.nickname] = true; 991 } 992 993 994 // onChannelAwarenessPart 995 /++ 996 Removes a user from being part of a channel when they leave it. 997 998 Remove the user from the [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 999 array if, by leaving, it left the last channel we can observe it from, so as 1000 not to leak users. It can be argued that this should be part of user awareness, 1001 however this would not be possible if it were not for channel-tracking. 1002 As such keep the behaviour in channel awareness. 1003 +/ 1004 void onChannelAwarenessPart(IRCPlugin plugin, const ref IRCEvent event) 1005 { 1006 auto channel = event.channel in plugin.state.channels; 1007 if (!channel) return; 1008 1009 if (event.sender.nickname !in channel.users) 1010 { 1011 // On Twitch servers with no NAMES on joining a channel, users 1012 // that you haven't seen may leave despite never having been seen 1013 return; 1014 } 1015 1016 channel.users.remove(event.sender.nickname); 1017 1018 // Remove entries in the mods AA (ops, halfops, voice, ...) 1019 foreach (/*immutable prefixChar,*/ ref prefixMods; channel.mods) 1020 { 1021 foreach (immutable modNickname, _; prefixMods) 1022 { 1023 prefixMods.remove(event.sender.nickname); 1024 } 1025 } 1026 1027 foreach (const foreachChannel; plugin.state.channels) 1028 { 1029 if (event.sender.nickname in foreachChannel.users) return; 1030 } 1031 1032 // event.sender is not in any of our tracked channels; remove 1033 plugin.state.users.remove(event.sender.nickname); 1034 } 1035 1036 1037 // onChannelAwarenessNick 1038 /++ 1039 Upon someone changing nickname, update their entry in the 1040 [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 1041 associative array to point to the new nickname. 1042 1043 Does *not* add a new entry if one doesn't exits, to counter the fact 1044 that [dialect.defs.IRCEvent.Type.NICK] events don't belong to a channel, 1045 and as such can't be regulated with [kameloso.plugins.common.core.ChannelPolicy|ChannelPolicy] 1046 annotations. This way the user will only be moved if it was already added elsewhere. 1047 Else we'll leak users. 1048 1049 Removes the old entry after assigning it to the new key. 1050 +/ 1051 void onChannelAwarenessNick(IRCPlugin plugin, const ref IRCEvent event) 1052 { 1053 // User awareness bits take care of the IRCPluginState.users AA 1054 1055 foreach (ref channel; plugin.state.channels) 1056 { 1057 if (event.sender.nickname !in channel.users) continue; 1058 1059 channel.users.remove(event.sender.nickname); 1060 channel.users[event.target.nickname] = true; 1061 } 1062 } 1063 1064 1065 // onChannelAwarenessQuit 1066 /++ 1067 Removes a user from all tracked channels if they disconnect. 1068 1069 Does not touch the internal list of users; the user awareness bits are 1070 expected to take care of that. 1071 +/ 1072 void onChannelAwarenessQuit(IRCPlugin plugin, const ref IRCEvent event) 1073 { 1074 foreach (ref channel; plugin.state.channels) 1075 { 1076 channel.users.remove(event.sender.nickname); 1077 } 1078 } 1079 1080 1081 // onChannelAwarenessTopic 1082 /++ 1083 Update the entry for an [dialect.defs.IRCChannel|IRCChannel] if someone changes 1084 the topic of it. 1085 +/ 1086 void onChannelAwarenessTopic(IRCPlugin plugin, const ref IRCEvent event) 1087 { 1088 auto channel = event.channel in plugin.state.channels; 1089 if (!channel) return; 1090 1091 channel.topic = event.content; // don't strip 1092 } 1093 1094 1095 // onChannelAwarenessCreationTime 1096 /++ 1097 Stores the timestamp of when a channel was created. 1098 +/ 1099 void onChannelAwarenessCreationTime(IRCPlugin plugin, const ref IRCEvent event) 1100 { 1101 auto channel = event.channel in plugin.state.channels; 1102 if (!channel) return; 1103 1104 channel.created = event.count[0].get; 1105 } 1106 1107 1108 // onChannelAwarenessMode 1109 /++ 1110 Sets a mode for a channel. 1111 1112 Most modes replace others of the same type, notable exceptions being 1113 bans and mode exemptions. We let [dialect.common.setMode] take care of that. 1114 +/ 1115 void onChannelAwarenessMode(IRCPlugin plugin, const ref IRCEvent event) 1116 { 1117 version(TwitchSupport) 1118 { 1119 if (plugin.state.server.daemon == IRCServer.Daemon.twitch) 1120 { 1121 // Twitch modes are unpredictable. Ignore and rely on badges instead. 1122 return; 1123 } 1124 } 1125 1126 auto channel = event.channel in plugin.state.channels; 1127 if (!channel) return; 1128 1129 import dialect.common : setMode; 1130 (*channel).setMode(event.aux[0], event.content, plugin.state.server); 1131 } 1132 1133 1134 // onChannelAwarenessWhoReply 1135 /++ 1136 Adds a user as being part of a channel upon receiving the reply from the 1137 request for info on all the participants. 1138 1139 This events includes all normal fields like ident and address, but not 1140 their channel modes (e.g. `@` for operator). 1141 +/ 1142 void onChannelAwarenessWhoReply(IRCPlugin plugin, const ref IRCEvent event) 1143 { 1144 import std.string : representation; 1145 1146 auto channel = event.channel in plugin.state.channels; 1147 if (!channel) return; 1148 1149 // User awareness bits add the IRCUser 1150 if (event.aux[0].length) 1151 { 1152 // Register operators, half-ops, voiced etc 1153 // Can be more than one if multi-prefix capability is enabled 1154 // Server-sent string, can assume ASCII (@,%,+...) and go char by char 1155 foreach (immutable modesign; event.aux[0].representation) 1156 { 1157 if (const modechar = modesign in plugin.state.server.prefixchars) 1158 { 1159 import dialect.common : setMode; 1160 import std.conv : to; 1161 1162 immutable modestring = (*modechar).to!string; 1163 (*channel).setMode(modestring, event.target.nickname, plugin.state.server); 1164 } 1165 else 1166 { 1167 //logger.warning("Invalid modesign in RPL_WHOREPLY: ", modesign); 1168 } 1169 } 1170 } 1171 1172 if (event.target.nickname == plugin.state.client.nickname) return; 1173 1174 // In case no mode was applied 1175 channel.users[event.target.nickname] = true; 1176 } 1177 1178 1179 // onChannelAwarenessNamesReply 1180 /++ 1181 Adds users as being part of a channel upon receiving the reply from the 1182 request for a list of all the participants. 1183 1184 On some servers this does not include information about the users, only 1185 their nickname and their channel mode (e.g. `@` for operator), but other 1186 servers express the users in the full `user!ident@address` form. 1187 +/ 1188 void onChannelAwarenessNamesReply(IRCPlugin plugin, const ref IRCEvent event) 1189 { 1190 import dialect.common : stripModesign; 1191 import lu.string : contains; 1192 import std.algorithm.iteration : splitter; 1193 import std.string : representation; 1194 1195 if (!event.content.length) return; 1196 1197 auto channel = event.channel in plugin.state.channels; 1198 if (!channel) return; 1199 1200 auto names = event.content.splitter(' '); 1201 1202 foreach (immutable userstring; names) 1203 { 1204 string slice = userstring; 1205 string nickname; 1206 1207 if (userstring.contains('!'))// && userstring.contains('@')) // No need to check both 1208 { 1209 import lu.string : nom; 1210 // SpotChat-like, names are in full nick!ident@address form 1211 nickname = slice.nom('!'); 1212 } 1213 else 1214 { 1215 // Freenode-like, only a nickname with possible @%+ prefix 1216 nickname = userstring; 1217 } 1218 1219 string modesigns; // mutable 1220 nickname = nickname.stripModesign(plugin.state.server, modesigns); 1221 1222 // Register operators, half-ops, voiced etc 1223 // Can be more than one if multi-prefix capability is enabled 1224 // Server-sent string, can assume ASCII (@,%,+...) and go char by char 1225 foreach (immutable modesign; modesigns.representation) 1226 { 1227 if (const modechar = modesign in plugin.state.server.prefixchars) 1228 { 1229 import dialect.common : setMode; 1230 import std.conv : to; 1231 1232 immutable modestring = (*modechar).to!string; 1233 (*channel).setMode(modestring, nickname, plugin.state.server); 1234 } 1235 else 1236 { 1237 //logger.warning("Invalid modesign in RPL_NAMREPLY: ", modesign); 1238 } 1239 } 1240 1241 channel.users[nickname] = true; 1242 } 1243 } 1244 1245 1246 // onChannelAwarenessModeLists 1247 /++ 1248 Adds users of a certain "list" mode to a tracked channel's list of modes 1249 (banlist, exceptlist, invitelist, etc). 1250 +/ 1251 void onChannelAwarenessModeLists(IRCPlugin plugin, const ref IRCEvent event) 1252 { 1253 import dialect.common : setMode; 1254 import std.conv : to; 1255 1256 // :kornbluth.freenode.net 367 kameloso #flerrp huerofi!*@* zorael!~NaN@2001:41d0:2:80b4:: 1513899527 1257 // :kornbluth.freenode.net 367 kameloso #flerrp harbl!harbl@snarbl.com zorael!~NaN@2001:41d0:2:80b4:: 1513899521 1258 // :niven.freenode.net 346 kameloso^ #flerrp asdf!fdas@asdf.net zorael!~NaN@2001:41d0:2:80b4:: 1514405089 1259 // :niven.freenode.net 728 kameloso^ #flerrp q qqqq!*@asdf.net zorael!~NaN@2001:41d0:2:80b4:: 1514405101 1260 1261 auto channel = event.channel in plugin.state.channels; 1262 if (!channel) return; 1263 1264 with (IRCEvent.Type) 1265 { 1266 string modestring; 1267 1268 switch (event.type) 1269 { 1270 case RPL_BANLIST: 1271 modestring = "b"; 1272 break; 1273 1274 case RPL_EXCEPTLIST: 1275 modestring = (plugin.state.server.exceptsChar == 'e') ? 1276 "e" : plugin.state.server.exceptsChar.to!string; 1277 break; 1278 1279 case RPL_INVITELIST: 1280 modestring = (plugin.state.server.invexChar == 'I') ? 1281 "I" : plugin.state.server.invexChar.to!string; 1282 break; 1283 1284 case RPL_REOPLIST: 1285 modestring = "R"; 1286 break; 1287 1288 case RPL_QUIETLIST: 1289 modestring = "q"; 1290 break; 1291 1292 default: 1293 assert(0, "Unexpected IRC event type annotation on " ~ 1294 "`onChannelAwarenessModeListMixin`"); 1295 } 1296 1297 (*channel).setMode(modestring, event.content, plugin.state.server); 1298 } 1299 } 1300 1301 1302 // onChannelAwarenessChannelModeIs 1303 /++ 1304 Adds the modes of a channel to a tracked channel's mode list. 1305 +/ 1306 void onChannelAwarenessChannelModeIs(IRCPlugin plugin, const ref IRCEvent event) 1307 { 1308 auto channel = event.channel in plugin.state.channels; 1309 if (!channel) return; 1310 1311 import dialect.common : setMode; 1312 // :niven.freenode.net 324 kameloso^ ##linux +CLPcnprtf ##linux-overflow 1313 (*channel).setMode(event.aux[0], event.content, plugin.state.server); 1314 } 1315 1316 1317 // TwitchAwareness 1318 /++ 1319 Implements scraping of Twitch message events for user details in a module. 1320 1321 Twitch doesn't always enumerate channel participants upon joining a channel. 1322 It seems to mostly be done on larger channels, and only rarely when the 1323 channel is small. 1324 1325 There is a chance of a user leak, if parting users are not broadcast. As 1326 such we mark when the user was last seen in the 1327 [dialect.defs.IRCUser.updated|IRCUser.updated] member, which opens up the possibility 1328 of pruning the plugin's [kameloso.plugins.common.core.IRCPluginState.users|IRCPluginState.users] 1329 array of old entries. 1330 1331 Twitch awareness needs channel awareness, or it is meaningless. 1332 1333 Params: 1334 channelPolicy = What [kameloso.plugins.common.core.ChannelPolicy|ChannelPolicy] 1335 to apply to enwrapped event handlers. 1336 debug_ = Whether or not to include debugging output. 1337 module_ = String name of the mixing-in module; generally leave as-is. 1338 1339 See_Also: 1340 [kameloso.plugins.common.awareness.MinimalAuthentication|MinimalAuthentication] 1341 [kameloso.plugins.common.awareness.UserAwareness|UserAwareness] 1342 [kameloso.plugins.common.awareness.ChannelAwareness|ChannelAwareness] 1343 +/ 1344 version(TwitchSupport) 1345 mixin template TwitchAwareness( 1346 ChannelPolicy channelPolicy = ChannelPolicy.home, 1347 Flag!"debug_" debug_ = No.debug_, 1348 string module_ = __MODULE__) 1349 { 1350 private import dialect.defs : IRCEvent; 1351 private static import kameloso.plugins.common.awareness; 1352 1353 /++ 1354 Flag denoting that [kameloso.plugins.common.awareness.TwitchAwareness|TwitchAwareness] 1355 has been mixed in. 1356 +/ 1357 package enum hasTwitchAwareness = true; 1358 1359 static if (!__traits(compiles, { alias _ = .hasChannelAwareness; })) 1360 { 1361 import std.format : format; 1362 1363 enum pattern = "`%s` is missing a `ChannelAwareness` mixin " ~ 1364 "(needed for `TwitchAwareness`)"; 1365 enum message = pattern.format(module_); 1366 static assert(0, message); 1367 } 1368 1369 // onTwitchAwarenessSenderCarryingEventMixin 1370 /++ 1371 Proxies to 1372 [kameloso.plugins.common.awareness.onTwitchAwarenessSenderCarryingEvent|onTwitchAwarenessSenderCarryingEvent]. 1373 1374 See_Also: 1375 [kameloso.plugins.common.awareness.onTwitchAwarenessSenderCarryingEvent|onTwitchAwarenessSenderCarryingEvent] 1376 +/ 1377 @(IRCEventHandler() 1378 .onEvent(IRCEvent.Type.CHAN) // Catch these as we don't index people by WHO on Twitch 1379 .onEvent(IRCEvent.Type.JOIN) 1380 .onEvent(IRCEvent.Type.SELFJOIN) 1381 .onEvent(IRCEvent.Type.PART) 1382 .onEvent(IRCEvent.Type.EMOTE) // As above 1383 .onEvent(IRCEvent.Type.TWITCH_SUB) 1384 .onEvent(IRCEvent.Type.TWITCH_CHEER) 1385 .onEvent(IRCEvent.Type.TWITCH_SUBGIFT) 1386 .onEvent(IRCEvent.Type.TWITCH_BITSBADGETIER) 1387 .onEvent(IRCEvent.Type.TWITCH_RAID) 1388 .onEvent(IRCEvent.Type.TWITCH_UNRAID) 1389 .onEvent(IRCEvent.Type.TWITCH_RITUAL) 1390 .onEvent(IRCEvent.Type.TWITCH_REWARDGIFT) 1391 .onEvent(IRCEvent.Type.TWITCH_GIFTCHAIN) 1392 .onEvent(IRCEvent.Type.TWITCH_SUBUPGRADE) 1393 .onEvent(IRCEvent.Type.TWITCH_CHARITY) 1394 .onEvent(IRCEvent.Type.TWITCH_BULKGIFT) 1395 .onEvent(IRCEvent.Type.TWITCH_EXTENDSUB) 1396 .onEvent(IRCEvent.Type.TWITCH_GIFTRECEIVED) 1397 .onEvent(IRCEvent.Type.TWITCH_PAYFORWARD) 1398 .onEvent(IRCEvent.Type.TWITCH_CROWDCHANT) 1399 .onEvent(IRCEvent.Type.TWITCH_ANNOUNCEMENT) 1400 .onEvent(IRCEvent.Type.TWITCH_DIRECTCHEER) 1401 .channelPolicy(channelPolicy) 1402 .when(Timing.early) 1403 .chainable(true) 1404 ) 1405 void onTwitchAwarenessSenderCarryingEventMixin(IRCPlugin plugin, const ref IRCEvent event) @system 1406 { 1407 return kameloso.plugins.common.awareness.onTwitchAwarenessSenderCarryingEvent(plugin, event); 1408 } 1409 1410 // onTwitchAwarenessTargetCarryingEventMixin 1411 /++ 1412 Catch targets from normal Twitch events. 1413 1414 This has to be done on certain Twitch channels whose participants are 1415 not enumerated upon joining it, nor joins or parts announced. By 1416 listening for any message with targets and catching that user that way 1417 we ensure we do our best to scrape the channels. 1418 1419 See_Also: 1420 [kameloso.plugins.common.awareness.onTwitchAwarenessSenderCarryingEvent|onTwitchAwarenessSenderCarryingEvent] 1421 +/ 1422 @(IRCEventHandler() 1423 .onEvent(IRCEvent.Type.TWITCH_BAN) 1424 .onEvent(IRCEvent.Type.TWITCH_SUBGIFT) 1425 .onEvent(IRCEvent.Type.TWITCH_REWARDGIFT) 1426 .onEvent(IRCEvent.Type.TWITCH_TIMEOUT) 1427 .onEvent(IRCEvent.Type.TWITCH_GIFTCHAIN) 1428 .onEvent(IRCEvent.Type.TWITCH_GIFTRECEIVED) 1429 .onEvent(IRCEvent.Type.TWITCH_PAYFORWARD) 1430 .onEvent(IRCEvent.Type.CLEARMSG) 1431 .onEvent(IRCEvent.Type.GLOBALUSERSTATE) 1432 .channelPolicy(channelPolicy) 1433 .when(Timing.early) 1434 .chainable(true) 1435 ) 1436 void onTwitchAwarenessTargetCarryingEventMixin(IRCPlugin plugin, const ref IRCEvent event) @system 1437 { 1438 return kameloso.plugins.common.awareness.onTwitchAwarenessTargetCarryingEvent(plugin, event); 1439 } 1440 } 1441 1442 1443 // onTwitchAwarenessSenderCarryingEvent 1444 /++ 1445 Catch senders from normal Twitch events. 1446 1447 This has to be done on certain Twitch channels whose participants are 1448 not enumerated upon joining it, nor joins or parts announced. By 1449 listening for any message and catching the user that way we ensure we 1450 do our best to scrape the channels. 1451 1452 See_Also: 1453 [kameloso.plugins.common.awareness.onTwitchAwarenessTargetCarryingEvent|onTwitchAwarenessTargetCarryingEvent] 1454 +/ 1455 version(TwitchSupport) 1456 void onTwitchAwarenessSenderCarryingEvent(IRCPlugin plugin, const ref IRCEvent event) 1457 { 1458 import kameloso.plugins.common.misc : catchUser; 1459 1460 if (plugin.state.server.daemon != IRCServer.Daemon.twitch) return; 1461 1462 if (!event.sender.nickname) return; 1463 1464 // Move the catchUser call here to populate the users array with users in guest channels 1465 //catchUser(plugin, event.sender); 1466 1467 auto channel = event.channel in plugin.state.channels; 1468 if (!channel) return; 1469 1470 if (event.sender.nickname !in channel.users) 1471 { 1472 channel.users[event.sender.nickname] = true; 1473 } 1474 1475 catchUser(plugin, event.sender); // <-- this one 1476 } 1477 1478 1479 // onTwitchAwarenessTargetCarryingEvent 1480 /++ 1481 Catch targets from normal Twitch events. 1482 1483 This has to be done on certain Twitch channels whose participants are 1484 not enumerated upon joining it, nor joins or parts announced. By 1485 listening for any message with targets and catching that user that way 1486 we ensure we do our best to scrape the channels. 1487 1488 See_Also: 1489 [kameloso.plugins.common.awareness.onTwitchAwarenessSenderCarryingEvent|onTwitchAwarenessSenderCarryingEvent] 1490 +/ 1491 version(TwitchSupport) 1492 void onTwitchAwarenessTargetCarryingEvent(IRCPlugin plugin, const ref IRCEvent event) 1493 { 1494 import kameloso.plugins.common.misc : catchUser; 1495 1496 if (plugin.state.server.daemon != IRCServer.Daemon.twitch) return; 1497 1498 if (!event.target.nickname) return; 1499 1500 // Move the catchUser call here to populate the users array with users in guest channels 1501 //catchUser(plugin, event.target); 1502 1503 auto channel = event.channel in plugin.state.channels; 1504 if (!channel) return; 1505 1506 if (event.target.nickname !in channel.users) 1507 { 1508 channel.users[event.target.nickname] = true; 1509 } 1510 1511 catchUser(plugin, event.target); // <-- this one 1512 } 1513 1514 1515 version(TwitchSupport) {} 1516 else 1517 /++ 1518 No-op mixin of version `!TwitchSupport` [kameloso.plugins.common.awareness.TwitchAwareness|TwitchAwareness]. 1519 +/ 1520 mixin template TwitchAwareness( 1521 ChannelPolicy channelPolicy = ChannelPolicy.home, 1522 Flag!"debug_" debug_ = No.debug_, 1523 string module_ = __MODULE__) 1524 { 1525 /++ 1526 Flag denoting that [kameloso.plugins.common.awareness.TwitchAwareness|TwitchAwareness] 1527 has been mixed in. 1528 +/ 1529 package enum hasTwitchAwareness = true; 1530 1531 static if (!__traits(compiles, { alias _ = .hasChannelAwareness; })) 1532 { 1533 import std.format : format; 1534 1535 enum pattern = "`%s` is missing a `ChannelAwareness` mixin " ~ 1536 "(needed for `TwitchAwareness`)"; 1537 enum message = pattern.format(module_); 1538 static assert(0, message); 1539 } 1540 }