1 /++
2     The section of [kameloso.plugins.common] that involves mixins.
3 
4     This was all in one `plugins/common.d` file that just grew too big.
5 
6     See_Also:
7         [kameloso.plugins.common.core],
8         [kameloso.plugins.common.delayawait]
9 
10     Copyright: [JR](https://github.com/zorael)
11     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
12 
13     Authors:
14         [JR](https://github.com/zorael)
15  +/
16 module kameloso.plugins.common.mixins;
17 
18 private:
19 
20 import dialect.defs;
21 import std.traits : isSomeFunction;
22 import std.typecons : Flag, No, Yes;
23 
24 public:
25 
26 
27 // WHOISFiberDelegate
28 /++
29     Functionality for catching WHOIS results and calling passed function aliases
30     with the resulting account information that was divined from it, in the form
31     of the actual [dialect.defs.IRCEvent|IRCEvent], the target
32     [dialect.defs.IRCUser|IRCUser] within it, the user's `account` field, or merely
33     alone as an arity-0 function.
34 
35     The mixed in function to call is named `enqueueAndWHOIS`. It will construct
36     the Fiber, enqueue it as awaiting the proper IRCEvent types, and issue the
37     WHOIS query.
38 
39     Example:
40     ---
41     void onSuccess(const ref IRCEvent successEvent) { /* ... */ }
42     void onFailure(const IRCUser failureUser) { /* .. */ }
43 
44     mixin WHOISFiberDelegate!(onSuccess, onFailure);
45 
46     enqueueAndWHOIS(specifiedNickname);
47     ---
48 
49     Params:
50         onSuccess = Function alias to call when successfully having received
51             account information from the server's WHOIS response.
52         onFailure = Function alias to call when the server didn't respond with
53             account information, or when the user is offline.
54         alwaysLookup = Whether or not to always issue a WHOIS query, even if
55             the requested user's account is already known.
56  +/
57 mixin template WHOISFiberDelegate(
58     alias onSuccess,
59     alias onFailure = null,
60     Flag!"alwaysLookup" alwaysLookup = No.alwaysLookup)
61 if (isSomeFunction!onSuccess && (is(typeof(onFailure) == typeof(null)) || isSomeFunction!onFailure))
62 {
63     import kameloso.plugins.common.core : IRCPlugin;
64     import std.traits : ParameterIdentifierTuple;
65     import std.typecons : Flag, No, Yes;
66 
67     alias paramNames = ParameterIdentifierTuple!(mixin(__FUNCTION__));
68 
69     static if ((paramNames.length == 0) || !is(typeof(mixin(paramNames[0])) : IRCPlugin))
70     {
71         import std.format : format;
72 
73         enum pattern = "`WHOISFiberDelegate` should be mixed into the context of an event handler. " ~
74             "(First parameter of `%s` is not an `IRCPlugin` subclass)";
75         enum message = pattern.format(__FUNCTION__);
76         static assert(0, message);
77     }
78     else
79     {
80         //alias context = mixin(paramNames[0]);  // Only works on 2.088 and later
81         // The mixin must be a concatenated string for 2.083 and earlier,
82         // but we only support 2.085+
83         mixin("alias context = ", paramNames[0], ";");
84     }
85 
86     static if (__traits(compiles, { alias _ = hasWHOISFiber; }))
87     {
88         import std.format : format;
89 
90         enum pattern = "Double mixin of `%s` in `%s`";
91         enum message = pattern.format("WHOISFiberDelegate", __FUNCTION__);
92         static assert(0, message);
93     }
94     else
95     {
96         /// Flag denoting that [WHOISFiberDelegate] has been mixed in.
97         enum hasWHOISFiber = true;
98     }
99 
100     static if (!alwaysLookup && !__traits(compiles, { alias _ = .hasUserAwareness; }))
101     {
102         pragma(msg, "Warning: `" ~ __FUNCTION__ ~ "` mixes in `WHOISFiberDelegate` " ~
103             "but its parent module does not mix in `UserAwareness`");
104     }
105 
106     // _kamelosoCarriedNickname
107     /++
108         Nickname being looked up, stored outside of any separate function to make
109         it available to all of them.
110      +/
111     string _kamelosoCarriedNickname;
112 
113     /++
114         Event types that we may encounter as responses to WHOIS queries.
115      +/
116     static immutable IRCEvent.Type[6] whoisEventTypes =
117     [
118         IRCEvent.Type.RPL_WHOISUSER,
119         IRCEvent.Type.RPL_WHOISACCOUNT,
120         IRCEvent.Type.RPL_WHOISREGNICK,
121         IRCEvent.Type.RPL_ENDOFWHOIS,
122         IRCEvent.Type.ERR_NOSUCHNICK,
123         IRCEvent.Type.ERR_UNKNOWNCOMMAND,
124     ];
125 
126     // whoisFiberDelegate
127     /++
128         Reusable mixin that catches WHOIS results.
129      +/
130     void whoisFiberDelegate()
131     {
132         import kameloso.plugins.common.delayawait : unawait;
133         import kameloso.thread : CarryingFiber;
134         import dialect.common : opEqualsCaseInsensitive;
135         import dialect.defs : IRCEvent, IRCUser;
136         import lu.conv : Enum;
137         import lu.traits : TakesParams;
138         import std.algorithm.searching : canFind;
139         import std.traits : arity;
140         import core.thread : Fiber;
141 
142         while (true)
143         {
144             auto thisFiber = cast(CarryingFiber!IRCEvent)(Fiber.getThis);
145             assert(thisFiber, "Incorrectly cast Fiber: " ~ typeof(thisFiber).stringof);
146             assert((thisFiber.payload != IRCEvent.init),
147                 "Uninitialised `payload` in " ~ typeof(thisFiber).stringof);
148 
149             immutable whoisEvent = thisFiber.payload;
150 
151             assert(whoisEventTypes[].canFind(whoisEvent.type),
152                 "WHOIS Fiber delegate was invoked with an unexpected event type: " ~
153                 "`IRCEvent.Type." ~ Enum!(IRCEvent.Type).toString(whoisEvent.type) ~'`');
154 
155             /++
156                 Invoke `onSuccess`.
157              +/
158             void callOnSuccess()
159             {
160                 static if (TakesParams!(onSuccess, IRCEvent))
161                 {
162                     return onSuccess(whoisEvent);
163                 }
164                 else static if (TakesParams!(onSuccess, IRCUser))
165                 {
166                     return onSuccess(whoisEvent.target);
167                 }
168                 else static if (TakesParams!(onSuccess, string))
169                 {
170                     return onSuccess(whoisEvent.target.account);
171                 }
172                 else static if (arity!onSuccess == 0)
173                 {
174                     return onSuccess();
175                 }
176                 else
177                 {
178                     import std.format : format;
179 
180                     enum pattern = "Unsupported signature of success function/delegate " ~
181                         "alias passed to mixin `WHOISFiberDelegate` in `%s`: `%s %s`";
182                     enum message = pattern.format(
183                         __FUNCTION__,
184                         typeof(onSuccess).stringof,
185                         __traits(identifier, onSuccess));
186                     static assert(0, message);
187                 }
188             }
189 
190             /++
191                 Invoke `onFailure`, if it's available.
192              +/
193             void callOnFailure()
194             {
195                 static if (!is(typeof(onFailure) == typeof(null)))
196                 {
197                     static if (TakesParams!(onFailure, IRCEvent))
198                     {
199                         return onFailure(whoisEvent);
200                     }
201                     else static if (TakesParams!(onFailure, IRCUser))
202                     {
203                         return onFailure(whoisEvent.target);
204                     }
205                     else static if (TakesParams!(onFailure, string))
206                     {
207                         // Never called when using hostmasks
208                         return onFailure(whoisEvent.target.account);
209                     }
210                     else static if (arity!onFailure == 0)
211                     {
212                         return onFailure();
213                     }
214                     else
215                     {
216                         import std.format : format;
217 
218                         enum pattern = "Unsupported signature of failure function/delegate " ~
219                             "alias passed to mixin `WHOISFiberDelegate` in `%s`: `%s %s`";
220                         enum message = pattern.format(
221                             __FUNCTION__,
222                             typeof(onFailure).stringof,
223                             __traits(identifier, onFailure));
224                         static assert(0, message);
225                     }
226                 }
227             }
228 
229             if (whoisEvent.type == IRCEvent.Type.ERR_UNKNOWNCOMMAND)
230             {
231                 if (!whoisEvent.aux[0].length || (whoisEvent.aux[0] == "WHOIS"))
232                 {
233                     // WHOIS query failed due to unknown command.
234                     // Some flavours of ERR_UNKNOWNCOMMAND don't say what the
235                     // command was, so we'll have to assume it's the right one.
236                     // Return and end Fiber.
237                     return callOnFailure();
238                 }
239                 else
240                 {
241                     // Wrong unknown command; await a new one
242                     Fiber.yield();
243                     continue;
244                 }
245             }
246 
247             immutable m = context.state.server.caseMapping;
248 
249             if (!whoisEvent.target.nickname.opEqualsCaseInsensitive(_kamelosoCarriedNickname, m))
250             {
251                 // Wrong WHOIS; await a new one
252                 Fiber.yield();
253                 continue;
254             }
255 
256             // Clean up awaiting fiber entries on exit, just to be neat.
257             scope(exit) unawait(context, thisFiber, whoisEventTypes[]);
258 
259             with (IRCEvent.Type)
260             switch (whoisEvent.type)
261             {
262             case RPL_WHOISACCOUNT:
263             case RPL_WHOISREGNICK:
264                 return callOnSuccess();
265 
266             case RPL_WHOISUSER:
267                 if (context.state.settings.preferHostmasks)
268                 {
269                     return callOnSuccess();
270                 }
271                 else
272                 {
273                     // We're not interested in RPL_WHOISUSER if we're not in hostmasks mode
274                     Fiber.yield();
275                     continue;
276                 }
277 
278             case RPL_ENDOFWHOIS:
279             case ERR_NOSUCHNICK:
280             //case ERR_UNKNOWNCOMMAND:  // Already handled above
281                 return callOnFailure();
282 
283             default:
284                 assert(0, "Unexpected WHOIS event type encountered in `whoisFiberDelegate`");
285             }
286 
287             // Would end loop here but statement not reachable
288             //return;
289             assert(0, "Escaped terminal switch in `whoisFiberDelegate`");
290         }
291     }
292 
293     // enqueueAndWHOIS
294     /++
295         Constructs a [kameloso.thread.CarryingFiber|CarryingFiber] carrying a
296         [dialect.defs.IRCEvent|IRCEvent] and enqueues it into the
297         [kameloso.plugins.common.core.IRCPluginState.awaitingFibers|IRCPluginState.awaitingFibers]
298         associative array, then issues a WHOIS query (unless overridden via
299         the `issueWhois` parameter).
300 
301         Params:
302             nickname = Nickname of the user the enqueueing event relates to.
303             issueWhois = Whether to actually issue `WHOIS` queries at all or just enqueue.
304             background = Whether or not to issue queries as low-priority background messages.
305 
306         Throws:
307             [object.Exception|Exception] if a success of failure function was to trigger
308             in an impossible scenario, such as on WHOIS results on Twitch.
309      +/
310     void enqueueAndWHOIS(
311         const string nickname,
312         const Flag!"issueWhois" issueWhois = Yes.issueWhois,
313         const Flag!"background" background = No.background)
314     {
315         import kameloso.plugins.common.delayawait : await;
316         import kameloso.constants : BufferSize;
317         import kameloso.messaging : whois;
318         import kameloso.thread : CarryingFiber;
319         import lu.string : contains, nom;
320         import lu.traits : TakesParams;
321         import std.traits : arity;
322         import std.typecons : Flag, No, Yes;
323         import core.thread : Fiber;
324 
325         version(TwitchSupport)
326         {
327             if (context.state.server.daemon == IRCServer.Daemon.twitch)
328             {
329                 // Define Twitch queries as always succeeding, since WHOIS isn't applicable
330 
331                 version(TwitchWarnings)
332                 {
333                     import kameloso.common : logger;
334                     logger.warning("Tried to enqueue and WHOIS on Twitch");
335 
336                     version(PrintStacktraces)
337                     {
338                         import kameloso.common: printStacktrace;
339                         printStacktrace();
340                     }
341                 }
342 
343                 static if (__traits(compiles, { alias _ = .hasUserAwareness; }))
344                 {
345                     if (const user = nickname in context.state.users)
346                     {
347                         static if (TakesParams!(onSuccess, IRCEvent))
348                         {
349                             // Can't WHOIS on Twitch
350                             enum message = "Tried to enqueue a `" ~
351                                 typeof(onSuccess).stringof ~ " onSuccess` function " ~
352                                 "when on Twitch (can't WHOIS)";
353                             throw new Exception(message);
354                         }
355                         else static if (TakesParams!(onSuccess, IRCUser))
356                         {
357                             return onSuccess(*user);
358                         }
359                         else static if (TakesParams!(onSuccess, string))
360                         {
361                             return onSuccess(user.account);
362                         }
363                         else static if (arity!onSuccess == 0)
364                         {
365                             return onSuccess();
366                         }
367                         else
368                         {
369                             // Will already have asserted previously
370                         }
371                     }
372                 }
373 
374                 static if (
375                     TakesParams!(onSuccess, IRCEvent) ||
376                     TakesParams!(onSuccess, IRCUser))
377                 {
378                     // Can't WHOIS on Twitch
379                     enum message = "Tried to enqueue a `" ~
380                         typeof(onSuccess).stringof ~ " onSuccess` function " ~
381                         "when on Twitch without `UserAwareness` (can't WHOIS)";
382                     throw new Exception(message);
383                 }
384                 else static if (TakesParams!(onSuccess, string))
385                 {
386                     return onSuccess(nickname);
387                 }
388                 else static if (arity!onSuccess == 0)
389                 {
390                     return onSuccess();
391                 }
392                 else
393                 {
394                     // Will already have asserted previously
395                 }
396             }
397         }
398 
399         static if (!alwaysLookup && __traits(compiles, { alias _ = .hasUserAwareness; }))
400         {
401             if (const user = nickname in context.state.users)
402             {
403                 if (user.account.length)
404                 {
405                     static if (TakesParams!(onSuccess, IRCEvent))
406                     {
407                         // No can do, drop down and WHOIS
408                     }
409                     else static if (TakesParams!(onSuccess, IRCUser))
410                     {
411                         return onSuccess(*user);
412                     }
413                     else static if (TakesParams!(onSuccess, string))
414                     {
415                         return onSuccess(user.account);
416                     }
417                     else static if (arity!onSuccess == 0)
418                     {
419                         return onSuccess();
420                     }
421                     else
422                     {
423                         // Will already have asserted previously
424                     }
425                 }
426                 else
427                 {
428                     static if (!is(typeof(onFailure) == typeof(null)))
429                     {
430                         import kameloso.constants : Timeout;
431                         import std.datetime.systime : Clock;
432 
433                         if ((Clock.currTime.toUnixTime - user.updated) <= Timeout.whoisRetry)
434                         {
435                             static if (TakesParams!(onFailure, IRCEvent))
436                             {
437                                 // No can do, drop down and WHOIS
438                             }
439                             else static if (TakesParams!(onFailure, IRCUser))
440                             {
441                                 return onFailure(*user);
442                             }
443                             else static if (TakesParams!(onFailure, string))
444                             {
445                                 return onFailure(user.account);
446                             }
447                             else static if (arity!onSuccess == 0)
448                             {
449                                 return onFailure();
450                             }
451                             else
452                             {
453                                 // Will already have asserted previously?
454                             }
455                         }
456                     }
457                 }
458             }
459         }
460 
461         Fiber fiber = new CarryingFiber!IRCEvent(&whoisFiberDelegate, BufferSize.fiberStack);
462         await(context, fiber, whoisEventTypes[]);
463 
464         string slice = nickname;  // mutable
465         immutable nicknamePart = slice.contains('!') ?
466             slice.nom('!') :
467             slice;
468 
469         version(WithPrinterPlugin)
470         {
471             import kameloso.thread : ThreadMessage, boxed;
472             import std.concurrency : send;
473 
474             plugin.state.mainThread.send(ThreadMessage.busMessage("printer", boxed("squelch " ~ nicknamePart)));
475         }
476 
477         if (issueWhois)
478         {
479             if (background)
480             {
481                 // Need Property.forced to not miss events
482                 enum properties =
483                     Message.Property.forced |
484                     Message.Property.quiet |
485                     Message.Property.background;
486                 whois(context.state, nicknamePart, properties);
487             }
488             else
489             {
490                 // Ditto
491                 enum properties =
492                     Message.Property.forced |
493                     Message.Property.quiet |
494                     Message.Property.priority;
495                 whois(context.state, nicknamePart, properties);
496             }
497         }
498 
499         _kamelosoCarriedNickname = nicknamePart;
500     }
501 }
502 
503 
504 // MessagingProxy
505 /++
506     Mixin to give shorthands to the functions in [kameloso.messaging], for
507     easier use when in a `with (plugin) { /* ... */ }` scope.
508 
509     This merely makes it possible to use commands like
510     `raw("PING :irc.freenode.net")` without having to import
511     [kameloso.messaging] and include the thread ID of the main thread in every
512     call of the functions.
513 
514     Params:
515         debug_ = Whether or not to include debugging output.
516  +/
517 mixin template MessagingProxy(Flag!"debug_" debug_ = No.debug_)
518 {
519 private:
520     import kameloso.plugins.common.core : IRCPlugin;
521     import kameloso.messaging : Message;
522     import std.meta : AliasSeq;
523     import std.typecons : Flag, No, Yes;
524     static import kameloso.messaging;
525 
526     static if (__traits(compiles, { alias _ = this.hasMessagingProxy; }))
527     {
528         import std.format : format;
529 
530         enum pattern = "Double mixin of `%s` in `%s`";
531         enum message = pattern.format("MessagingProxy", typeof(this).stringof);
532         static assert(0, message);
533     }
534     else
535     {
536         /// Flag denoting that [MessagingProxy] has been mixed in.
537         private enum hasMessagingProxy = true;
538     }
539 
540     pragma(inline, true);
541 
542     // chan
543     /++
544         Sends a channel message.
545      +/
546     void chan(
547         const string channelName,
548         const string content,
549         const Message.Property properties = Message.Property.none,
550         const string caller = __FUNCTION__)
551     {
552         return kameloso.messaging.chan(
553             state,
554             channelName,
555             content,
556             properties,
557             caller);
558     }
559 
560     // reply
561     /++
562         Replies to a channel message.
563      +/
564     void reply(
565         const ref IRCEvent event,
566         const string content,
567         const Message.Property properties = Message.Property.none,
568         const string caller = __FUNCTION__)
569     {
570         return kameloso.messaging.reply(
571             state,
572             event,
573             content,
574             properties,
575             caller);
576     }
577 
578     // query
579     /++
580         Sends a private query message to a user.
581      +/
582     void query(
583         const string nickname,
584         const string content,
585         const Message.Property properties = Message.Property.none,
586         const string caller = __FUNCTION__)
587     {
588         return kameloso.messaging.query(
589             state,
590             nickname,
591             content,
592             properties,
593             caller);
594     }
595 
596     // privmsg
597     /++
598         Sends either a channel message or a private query message depending on
599         the arguments passed to it.
600 
601         This reflects how channel messages and private messages are both the
602         underlying same type; [dialect.defs.IRCEvent.Type.PRIVMSG].
603      +/
604     void privmsg(
605         const string channel,
606         const string nickname,
607         const string content,
608         const Message.Property properties = Message.Property.none,
609         const string caller = __FUNCTION__)
610     {
611         return kameloso.messaging.privmsg(
612             state,
613             channel,
614             nickname,
615             content,
616             properties,
617             caller);
618     }
619 
620     // emote
621     /++
622         Sends an `ACTION` "emote" to the supplied target (nickname or channel).
623      +/
624     void emote(
625         const string emoteTarget,
626         const string content,
627         const Message.Property properties = Message.Property.none,
628         const string caller = __FUNCTION__)
629     {
630         return kameloso.messaging.emote(
631             state,
632             emoteTarget,
633             content,
634             properties,
635             caller);
636     }
637 
638     // mode
639     /++
640         Sets a channel mode.
641 
642         This includes modes that pertain to a user in the context of a channel, like bans.
643      +/
644     void mode(
645         const string channel,
646         const const(char)[] modes,
647         const string content = string.init,
648         const Message.Property properties = Message.Property.none,
649         const string caller = __FUNCTION__)
650     {
651         return kameloso.messaging.mode(
652             state,
653             channel,
654             modes,
655             content,
656             properties,
657             caller);
658     }
659 
660     // topic
661     /++
662         Sets the topic of a channel.
663      +/
664     void topic(
665         const string channel,
666         const string content,
667         const Message.Property properties = Message.Property.none,
668         const string caller = __FUNCTION__)
669     {
670         return kameloso.messaging.topic(
671             state,
672             channel,
673             content,
674             properties,
675             caller);
676     }
677 
678     // invite
679     /++
680         Invites a user to a channel.
681      +/
682     void invite(
683         const string channel,
684         const string nickname,
685         const Message.Property properties = Message.Property.none,
686         const string caller = __FUNCTION__)
687     {
688         return kameloso.messaging.invite(
689             state,
690             channel,
691             nickname,
692             properties,
693             caller);
694     }
695 
696     // join
697     /++
698         Joins a channel.
699      +/
700     void join(
701         const string channel,
702         const string key = string.init,
703         const Message.Property properties = Message.Property.none,
704         const string caller = __FUNCTION__)
705     {
706         return kameloso.messaging.join(
707             state,
708             channel,
709             key,
710             properties,
711             caller);
712     }
713 
714     // kick
715     /++
716         Kicks a user from a channel.
717      +/
718     void kick(
719         const string channel,
720         const string nickname,
721         const string reason = string.init,
722         const Message.Property properties = Message.Property.none,
723         const string caller = __FUNCTION__)
724     {
725         return kameloso.messaging.kick(
726             state,
727             channel,
728             nickname,
729             reason,
730             properties,
731             caller);
732     }
733 
734     // part
735     /++
736         Leaves a channel.
737      +/
738     void part(
739         const string channel,
740         const string reason = string.init,
741         const Message.Property properties = Message.Property.none,
742         const string caller = __FUNCTION__)
743     {
744         return kameloso.messaging.part(
745             state,
746             channel,
747             reason,
748             properties,
749             caller);
750     }
751 
752     // quit
753     /++
754         Disconnects from the server, optionally with a quit reason.
755      +/
756     void quit(
757         const string reason = string.init,
758         const Message.Property properties = Message.Property.none,
759         const string caller = __FUNCTION__)
760     {
761         return kameloso.messaging.quit(
762             state,
763             reason,
764             properties,
765             caller);
766     }
767 
768     // whois
769     /++
770         Queries the server for WHOIS information about a user.
771      +/
772     void whois(
773         const string nickname,
774         const Message.Property properties = Message.Property.none,
775         const string caller = __FUNCTION__)
776     {
777         return kameloso.messaging.whois(
778             state,
779             nickname,
780             properties,
781             caller);
782     }
783 
784     // raw
785     /++
786         Sends text to the server, verbatim.
787 
788         This is used to send messages of types for which there exist no helper
789         functions.
790      +/
791     void raw(
792         const string line,
793         const Message.Property properties = Message.Property.none,
794         const string caller = __FUNCTION__)
795     {
796         return kameloso.messaging.raw(
797             state,
798             line,
799             properties,
800             caller);
801     }
802 
803     // immediate
804     /++
805         Sends raw text to the server, verbatim, bypassing all queues and
806         throttling delays.
807      +/
808     void immediate(
809         const string line,
810         const Message.Property properties = Message.Property.none,
811         const string caller = __FUNCTION__)
812     {
813         return kameloso.messaging.immediate(
814             state,
815             line,
816             properties,
817             caller);
818     }
819 
820     // immediateline
821     /++
822         Merely an alias to [immediate], because we use both terms at different places.
823      +/
824     alias immediateline = immediate;
825 
826     /+
827         Generates the functions `askToWriteln`, `askToTrace`, `askToLog`,
828         `askToInfo`, `askToWarning`, and `askToError`,
829      +/
830     static foreach (immutable verb; AliasSeq!(
831         "Writeln",
832         "Trace",
833         "Log",
834         "Info",
835         "Warn",
836         "Warning",
837         "Error"))
838     {
839         /++
840             Generated `askToVerb` function. Asks the main thread to output text
841             to the local terminal.
842 
843             No need for any annotation;
844             [kameloso.messaging.askToOutputImpl|askToOutputImpl] is
845             `@system` and nothing else.
846          +/
847         mixin("
848 void askTo" ~ verb ~ "(const string line)
849 {
850     return kameloso.messaging.askTo" ~ verb ~ "(state, line);
851 }");
852     }
853 }
854 
855 ///
856 unittest
857 {
858     import kameloso.plugins.common.core : IRCPlugin, IRCPluginImpl, IRCPluginState;
859 
860     class MyPlugin : IRCPlugin
861     {
862         mixin MessagingProxy;
863         mixin IRCPluginImpl;
864     }
865 
866     IRCPluginState state;
867     MyPlugin plugin = new MyPlugin(state);
868 
869     with (plugin)
870     {
871         // The below calls will fail in-contracts, so don't call them.
872         // Just generate the code so we know they compile.
873         if (plugin !is null) return;
874 
875         /*chan(string.init, string.init);
876         query(string.init, string.init);
877         privmsg(string.init, string.init, string.init);
878         emote(string.init, string.init);
879         mode(string.init, string.init, string.init);
880         topic(string.init, string.init);
881         invite(string.init, string.init);
882         join(string.init, string.init);
883         kick(string.init, string.init, string.init);
884         part(string.init, string.init);
885         quit(string.init);
886         enum whoisProperties = (Message.Property.forced | Message.Property.quiet);
887         whois(string.init, whoisProperties);
888         raw(string.init);
889         immediate(string.init);
890         immediateline(string.init);*/
891         askToWriteln(string.init);
892         askToTrace(string.init);
893         askToLog(string.init);
894         askToInfo(string.init);
895         askToWarn(string.init);
896         askToWarning(string.init);
897         askToError(string.init);
898     }
899 
900     class MyPlugin2 : IRCPlugin
901     {
902         mixin MessagingProxy fromMixin;
903         mixin IRCPluginImpl;
904     }
905 
906     static if (__VERSION__ >= 2097L)
907     {
908         static import kameloso.messaging;
909 
910         MyPlugin2 plugin2 = new MyPlugin2(state);
911 
912         foreach (immutable funstring; __traits(derivedMembers, kameloso.messaging))
913         {
914             import lu.string : beginsWith;
915             import std.algorithm.comparison : among;
916 
917             static if (funstring.among!(
918                     "object",
919                     "dialect",
920                     "kameloso",
921                     "Message") ||
922                 funstring.beginsWith("askTo"))
923             {
924                 //pragma(msg, "ignoring " ~ funstring);
925             }
926             else static if (!__traits(compiles, { mixin("alias _ = plugin2.fromMixin." ~ funstring ~ ";"); }))
927             {
928                 import std.format : format;
929 
930                 enum pattern = "`MessageProxy` is missing a wrapper for `kameloso.messaging.%s`";
931                 enum message = pattern.format(funstring);
932                 static assert(0, message);
933             }
934         }
935     }
936 }