1 /++
2     Functions used to send messages to the server.
3 
4     To send a server message some information is needed; like
5     message type, message target, perhaps channel, content and such.
6     [dialect.defs.IRCEvent|IRCEvent] has all of this, so it lends itself to
7     repurposing it to aggregate and carry them, through concurrency messages.
8     These are caught by the concurrency message-reading parts of the main loop,
9     which reversely parses them into strings and sends them on to the server.
10 
11     Example:
12     ---
13     //IRCPluginState state;
14 
15     chan(state, "#channel", "Hello world!");
16     query(state, "nickname", "foo bar");
17     mode(state, "#channel", "nickname", "+o");
18     topic(state, "#channel", "I thought what I'd do was, I'd pretend I was one of those deaf-mutes.");
19     ---
20 
21     Having to supply the [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]
22     on every call can be avoided for plugins, by mixing in
23     [kameloso.plugins.common.mixins.MessagingProxy|MessagingProxy]
24     and placing the messaging function calls inside a `with (plugin)` block.
25 
26     Example:
27     ---
28     IRCPluginState state;
29     auto plugin = new MyPlugin(state);  // has mixin MessagingProxy;
30 
31     with (plugin)
32     {
33         chan("#channel", "Foo bar baz");
34         query("nickname", "hello");
35         mode("#channel", string.init, "+b", "dudebro!*@*");
36         mode(string.init, "nickname", "+i");
37     }
38     ---
39 
40     See_Also:
41         [kameloso.thread]
42 
43     Copyright: [JR](https://github.com/zorael)
44     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
45 
46     Authors:
47         [JR](https://github.com/zorael)
48  +/
49 module kameloso.messaging;
50 
51 private:
52 
53 import kameloso.plugins.common.core : IRCPluginState;
54 import kameloso.irccolours : expandIRCTags;
55 import dialect.defs;
56 import std.concurrency : Tid, prioritySend, send;
57 import std.typecons : Flag, No, Yes;
58 static import kameloso.common;
59 
60 version(unittest)
61 {
62     import lu.conv : Enum;
63     import std.concurrency : receive, receiveOnly, thisTid;
64     import std.conv : to;
65 }
66 
67 public:
68 
69 
70 // Message
71 /++
72     An [dialect.defs.IRCEvent|IRCEvent] with some metadata, to be used when
73     crafting an outgoing message to the server.
74  +/
75 struct Message
76 {
77     /++
78         Properties of a [Message]. Describes how it should be sent.
79      +/
80     enum Property
81     {
82         none       = 1 << 0,  /// Unset value.
83         fast        = 1 << 1,  /// Message should be sent faster than normal. (Twitch)
84         quiet       = 1 << 2,  /// Message should be sent without echoing it to the terminal.
85         background  = 1 << 3,  /// Message should be lazily sent in the background.
86         forced      = 1 << 4,  /// Message should bypass some checks.
87         priority    = 1 << 5,  /// Message should be given higher priority.
88         immediate   = 1 << 6,  /// Message should be sent immediately.
89     }
90 
91     /++
92         The [dialect.defs.IRCEvent|IRCEvent] that contains the information we
93         want to send to the server.
94      +/
95     IRCEvent event;
96 
97     /++
98         The properties of this message. More than one may be used, with bitwise-or.
99      +/
100     Property properties;
101 
102     /++
103         String name of the function that is sending this message, or something
104         else that gives context.
105      +/
106     string caller;
107 }
108 
109 
110 // chan
111 /++
112     Sends a channel message.
113 
114     Params:
115         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
116             via which to send messages to the server.
117         channelName = Channel in which to send the message.
118         content = Message body content to send.
119         properties = Custom message properties, such as [Message.Property.quiet]
120             and [Message.Property.forced].
121         caller = String name of the calling function, or something else that gives context.
122  +/
123 void chan(
124     IRCPluginState state,
125     const string channelName,
126     const string content,
127     const Message.Property properties = Message.Property.none,
128     const string caller = __FUNCTION__)
129 in (channelName.length, "Tried to send a channel message but no channel was given")
130 {
131     Message m;
132 
133     m.event.type = IRCEvent.Type.CHAN;
134     m.event.channel = channelName;
135     m.event.content = content.expandIRCTags;
136     m.properties = properties;
137     m.caller = caller;
138 
139     version(TwitchSupport)
140     {
141         if (state.server.daemon == IRCServer.Daemon.twitch)
142         {
143             if (auto channel = channelName in state.channels)
144             {
145                 if (auto ops = 'o' in channel.mods)
146                 {
147                     if (state.client.nickname in *ops)
148                     {
149                         // We are a moderator and can as such send things fast
150                         m.properties |= Message.Property.fast;
151                     }
152                 }
153             }
154         }
155     }
156 
157     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
158     else state.mainThread.send(m);
159 }
160 
161 ///
162 unittest
163 {
164     IRCPluginState state;
165     state.mainThread = thisTid;
166 
167     enum properties = (Message.Property.quiet | Message.Property.background);
168     chan(state, "#channel", "content", properties);
169 
170     receive(
171         (Message m)
172         {
173             with (m.event)
174             {
175                 assert((type == IRCEvent.Type.CHAN), Enum!(IRCEvent.Type).toString(type));
176                 assert((channel == "#channel"), channel);
177                 assert((content == "content"), content);
178                 //assert(m.properties & Message.Property.fast);
179             }
180         }
181     );
182 }
183 
184 
185 // reply
186 /++
187     Replies to a message in a Twitch channel. Requires version `TwitchSupport`,
188     without which it will just pass on to [chan].
189 
190     Params:
191         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
192             via which to send messages to the server.
193         event = Original event, to which we're replying.
194         content = Message body content to send.
195         properties = Custom message properties, such as [Message.Property.quiet]
196             and [Message.Property.forced].
197         caller = String name of the calling function, or something else that gives context.
198  +/
199 void reply(
200     IRCPluginState state,
201     const ref IRCEvent event,
202     const string content,
203     const Message.Property properties = Message.Property.none,
204     const string caller = __FUNCTION__)
205 in (event.channel.length, "Tried to reply to a channel message but no channel was given")
206 {
207     version(TwitchSupport)
208     {
209         if ((state.server.daemon != IRCServer.Daemon.twitch) || !event.id.length)
210         {
211             return chan(
212                 state,
213                 event.channel,
214                 content,
215                 properties,
216                 caller);
217         }
218 
219         Message m;
220 
221         m.event.type = IRCEvent.Type.CHAN;
222         m.event.channel = event.channel;
223         m.event.content = content.expandIRCTags;
224         m.event.tags = "reply-parent-msg-id=" ~ event.id;
225         m.properties = properties;
226         m.caller = caller;
227 
228         if (auto channel = m.event.channel in state.channels)
229         {
230             if (auto ops = 'o' in channel.mods)
231             {
232                 if (state.client.nickname in *ops)
233                 {
234                     // We are a moderator and can as such send things fast
235                     m.properties |= Message.Property.fast;
236                 }
237             }
238         }
239 
240         if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
241         else state.mainThread.send(m);
242     }
243     else
244     {
245         return chan(
246             state,
247             event.channel,
248             content,
249             properties,
250             caller);
251     }
252 }
253 
254 ///
255 version(TwitchSupport)
256 unittest
257 {
258     IRCPluginState state;
259     state.server.daemon = IRCServer.Daemon.twitch;
260     state.mainThread = thisTid;
261 
262     IRCEvent event;
263     event.sender.nickname = "kameloso";
264     event.channel = "#channel";
265     event.content = "content";
266     event.id = "some-reply-id";
267 
268     reply(state, event, "reply content");
269 
270     receive(
271         (Message m)
272         {
273             with (m.event)
274             {
275                 assert((type == IRCEvent.Type.CHAN), Enum!(IRCEvent.Type).toString(type));
276                 assert((content == "reply content"), content);
277                 assert((tags == "reply-parent-msg-id=some-reply-id"), tags);
278                 assert((m.properties == Message.Property.init));
279             }
280         }
281     );
282 }
283 
284 
285 // query
286 /++
287     Sends a private query message to a user.
288 
289     Params:
290         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
291             via which to send messages to the server.
292         nickname = Nickname of user to which to send the private message.
293         content = Message body content to send.
294         properties = Custom message properties, such as [Message.Property.quiet]
295             and [Message.Property.forced].
296         caller = String name of the calling function, or something else that gives context.
297  +/
298 void query(
299     IRCPluginState state,
300     const string nickname,
301     const string content,
302     const Message.Property properties = Message.Property.none,
303     const string caller = __FUNCTION__)
304 in (nickname.length, "Tried to send a private query but no nickname was given")
305 {
306     Message m;
307 
308     m.event.type = IRCEvent.Type.QUERY;
309     m.event.target.nickname = nickname;
310     m.event.content = content.expandIRCTags;
311     m.properties = properties;
312     m.caller = caller;
313 
314     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
315     else state.mainThread.send(m);
316 }
317 
318 ///
319 unittest
320 {
321     IRCPluginState state;
322     state.mainThread = thisTid;
323 
324     query(state, "kameloso", "content");
325 
326     receive(
327         (Message m)
328         {
329             with (m.event)
330             {
331                 assert((type == IRCEvent.Type.QUERY), Enum!(IRCEvent.Type).toString(type));
332                 assert((target.nickname == "kameloso"), target.nickname);
333                 assert((content == "content"), content);
334                 assert((m.properties == Message.Property.init));
335             }
336         }
337     );
338 }
339 
340 
341 // privmsg
342 /++
343     Sends either a channel message or a private query message depending on
344     the arguments passed to it.
345 
346     This reflects how channel messages and private messages are both the
347     underlying same type; [dialect.defs.IRCEvent.Type.PRIVMSG|PRIVMSG].
348 
349     Params:
350         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
351             via which to send messages to the server.
352         channel = Channel in which to send the message, if applicable.
353         nickname = Nickname of user to which to send the message, if applicable.
354         content = Message body content to send.
355         properties = Custom message properties, such as [Message.Property.quiet]
356             and [Message.Property.forced].
357         caller = String name of the calling function, or something else that gives context.
358  +/
359 void privmsg(
360     IRCPluginState state,
361     const string channel,
362     const string nickname,
363     const string content,
364     const Message.Property properties = Message.Property.none,
365     const string caller = __FUNCTION__)
366 in ((channel.length || nickname.length), "Tried to send a PRIVMSG but no channel nor nickname was given")
367 {
368     immutable expandedContent = content.expandIRCTags;
369 
370     if (channel.length)
371     {
372         return chan(state, channel, expandedContent, properties, caller);
373     }
374     else if (nickname.length)
375     {
376         return query(state, nickname, expandedContent, properties, caller);
377     }
378     else
379     {
380         // In case contracts are disabled?
381         assert(0, "Tried to send a PRIVMSG but no channel nor nickname was given");
382     }
383 }
384 
385 ///
386 unittest
387 {
388     IRCPluginState state;
389     state.mainThread = thisTid;
390 
391     privmsg(state, "#channel", string.init, "content");
392 
393     receive(
394         (Message m)
395         {
396             with (m.event)
397             {
398                 assert((type == IRCEvent.Type.CHAN), Enum!(IRCEvent.Type).toString(type));
399                 assert((channel == "#channel"), channel);
400                 assert((content == "content"), content);
401                 assert(!target.nickname.length, target.nickname);
402                 assert(m.properties == Message.Property.init);
403             }
404         }
405     );
406 
407     privmsg(state, string.init, "kameloso", "content");
408 
409     receive(
410         (Message m)
411         {
412             with (m.event)
413             {
414                 assert((type == IRCEvent.Type.QUERY), Enum!(IRCEvent.Type).toString(type));
415                 assert(!channel.length, channel);
416                 assert((target.nickname == "kameloso"), target.nickname);
417                 assert((content == "content"), content);
418                 assert(m.properties == Message.Property.init);
419             }
420         }
421     );
422 }
423 
424 
425 // emote
426 /++
427     Sends an `ACTION` "emote" to the supplied target (nickname or channel).
428 
429     Params:
430         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
431             via which to send messages to the server.
432         emoteTarget = Target of the emote, either a nickname to be sent as a
433             private message, or a channel.
434         content = Message body content to send.
435         properties = Custom message properties, such as [Message.Property.quiet]
436             and [Message.Property.forced].
437         caller = String name of the calling function, or something else that gives context.
438  +/
439 void emote(
440     IRCPluginState state,
441     const string emoteTarget,
442     const string content,
443     const Message.Property properties = Message.Property.none,
444     const string caller = __FUNCTION__)
445 in (emoteTarget.length, "Tried to send an emote but no target was given")
446 {
447     import lu.string : contains;
448 
449     Message m;
450 
451     m.event.type = IRCEvent.Type.EMOTE;
452     m.event.content = content.expandIRCTags;
453     m.properties = properties;
454     m.caller = caller;
455 
456     if (state.server.chantypes.contains(emoteTarget[0]))
457     {
458         m.event.channel = emoteTarget;
459     }
460     else
461     {
462         m.event.target.nickname = emoteTarget;
463     }
464 
465     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
466     else state.mainThread.send(m);
467 }
468 
469 ///
470 unittest
471 {
472     IRCPluginState state;
473     state.mainThread = thisTid;
474 
475     emote(state, "#channel", "content");
476 
477     receive(
478         (Message m)
479         {
480             with (m.event)
481             {
482                 assert((type == IRCEvent.Type.EMOTE), Enum!(IRCEvent.Type).toString(type));
483                 assert((channel == "#channel"), channel);
484                 assert((content == "content"), content);
485                 assert(!target.nickname.length, target.nickname);
486                 assert(m.properties == Message.Property.init);
487             }
488         }
489     );
490 
491     emote(state, "kameloso", "content");
492 
493     receive(
494         (Message m)
495         {
496             with (m.event)
497             {
498                 assert((type == IRCEvent.Type.EMOTE), Enum!(IRCEvent.Type).toString(type));
499                 assert(!channel.length, channel);
500                 assert((target.nickname == "kameloso"), target.nickname);
501                 assert((content == "content"), content);
502                 assert(m.properties == Message.Property.init);
503             }
504         }
505     );
506 }
507 
508 
509 // mode
510 /++
511     Sets a channel mode.
512 
513     This includes modes that pertain to a user in the context of a channel, like bans.
514 
515     Params:
516         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
517             via which to send messages to the server.
518         channel = Channel to change the modes of.
519         modes = Mode characters to apply to the channel.
520         content = Target of mode change, if applicable.
521         properties = Custom message properties, such as [Message.Property.quiet]
522             and [Message.Property.forced].
523         caller = String name of the calling function, or something else that gives context.
524  +/
525 void mode(
526     IRCPluginState state,
527     const string channel,
528     const const(char)[] modes,
529     const string content = string.init,
530     const Message.Property properties = Message.Property.none,
531     const string caller = __FUNCTION__)
532 in (channel.length, "Tried to set a mode but no channel was given")
533 {
534     Message m;
535 
536     m.event.type = IRCEvent.Type.MODE;
537     m.event.channel = channel;
538     m.event.aux[0] = modes.idup;
539     m.event.content = content.expandIRCTags;
540     m.properties = properties;
541     m.caller = caller;
542 
543     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
544     else state.mainThread.send(m);
545 }
546 
547 ///
548 unittest
549 {
550     IRCPluginState state;
551     state.mainThread = thisTid;
552 
553     mode(state, "#channel", "+o", "content");
554 
555     receive(
556         (Message m)
557         {
558             with (m.event)
559             {
560                 assert((type == IRCEvent.Type.MODE), Enum!(IRCEvent.Type).toString(type));
561                 assert((channel == "#channel"), channel);
562                 assert((content == "content"), content);
563                 assert((aux[0] == "+o"), aux[0]);
564                 assert(m.properties == Message.Property.init);
565             }
566         }
567     );
568 }
569 
570 
571 // topic
572 /++
573     Sets the topic of a channel.
574 
575     Params:
576         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
577             via which to send messages to the server.
578         channel = Channel whose topic to change.
579         content = Topic body text.
580         properties = Custom message properties, such as [Message.Property.quiet]
581             and [Message.Property.forced].
582         caller = String name of the calling function, or something else that gives context.
583  +/
584 void topic(
585     IRCPluginState state,
586     const string channel,
587     const string content,
588     const Message.Property properties = Message.Property.none,
589     const string caller = __FUNCTION__)
590 in (channel.length, "Tried to set a topic but no channel was given")
591 {
592     Message m;
593 
594     m.event.type = IRCEvent.Type.TOPIC;
595     m.event.channel = channel;
596     m.event.content = content.expandIRCTags;
597     m.properties = properties;
598     m.caller = caller;
599 
600     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
601     else state.mainThread.send(m);
602 }
603 
604 ///
605 unittest
606 {
607     IRCPluginState state;
608     state.mainThread = thisTid;
609 
610     topic(state, "#channel", "content");
611 
612     receive(
613         (Message m)
614         {
615             with (m.event)
616             {
617                 assert((type == IRCEvent.Type.TOPIC), Enum!(IRCEvent.Type).toString(type));
618                 assert((channel == "#channel"), channel);
619                 assert((content == "content"), content);
620                 assert(m.properties == Message.Property.init);
621             }
622         }
623     );
624 }
625 
626 
627 // invite
628 /++
629     Invites a user to a channel.
630 
631     Params:
632         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
633             via which to send messages to the server.
634         channel = Channel to which to invite the user.
635         nickname = Nickname of user to invite.
636         properties = Custom message properties, such as [Message.Property.quiet]
637             and [Message.Property.forced].
638         caller = String name of the calling function, or something else that gives context.
639  +/
640 void invite(
641     IRCPluginState state,
642     const string channel,
643     const string nickname,
644     const Message.Property properties = Message.Property.none,
645     const string caller = __FUNCTION__)
646 in (channel.length, "Tried to send an invite but no channel was given")
647 in (nickname.length, "Tried to send an invite but no nickname was given")
648 {
649     Message m;
650 
651     m.event.type = IRCEvent.Type.INVITE;
652     m.event.channel = channel;
653     m.event.target.nickname = nickname;
654     m.properties = properties;
655     m.caller = caller;
656 
657     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
658     else state.mainThread.send(m);
659 }
660 
661 ///
662 unittest
663 {
664     IRCPluginState state;
665     state.mainThread = thisTid;
666 
667     invite(state, "#channel", "kameloso");
668 
669     receive(
670         (Message m)
671         {
672             with (m.event)
673             {
674                 assert((type == IRCEvent.Type.INVITE), Enum!(IRCEvent.Type).toString(type));
675                 assert((channel == "#channel"), channel);
676                 assert((target.nickname == "kameloso"), target.nickname);
677                 assert(m.properties == Message.Property.init);
678             }
679         }
680     );
681 }
682 
683 
684 // join
685 /++
686     Joins a channel.
687 
688     Params:
689         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
690             via which to send messages to the server.
691         channel = Channel to join.
692         key = Channel key to join the channel with, if it's locked.
693         properties = Custom message properties, such as [Message.Property.quiet]
694             and [Message.Property.forced].
695         caller = String name of the calling function, or something else that gives context.
696  +/
697 void join(
698     IRCPluginState state,
699     const string channel,
700     const string key = string.init,
701     const Message.Property properties = Message.Property.none,
702     const string caller = __FUNCTION__)
703 in (channel.length, "Tried to join a channel but no channel was given")
704 {
705     Message m;
706 
707     m.event.type = IRCEvent.Type.JOIN;
708     m.event.channel = channel;
709     m.event.aux[0] = key;
710     m.properties = properties;
711     m.caller = caller;
712 
713     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
714     else state.mainThread.send(m);
715 }
716 
717 ///
718 unittest
719 {
720     IRCPluginState state;
721     state.mainThread = thisTid;
722 
723     join(state, "#channel");
724 
725     receive(
726         (Message m)
727         {
728             with (m.event)
729             {
730                 assert((type == IRCEvent.Type.JOIN), Enum!(IRCEvent.Type).toString(type));
731                 assert((channel == "#channel"), channel);
732                 assert(m.properties == Message.Property.init);
733             }
734         }
735     );
736 }
737 
738 
739 // kick
740 /++
741     Kicks a user from a channel.
742 
743     Params:
744         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
745             via which to send messages to the server.
746         channel = Channel from which to kick the user.
747         nickname = Nickname of user to kick.
748         reason = Optionally the reason behind the kick.
749         properties = Custom message properties, such as [Message.Property.quiet]
750             and [Message.Property.forced].
751         caller = String name of the calling function, or something else that gives context.
752  +/
753 void kick(
754     IRCPluginState state,
755     const string channel,
756     const string nickname,
757     const string reason = string.init,
758     const Message.Property properties = Message.Property.none,
759     const string caller = __FUNCTION__)
760 in (channel.length, "Tried to kick someone but no channel was given")
761 in (nickname.length, "Tried to kick someone but no nickname was given")
762 {
763     Message m;
764 
765     m.event.type = IRCEvent.Type.KICK;
766     m.event.channel = channel;
767     m.event.target.nickname = nickname;
768     m.event.content = reason.expandIRCTags;
769     m.properties = properties;
770     m.caller = caller;
771 
772     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
773     else state.mainThread.send(m);
774 }
775 
776 ///
777 unittest
778 {
779     IRCPluginState state;
780     state.mainThread = thisTid;
781 
782     kick(state, "#channel", "kameloso", "content");
783 
784     receive(
785         (Message m)
786         {
787             with (m.event)
788             {
789                 assert((type == IRCEvent.Type.KICK), Enum!(IRCEvent.Type).toString(type));
790                 assert((channel == "#channel"), channel);
791                 assert((content == "content"), content);
792                 assert((target.nickname == "kameloso"), target.nickname);
793                 assert(m.properties == Message.Property.init);
794             }
795         }
796     );
797 }
798 
799 
800 // part
801 /++
802     Leaves a channel.
803 
804     Params:
805         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
806             via which to send messages to the server.
807         channel = Channel to leave.
808         reason = Optionally, reason behind leaving.
809         properties = Custom message properties, such as [Message.Property.quiet]
810             and [Message.Property.forced].
811         caller = String name of the calling function, or something else that gives context.
812  +/
813 void part(
814     IRCPluginState state,
815     const string channel,
816     const string reason = string.init,
817     const Message.Property properties = Message.Property.none,
818     const string caller = __FUNCTION__)
819 in (channel.length, "Tried to part a channel but no channel was given")
820 {
821     Message m;
822 
823     m.event.type = IRCEvent.Type.PART;
824     m.event.channel = channel;
825     m.event.content = reason.length ? reason.expandIRCTags : state.bot.partReason;
826     m.properties = properties;
827     m.caller = caller;
828 
829     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
830     else state.mainThread.send(m);
831 }
832 
833 ///
834 unittest
835 {
836     IRCPluginState state;
837     state.mainThread = thisTid;
838 
839     part(state, "#channel", "reason");
840 
841     receive(
842         (Message m)
843         {
844             with (m.event)
845             {
846                 assert((type == IRCEvent.Type.PART), Enum!(IRCEvent.Type).toString(type));
847                 assert((channel == "#channel"), channel);
848                 assert((content == "reason"), content);
849                 assert(m.properties == Message.Property.init);
850             }
851         }
852     );
853 }
854 
855 
856 // quit
857 /++
858     Disconnects from the server, optionally with a quit reason.
859 
860     Params:
861         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
862             via which to send messages to the server.
863         reason = Optionally, the reason for quitting.
864         properties = Custom message properties, such as [Message.Property.quiet]
865             and [Message.Property.forced].
866         caller = String name of the calling function, or something else that gives context.
867  +/
868 
869 void quit(
870     IRCPluginState state,
871     const string reason = string.init,
872     const Message.Property properties = Message.Property.none,
873     const string caller = __FUNCTION__)
874 {
875     Message m;
876 
877     m.event.type = IRCEvent.Type.QUIT;
878     m.event.content = reason.length ? reason : state.bot.quitReason;
879     m.caller = caller;
880     m.properties = (properties | Message.Property.priority);
881 
882     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
883     else state.mainThread.send(m);
884 }
885 
886 ///
887 unittest
888 {
889     IRCPluginState state;
890     state.mainThread = thisTid;
891 
892     enum properties = Message.Property.quiet;
893     quit(state, "reason", properties);
894 
895     receive(
896         (Message m)
897         {
898             with (m.event)
899             {
900                 assert((type == IRCEvent.Type.QUIT), Enum!(IRCEvent.Type).toString(type));
901                 assert((content == "reason"), content);
902                 assert(m.caller.length);
903                 assert(m.properties & (Message.Property.forced | Message.Property.priority | Message.Property.quiet));
904             }
905         }
906     );
907 }
908 
909 
910 // whois
911 /++
912     Queries the server for WHOIS information about a user.
913 
914     Params:
915         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
916             via which to send messages to the server.
917         nickname = String nickname to query for.
918         properties = Custom message properties, such as [Message.Property.quiet]
919             and [Message.Property.forced].
920         caller = String name of the calling function, or something else that gives context.
921  +/
922 void whois(
923     IRCPluginState state,
924     const string nickname,
925     const Message.Property properties = Message.Property.none,
926     const string caller = __FUNCTION__)
927 in (nickname.length, caller ~ " tried to WHOIS but no nickname was given")
928 {
929     Message m;
930 
931     m.event.type = IRCEvent.Type.RPL_WHOISACCOUNT;
932     m.event.target.nickname = nickname;
933     m.properties = properties;
934     m.caller = caller;
935 
936     version(TraceWhois)
937     {
938         import std.stdio : stdout, writefln;
939         writefln("[TraceWhois] messaging.whois caught request to WHOIS \"%s\" " ~
940             "from %s (priority:%s force:%s, quiet:%s, background:%s)",
941             nickname, caller, cast(bool)priority, force, quiet, background);
942         if (state.settings.flush) stdout.flush();
943     }
944 
945     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
946     else state.mainThread.send(m);
947 }
948 
949 ///
950 unittest
951 {
952     IRCPluginState state;
953     state.mainThread = thisTid;
954 
955     enum properties = Message.Property.forced;
956     whois(state, "kameloso", properties);
957 
958     receive(
959         (Message m)
960         {
961             with (m.event)
962             {
963                 assert((type == IRCEvent.Type.RPL_WHOISACCOUNT), Enum!(IRCEvent.Type).toString(type));
964                 assert((target.nickname == "kameloso"), target.nickname);
965                 assert(m.properties & Message.Property.forced);
966             }
967         }
968     );
969 }
970 
971 
972 // raw
973 /++
974     Sends text to the server, verbatim.
975 
976     This is used to send messages of types for which there exist no helper functions.
977 
978     See_Also:
979         [immediate]
980 
981     Params:
982         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
983             via which to send messages to the server.
984         line = Raw IRC string to send to the server.
985         properties = Custom message properties, such as [Message.Property.quiet]
986             and [Message.Property.forced].
987         caller = String name of the calling function, or something else that gives context.
988  +/
989 void raw(
990     IRCPluginState state,
991     const string line,
992     const Message.Property properties = Message.Property.none,
993     const string caller = __FUNCTION__)
994 {
995     Message m;
996 
997     m.event.type = IRCEvent.Type.UNSET;
998     m.event.content = line.expandIRCTags;
999     m.properties = properties;
1000     m.caller = caller;
1001 
1002     if (properties & Message.Property.priority) state.mainThread.prioritySend(m);
1003     else state.mainThread.send(m);
1004 }
1005 
1006 ///
1007 unittest
1008 {
1009     IRCPluginState state;
1010     state.mainThread = thisTid;
1011 
1012     raw(state, "commands");
1013 
1014     receive(
1015         (Message m)
1016         {
1017             with (m.event)
1018             {
1019                 assert((type == IRCEvent.Type.UNSET), Enum!(IRCEvent.Type).toString(type));
1020                 assert((content == "commands"), content);
1021                 assert(m.properties == Message.Property.init);
1022             }
1023         }
1024     );
1025 }
1026 
1027 
1028 // immediate
1029 /++
1030     Immediately sends text to the server, verbatim. Skips all queues.
1031 
1032     This is used to send messages of types for which there exist no helper
1033     functions, and where they must be sent at once.
1034 
1035     See_Also:
1036         [raw]
1037 
1038     Params:
1039         state = The current plugin's [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
1040             via which to send messages to the server.
1041         line = Raw IRC string to send to the server.
1042         properties = Custom message properties, such as [Message.Property.quiet]
1043             and [Message.Property.forced].
1044         caller = String name of the calling function, or something else that gives context.
1045  +/
1046 void immediate(
1047     IRCPluginState state,
1048     const string line,
1049     const Message.Property properties = Message.Property.none,
1050     const string caller = __FUNCTION__)
1051 {
1052     Message m;
1053 
1054     m.event.type = IRCEvent.Type.UNSET;
1055     m.event.content = line.expandIRCTags;
1056     m.caller = caller;
1057     m.properties = (properties | Message.Property.immediate);
1058 
1059     state.mainThread.prioritySend(m);
1060 }
1061 
1062 ///
1063 unittest
1064 {
1065     IRCPluginState state;
1066     state.mainThread = thisTid;
1067 
1068     immediate(state, "commands");
1069 
1070     receive(
1071         (Message m)
1072         {
1073             with (m.event)
1074             {
1075                 assert((type == IRCEvent.Type.UNSET), Enum!(IRCEvent.Type).toString(type));
1076                 assert((content == "commands"), content);
1077                 assert(m.properties & Message.Property.immediate);
1078             }
1079         }
1080     );
1081 }
1082 
1083 /// Merely an alias to [immediate], because we use both terms at different places.
1084 alias immediateline = immediate;
1085 
1086 
1087 // askToOutputImpl
1088 /++
1089     Sends a concurrency message asking to print the supplied text to the local
1090     terminal, instead of doing it directly.
1091 
1092     Params:
1093         logLevel = The [kameloso.logger.LogLevel|LogLevel] at which to log the message.
1094         state = Current [kameloso.plugins.common.core.IRCPluginState|IRCPluginState],
1095             used to send the concurrency message to the main thread.
1096         line = The text body to ask the main thread to display.
1097  +/
1098 void askToOutputImpl(string logLevel)(IRCPluginState state, const string line)
1099 {
1100     import kameloso.thread : OutputRequest;
1101     import std.concurrency : prioritySend;
1102 
1103     mixin("state.mainThread.prioritySend(OutputRequest(OutputRequest.Level.", logLevel, ", line));");
1104 }
1105 
1106 
1107 /+
1108     Generate `askToLevel` family of functions at compile-time, provided the compiler
1109     is recent enough to support it. Too old compilers fail at resolving the "static"
1110     [askToWarn] alias.
1111 
1112     For older compilers, just provide the handwritten aliases.
1113  +/
1114 static if (__VERSION__ >= 2099L)
1115 {
1116     private import kameloso.thread : OutputRequest;
1117     private import std.string : capitalize;
1118     private import std.traits : EnumMembers;
1119 
1120     static foreach (immutable member; EnumMembers!(OutputRequest.Level))
1121     {
1122         mixin(
1123 `
1124         /// Sends a concurrency message to the main thread to [KamelosoLogger.trace] text to the local terminal.
1125         alias askTo` ~ __traits(identifier, member).capitalize ~ ` =
1126             askToOutputImpl!"` ~ __traits(identifier, member) ~ `";
1127 `);
1128     }
1129 
1130     /// Simple alias to [askToWarn], because both spellings are right.
1131     alias askToWarn = askToWarning;
1132 }
1133 else
1134 {
1135     /// Sends a concurrency message to the main thread asking to print text to the local terminal.
1136     alias askToWriteln = askToOutputImpl!"writeln";
1137     /// Sends a concurrency message to the main thread to [KamelosoLogger.trace] text to the local terminal.
1138     alias askToTrace = askToOutputImpl!"trace";
1139     /// Sends a concurrency message to the main thread to [KamelosoLogger.log] text to the local terminal.
1140     alias askToLog = askToOutputImpl!"log";
1141     /// Sends a concurrency message to the main thread to [KamelosoLogger.info] text to the local terminal.
1142     alias askToInfo = askToOutputImpl!"info";
1143     /// Sends a concurrency message to the main thread to [KamelosoLogger.warning] text to the local terminal.
1144     alias askToWarn = askToOutputImpl!"warning";
1145     /// Simple alias to [askToWarn], because both spellings are right.
1146     alias askToWarning = askToWarn;
1147     /// Sends a concurrency message to the main thread to [KamelosoLogger.error] text to the local terminal.
1148     alias askToError = askToOutputImpl!"error";
1149     /// Sends a concurrency message to the main thread to [KamelosoLogger.critical] text to the local terminal.
1150     alias askToCritical = askToOutputImpl!"critical";
1151     /// Sends a concurrency message to the main thread to [KamelosoLogger.fatal] text to the local terminal.
1152     alias askToFatal = askToOutputImpl!"fatal";
1153 }
1154 
1155 unittest
1156 {
1157     import kameloso.thread : OutputRequest;
1158 
1159     IRCPluginState state;
1160     state.mainThread = thisTid;
1161 
1162     state.askToWriteln("writeln");
1163     state.askToTrace("trace");
1164     state.askToLog("log");
1165     state.askToInfo("info");
1166     state.askToWarn("warning");
1167     state.askToError("error");
1168     state.askToCritical("critical");
1169 
1170     alias T = OutputRequest.Level;
1171 
1172     static immutable T[7] expectedLevels =
1173     [
1174         T.writeln,
1175         T.trace,
1176         T.log,
1177         T.info,
1178         T.warning,
1179         T.error,
1180         T.critical,
1181     ];
1182 
1183     static immutable string[7] expectedMessages =
1184     [
1185         "writeln",
1186         "trace",
1187         "log",
1188         "info",
1189         "warning",
1190         "error",
1191         "critical",
1192     ];
1193 
1194     static assert(expectedLevels.length == expectedMessages.length);
1195 
1196     foreach (immutable i; 0..expectedMessages.length)
1197     {
1198         import std.concurrency : receiveTimeout;
1199         import std.variant : Variant;
1200         import core.time : Duration;
1201 
1202         cast(void)receiveTimeout(Duration.zero,
1203             (OutputRequest request)
1204             {
1205                 assert((request.logLevel == expectedLevels[i]), request.logLevel.to!string);
1206                 assert((request.line == expectedMessages[i]), request.line);
1207             },
1208             (Variant _)
1209             {
1210                 assert(0, "Receive loop test in `messaging.d` failed.");
1211             }
1212         );
1213     }
1214 }