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