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 }