1 // TODO: more accurate/atomic tracking starting procedure 2 module irc.tracker; 3 4 import irc.client; 5 import irc.util : ExceptionConstructor; 6 7 import std.exception : enforceEx; 8 import std.traits : Unqual; 9 import std.typetuple : TypeTuple; 10 11 /// 12 class IrcTrackingException : Exception 13 { 14 mixin ExceptionConstructor!(); 15 } 16 17 // TODO: Add example 18 /** 19 * Create a new channel and user tracking object for the given 20 * $(DPREF _client, IrcClient). Tracking for the new object 21 * is initially disabled; use $(MREF IrcTracker.start) to commence tracking. 22 * 23 * Params: 24 * Payload = type of extra storage per $(MREF TrackedUser) object 25 * See_Also: 26 * $(MREF IrcTracker), $(MREF TrackedUser.payload) 27 */ 28 CustomIrcTracker!Payload track(Payload = void)(IrcClient client) 29 { 30 return new typeof(return)(client); 31 } 32 33 /** 34 * Keeps track of all channels and channel members 35 * visible to the associated $(DPREF client, IrcClient) connection. 36 * 37 * Params: 38 * Payload = type of extra storage per $(MREF TrackedUser) object 39 * See_Also: 40 * $(MREF CustomTrackedUser.payload) 41 */ 42 class CustomIrcTracker(Payload = void) 43 { 44 // TODO: mode tracking 45 private: 46 IrcClient _client; 47 CustomTrackedChannel!Payload[string] _channels; 48 CustomTrackedUser!Payload*[string] _users; 49 CustomTrackedUser!Payload* thisUser; 50 51 enum State { disabled, starting, enabled } 52 auto _isTracking = State.disabled; 53 54 debug(IrcTracker) import std.stdio; 55 56 final: 57 debug(IrcTracker) void checkIntegrity() 58 { 59 import std.algorithm; 60 61 if(!isTracking) 62 { 63 assert(channels.empty); 64 assert(_channels is null); 65 assert(_users is null); 66 return; 67 } 68 69 foreach(channel; channels) 70 { 71 assert(channel.name.length != 0); 72 assert(channel.users.length != 0); 73 foreach(member; channel.users) 74 { 75 auto user = findUser(member.nickName); 76 assert(user); 77 assert(user == member); 78 } 79 } 80 81 assert(thisUser == _users[thisUser.nickName]); 82 83 foreach(user; users) 84 if(user != thisUser) 85 assert(channels.map!(chan => chan.users).joiner().canFind(user), "unable to find " ~ user.nickName ~ " in any channels"); 86 } 87 88 void onConnect() 89 { 90 thisUser.nickName = _client.nickName; 91 thisUser.userName = _client.userName; 92 thisUser.realName = _client.realName; 93 94 debug(IrcTracker) writeln("tracker connected; thisUser = ", *thisUser); 95 } 96 97 void onSuccessfulJoin(in char[] channelName) 98 { 99 debug(IrcTracker) 100 { 101 writeln("onmejoin: ", channelName); 102 checkIntegrity(); 103 } 104 105 auto channel = CustomTrackedChannel!Payload(channelName.idup); 106 channel._users = [_client.nickName: thisUser]; 107 _channels[channel.name] = channel; 108 109 debug(IrcTracker) 110 { 111 write("checking... "); 112 checkIntegrity(); 113 writeln("done."); 114 } 115 } 116 117 void onNameList(in char[] channelName, in char[][] nickNames) 118 { 119 debug(IrcTracker) 120 { 121 writefln("names %s: %(%s%|, %)", channelName, nickNames); 122 checkIntegrity(); 123 } 124 125 auto channel = _channels[channelName]; 126 127 foreach(nickName; nickNames) 128 { 129 if(auto pUser = nickName in _users) 130 { 131 auto user = *pUser; 132 user.channels ~= channel.name; 133 channel._users[cast(immutable)nickName] = user; 134 } 135 else 136 { 137 auto immNick = nickName.idup; 138 139 auto user = new CustomTrackedUser!Payload(immNick); 140 user.channels = [channel.name]; 141 142 channel._users[immNick] = user; 143 _users[immNick] = user; 144 } 145 } 146 147 debug(IrcTracker) 148 { 149 import std.algorithm : map; 150 writeln(channel._users.values.map!(user => *user)); 151 write("checking... "); 152 checkIntegrity(); 153 writeln("done."); 154 } 155 } 156 157 void onJoin(IrcUser user, in char[] channelName) 158 { 159 debug(IrcTracker) 160 { 161 writefln("%s joined %s", user.nickName, channelName); 162 checkIntegrity(); 163 } 164 165 auto channel = _channels[channelName]; 166 167 if(auto pUser = user.nickName in _users) 168 { 169 auto storedUser = *pUser; 170 if(!storedUser.userName) 171 storedUser.userName = user.userName.idup; 172 if(!storedUser.hostName) 173 storedUser.hostName = user.hostName.idup; 174 175 storedUser.channels ~= channel.name; 176 channel._users[user.nickName] = storedUser; 177 } 178 else 179 { 180 auto immNick = user.nickName.idup; 181 182 auto newUser = new CustomTrackedUser!Payload(immNick); 183 newUser.userName = user.userName.idup; 184 newUser.hostName = user.hostName.idup; 185 newUser.channels = [channel.name]; 186 187 _users[immNick] = newUser; 188 channel._users[immNick] = newUser; 189 } 190 191 debug(IrcTracker) 192 { 193 write("checking... "); 194 checkIntegrity(); 195 writeln("done."); 196 } 197 } 198 199 // Utility function 200 void onMeLeave(in char[] channelName) 201 { 202 import std.algorithm : countUntil, remove, SwapStrategy; 203 204 debug(IrcTracker) 205 { 206 writeln("onmeleave: ", channelName); 207 checkIntegrity(); 208 } 209 210 auto channel = _channels[channelName]; 211 212 foreach(ref user; channel._users) 213 { 214 auto channelIndex = user.channels.countUntil(channelName); 215 assert(channelIndex != -1); 216 user.channels = user.channels.remove!(SwapStrategy.unstable)(channelIndex); 217 if(user.channels.length == 0 && user.nickName != client.nickName) 218 _users.remove(cast(immutable)user.nickName); 219 } 220 221 _channels.remove(channel.name); 222 223 debug(IrcTracker) 224 { 225 write("checking... "); 226 checkIntegrity(); 227 writeln("done."); 228 } 229 } 230 231 // Utility function 232 void onLeave(in char[] channelName, in char[] nick) 233 { 234 import std.algorithm : countUntil, remove, SwapStrategy; 235 236 debug(IrcTracker) 237 { 238 writefln("%s left %s", nick, channelName); 239 checkIntegrity(); 240 } 241 242 _channels[channelName]._users.remove(cast(immutable)nick); 243 244 auto pUser = nick in _users; 245 auto user = *pUser; 246 auto channelIndex = user.channels.countUntil(channelName); 247 assert(channelIndex != -1); 248 user.channels = user.channels.remove!(SwapStrategy.unstable)(channelIndex); 249 if(user.channels.length == 0) 250 _users.remove(cast(immutable)nick); 251 252 debug(IrcTracker) 253 { 254 write("checking... "); 255 checkIntegrity(); 256 writeln("done."); 257 } 258 } 259 260 void onPart(IrcUser user, in char[] channelName) 261 { 262 if(user.nickName == client.nickName) 263 onMeLeave(channelName); 264 else 265 onLeave(channelName, user.nickName); 266 } 267 268 void onKick(IrcUser kicker, in char[] channelName, in char[] nick, in char[] comment) 269 { 270 debug(IrcTracker) writefln(`%s kicked %s: %s`, kicker.nickName, nick, comment); 271 if(nick == client.nickName) 272 onMeLeave(channelName); 273 else 274 onLeave(channelName, nick); 275 } 276 277 void onQuit(IrcUser user, in char[] comment) 278 { 279 debug(IrcTracker) 280 { 281 writefln("%s quit", user.nickName); 282 checkIntegrity(); 283 } 284 285 foreach(channelName; _users[user.nickName].channels) 286 { 287 debug(IrcTracker) writefln("%s left %s by quitting", user.nickName, channelName); 288 _channels[channelName]._users.remove(cast(immutable)user.nickName); 289 } 290 291 _users.remove(cast(immutable)user.nickName); 292 293 debug(IrcTracker) 294 { 295 write("checking... "); 296 checkIntegrity(); 297 writeln("done."); 298 } 299 } 300 301 void onNickChange(IrcUser user, in char[] newNick) 302 { 303 debug(IrcTracker) 304 { 305 writefln("%s changed nick to %s", user.nickName, newNick); 306 checkIntegrity(); 307 } 308 309 auto userInSet = _users[user.nickName]; 310 _users.remove(cast(immutable)user.nickName); 311 312 auto immNewNick = newNick.idup; 313 userInSet.nickName = immNewNick; 314 _users[immNewNick] = userInSet; 315 316 debug(IrcTracker) 317 { 318 write("checking... "); 319 checkIntegrity(); 320 writeln("done."); 321 } 322 } 323 324 alias eventHandlers = TypeTuple!( 325 onConnect, onSuccessfulJoin, onNameList, onJoin, onPart, onKick, onQuit, onNickChange 326 ); 327 328 // Start tracking functions 329 void onMyChannelsReply(in char[] nick, in char[][] channels) 330 { 331 if(nick != client.nickName) 332 return; 333 334 _client.onWhoisChannelsReply.unsubscribeHandler(&onMyChannelsReply); 335 _client.onWhoisEnd.unsubscribeHandler(&onWhoisEnd); 336 337 if(_isTracking != State.starting) 338 return; 339 340 startNow(); 341 342 foreach(channel; channels) 343 onSuccessfulJoin(channel); 344 345 _client.queryNames(channels); 346 } 347 348 void onWhoisEnd(in char[] nick) 349 { 350 if(nick != client.nickName) 351 return; 352 353 // Weren't in any channels. 354 355 _client.onWhoisChannelsReply.unsubscribeHandler(&onMyChannelsReply); 356 _client.onWhoisEnd.unsubscribeHandler(&onWhoisEnd); 357 358 if(_isTracking != State.starting) 359 return; 360 361 startNow(); 362 } 363 364 private void startNow() 365 { 366 assert(_isTracking != State.enabled); 367 368 foreach(handler; eventHandlers) 369 mixin("client." ~ __traits(identifier, handler)) ~= &handler; 370 371 auto thisNick = _client.nickName; 372 thisUser = new CustomTrackedUser!Payload(thisNick); 373 thisUser.userName = _client.userName; 374 thisUser.realName = _client.realName; 375 _users[thisNick] = thisUser; 376 377 _isTracking = State.enabled; 378 } 379 380 public: 381 this(IrcClient client) 382 { 383 this._client = client; 384 } 385 386 ~this() 387 { 388 stop(); 389 } 390 391 /** 392 * Initiate or restart tracking, or do nothing if the tracker is already tracking. 393 * 394 * If the associated client is unconnected, tracking starts immediately. 395 * If it is connected, information about the client's current channels will be queried, 396 * and tracking starts as soon as the information has been received. 397 */ 398 void start() 399 { 400 if(_isTracking != State.disabled) 401 return; 402 403 if(_client.connected) 404 { 405 _client.onWhoisChannelsReply ~= &onMyChannelsReply; 406 _client.onWhoisEnd ~= &onWhoisEnd; 407 _client.queryWhois(_client.nickName); 408 _isTracking = State.starting; 409 } 410 else 411 startNow(); 412 } 413 414 /** 415 * Stop tracking, or do nothing if the tracker is not currently tracking. 416 */ 417 void stop() 418 { 419 final switch(_isTracking) 420 { 421 case State.enabled: 422 _users = null; 423 thisUser = null; 424 _channels = null; 425 foreach(handler; eventHandlers) 426 mixin("client." ~ __traits(identifier, handler)).unsubscribeHandler(&handler); 427 break; 428 case State.starting: 429 _client.onWhoisChannelsReply.unsubscribeHandler(&onMyChannelsReply); 430 _client.onWhoisEnd.unsubscribeHandler(&onWhoisEnd); 431 break; 432 case State.disabled: 433 return; 434 } 435 436 _isTracking = State.disabled; 437 } 438 439 /// Boolean whether or not the tracker is currently tracking. 440 bool isTracking() const @property @safe pure nothrow 441 { 442 return _isTracking == State.enabled; 443 } 444 445 /// $(DPREF _client, IrcClient) that this tracker is tracking for. 446 inout(IrcClient) client() inout @property @safe pure nothrow 447 { 448 return _client; 449 } 450 451 /** 452 * $(D InputRange) (with $(D length)) of all _channels the associated client is currently 453 * a member of. 454 * Throws: 455 * $(MREF IrcTrackingException) if the tracker is disabled or not yet ready 456 */ 457 auto channels() @property 458 { 459 import std.range : takeExactly; 460 enforceEx!IrcTrackingException(_isTracking, "not currently tracking"); 461 return _channels.byValue.takeExactly(_channels.length); 462 } 463 464 unittest 465 { 466 import std.range; 467 static assert(isInputRange!(typeof(CustomIrcTracker.init.channels))); 468 static assert(is(ElementType!(typeof(CustomIrcTracker.init.channels)) : CustomTrackedChannel!Payload)); 469 static assert(hasLength!(typeof(CustomIrcTracker.init.channels))); 470 } 471 472 /** 473 * $(D InputRange) (with $(D length)) of all _users currently seen by the associated client. 474 * 475 * The range includes the user for the associated client. Users that are not a member of any 476 * of the channels the associated client is a member of, but have sent a private message to 477 * the associated client, are $(I not) included. 478 * Throws: 479 * $(MREF IrcTrackingException) if the tracker is disabled or not yet ready 480 */ 481 auto users() @property 482 { 483 import std.algorithm : map; 484 import std.range : takeExactly; 485 enforceEx!IrcTrackingException(_isTracking, "not currently tracking"); 486 return _users.byValue.takeExactly(_users.length); 487 } 488 489 unittest 490 { 491 import std.range; 492 static assert(isInputRange!(typeof(CustomIrcTracker.init.users))); 493 static assert(is(ElementType!(typeof(CustomIrcTracker.init.users)) == CustomTrackedUser!Payload*)); 494 static assert(hasLength!(typeof(CustomIrcTracker.init.users))); 495 } 496 497 /** 498 * Lookup a channel on this tracker by name. 499 * 500 * The channel name must include the channel name prefix. Returns $(D null) 501 * if the associated client is not currently a member of the given channel. 502 * Params: 503 * channelName = name of channel to lookup 504 * Throws: 505 * $(MREF IrcTrackingException) if the tracker is disabled or not yet ready 506 * See_Also: 507 * $(MREF TrackedChannel) 508 */ 509 CustomTrackedChannel!Payload* findChannel(in char[] channelName) 510 { 511 enforceEx!IrcTrackingException(_isTracking, "not currently tracking"); 512 return channelName in _channels; 513 } 514 515 /** 516 * Lookup a user on this tracker by nick name. 517 * 518 * Users are searched among the members of all channels the associated 519 * client is currently a member of. The set includes the user for the 520 * associated client. 521 * Params: 522 * nickName = nick name of user to lookup 523 * Throws: 524 * $(MREF IrcTrackingException) if the tracker is disabled or not yet ready 525 * See_Also: 526 * $(MREF TrackedUser) 527 */ 528 CustomTrackedUser!Payload* findUser(in char[] nickName) 529 { 530 enforceEx!IrcTrackingException(_isTracking, "not currently tracking"); 531 if(auto user = nickName in _users) 532 return *user; 533 else 534 return null; 535 } 536 } 537 538 /// Ditto 539 alias IrcTracker = CustomIrcTracker!void; 540 541 /** 542 * Represents an IRC channel and its member users for use by $(MREF IrcTracker). 543 * 544 * The list of members includes the user associated with the tracking object. 545 * If the $(D IrcTracker) used to access an instance of this type 546 * was since stopped, the channel presents the list of members as it were 547 * at the time of the tracker being stopped. 548 * 549 * Params: 550 * Payload = type of extra storage per $(MREF TrackedUser) object 551 * See_Also: 552 * $(MREF CustomTrackedUser.payload) 553 */ 554 struct CustomTrackedChannel(Payload = void) 555 { 556 private: 557 string _name; 558 CustomTrackedUser!Payload*[string] _users; 559 560 this(string name, CustomTrackedUser!Payload*[string] users = null) 561 { 562 _name = name; 563 _users = users; 564 } 565 566 public: 567 @disable this(); 568 569 /// Name of the channel, including the channel prefix. 570 string name() @property 571 { 572 return _name; 573 } 574 575 /// $(D InputRange) of all member _users of this channel, 576 /// where each user is given as a $(D (MREF TrackedUser)*). 577 auto users() @property 578 { 579 import std.range : takeExactly; 580 return _users.byValue.takeExactly(_users.length); 581 } 582 583 /** 584 * Lookup a member of this channel by nick name. 585 * $(D null) is returned if the given nick is not a member 586 * of this channel. 587 * Params: 588 * nick = nick name of member to lookup 589 */ 590 CustomTrackedUser!Payload* opBinary(string op : "in")(in char[] nick) 591 { 592 enforceEx!IrcTrackingException(cast(bool)this, "the TrackedChannel is invalid"); 593 if(auto pUser = nick in _users) 594 return *pUser; 595 else 596 return null; 597 } 598 599 static if(!is(Payload == void)) 600 { 601 TrackedChannel erasePayload() @property 602 { 603 return TrackedChannel(_name, cast(TrackedUser*[string])_users); 604 } 605 606 alias erasePayload this; 607 } 608 } 609 610 /// Ditto 611 alias TrackedChannel = CustomTrackedChannel!void; 612 613 /** 614 * Represents an IRC user for use by $(MREF IrcTracker). 615 */ 616 struct TrackedUser 617 { 618 private: 619 this(string nickName) 620 { 621 this.nickName = nickName; 622 } 623 624 public: 625 @disable this(); 626 627 /** 628 * Nick name, user name and host name of the _user. 629 * 630 * $(D TrackedUser) is a super-type of $(DPREF protocol, IrcUser). 631 * 632 * Only the nick name is guaranteed to be non-null. 633 * See_Also: 634 * $(DPREF protocol, IrcUser) 635 */ 636 IrcUser user; 637 638 /// Ditto 639 alias user this; 640 641 /** 642 * Real name of the user. Is $(D null) unless a whois-query 643 * has been successfully issued for the user. 644 * 645 * See_Also: 646 * $(DPREF client, IrcClient.queryWhois) 647 */ 648 string realName; 649 650 /** 651 * Channels in which both the current user and the tracked 652 * user share membership. 653 * 654 * See_Also: 655 * $(DPREF client, IrcClient.queryWhois) to query channels 656 * a user is in, regardless of shared membership with the current user. 657 */ 658 string[] channels; 659 660 void toString(scope void delegate(const(char)[]) sink) const 661 { 662 import std.format; 663 user.toString(sink); 664 665 if(realName) 666 { 667 sink("$"); 668 sink(realName); 669 } 670 671 formattedWrite(sink, "(%(%s%|,%))", channels); 672 } 673 674 unittest 675 { 676 import std.string : format; 677 auto user = TrackedUser("nick"); 678 user.userName = "user"; 679 user.realName = "Foo Bar"; 680 user.channels = ["#a", "#b"]; 681 assert(format("%s", user) == `nick!user$Foo Bar("#a","#b")`); 682 } 683 } 684 685 /** 686 * Represents an IRC user for use by $(MREF CustomIrcTracker). 687 * Params: 688 * Payload = type of extra data per user. 689 */ 690 align(1) struct CustomTrackedUser(Payload) 691 { 692 /// $(D CustomTrackedUser) is a super-type of $(MREF TrackedUser). 693 TrackedUser user; 694 695 /// Ditto 696 alias user this; 697 698 /** 699 * Extra data attached to this user for per-application data. 700 */ 701 Payload payload; 702 703 /// 704 this(string nickName) 705 { 706 user = TrackedUser(nickName); 707 } 708 } 709 710 /// 711 alias CustomTrackedUser(Payload : void) = TrackedUser;