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 }