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 }