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 }