1from __future__ import absolute_import 2 3import logging 4import re 5import struct 6import subprocess 7import sys 8import threading 9 10from markdown import Markdown 11from markdown.extensions.extra import ExtraExtension 12 13from errbot.backends.base import ( 14 ONLINE, 15 Message, 16 Person, 17 Room, 18 RoomError, 19 RoomNotJoinedError, 20 RoomOccupant, 21 Stream, 22) 23from errbot.core import ErrBot 24from errbot.rendering.ansiext import NSC, AnsiExtension, CharacterTable, enable_format 25from errbot.utils import rate_limited 26 27log = logging.getLogger(__name__) 28 29IRC_CHRS = CharacterTable( 30 fg_black=NSC("\x0301"), 31 fg_red=NSC("\x0304"), 32 fg_green=NSC("\x0303"), 33 fg_yellow=NSC("\x0308"), 34 fg_blue=NSC("\x0302"), 35 fg_magenta=NSC("\x0306"), 36 fg_cyan=NSC("\x0310"), 37 fg_white=NSC("\x0300"), 38 fg_default=NSC("\x03"), 39 bg_black=NSC("\x03,01"), 40 bg_red=NSC("\x03,04"), 41 bg_green=NSC("\x03,03"), 42 bg_yellow=NSC("\x03,08"), 43 bg_blue=NSC("\x03,02"), 44 bg_magenta=NSC("\x03,06"), 45 bg_cyan=NSC("\x03,10"), 46 bg_white=NSC("\x03,00"), 47 bg_default=NSC("\x03,"), 48 fx_reset=NSC("\x03"), 49 fx_bold=NSC("\x02"), 50 fx_italic=NSC("\x1D"), 51 fx_underline=NSC("\x1F"), 52 fx_not_italic=NSC("\x0F"), 53 fx_not_underline=NSC("\x0F"), 54 fx_normal=NSC("\x0F"), 55 fixed_width="", 56 end_fixed_width="", 57 inline_code="", 58 end_inline_code="", 59) 60 61IRC_NICK_REGEX = r"[a-zA-Z\[\]\\`_\^\{\|\}][a-zA-Z0-9\[\]\\`_\^\{\|\}-]+" 62 63try: 64 import irc.connection 65 from irc.bot import SingleServerIRCBot 66 from irc.client import NickMask, ServerNotConnectedError 67except ImportError: 68 log.fatal( 69 """You need the IRC support to use IRC, you can install it with: 70 pip install errbot[IRC] 71 """ 72 ) 73 sys.exit(-1) 74 75 76def irc_md(): 77 """This makes a converter from markdown to mirc color format.""" 78 md = Markdown(output_format="irc", extensions=[ExtraExtension(), AnsiExtension()]) 79 md.stripTopLevelTags = False 80 return md 81 82 83class IRCPerson(Person): 84 def __init__(self, mask): 85 self._nickmask = NickMask(mask) 86 self._email = "" 87 88 @property 89 def nick(self): 90 return self._nickmask.nick 91 92 @property 93 def user(self): 94 return self._nickmask.user 95 96 @property 97 def host(self): 98 return self._nickmask.host 99 100 # generic compatibility 101 person = nick 102 103 @property 104 def client(self): 105 return self._nickmask.userhost 106 107 @property 108 def fullname(self): 109 # TODO: this should be possible to get 110 return None 111 112 @property 113 def email(self): 114 return self._email 115 116 @property 117 def aclattr(self): 118 return IRCBackend.aclpattern.format( 119 nick=self._nickmask.nick, user=self._nickmask.user, host=self._nickmask.host 120 ) 121 122 def __unicode__(self): 123 return str(self._nickmask) 124 125 def __str__(self): 126 return self.__unicode__() 127 128 def __eq__(self, other): 129 if not isinstance(other, IRCPerson): 130 log.warning("Weird you are comparing an IRCPerson to a %s.", type(other)) 131 return False 132 return self.person == other.person 133 134 135class IRCRoomOccupant(IRCPerson, RoomOccupant): 136 def __init__(self, mask, room): 137 super().__init__(mask) 138 self._room = room 139 140 @property 141 def room(self): 142 return self._room 143 144 def __unicode__(self): 145 return self._nickmask 146 147 def __str__(self): 148 return self.__unicode__() 149 150 def __repr__(self): 151 return f"<{self.__unicode__()} - {super().__repr__()}>" 152 153 154class IRCRoom(Room): 155 """ 156 Represent the specifics of a IRC Room/Channel. 157 158 This lifecycle of this object is: 159 - Created in IRCConnection.on_join 160 - The joined status change in IRCConnection on_join/on_part 161 - Deleted/destroyed in IRCConnection.on_disconnect 162 """ 163 164 def __init__(self, room, bot): 165 self._bot = bot 166 self.room = room 167 self.connection = self._bot.conn.connection 168 self._topic_lock = threading.Lock() 169 self._topic = None 170 171 def __unicode__(self): 172 return self.room 173 174 def __str__(self): 175 return self.__unicode__() 176 177 def __repr__(self): 178 return f"<{self.__unicode__()} - {super().__repr__()}>" 179 180 def cb_set_topic(self, current_topic): 181 """ 182 Store the current topic for this room. 183 184 This method is called by the IRC backend when a `currenttopic`, 185 `topic` or `notopic` IRC event is received to store the topic set for this channel. 186 187 This function is not meant to be executed by regular plugins. 188 To get or set 189 """ 190 with self._topic_lock: 191 self._topic = current_topic 192 193 def join(self, username=None, password=None): 194 """ 195 Join the room. 196 197 If the room does not exist yet, this will automatically call 198 :meth:`create` on it first. 199 """ 200 if username is not None: 201 log.debug( 202 "Ignored username parameter on join(), it is unsupported on this back-end." 203 ) 204 if password is None: 205 password = "" # nosec 206 207 self.connection.join(self.room, key=password) 208 self._bot.callback_room_joined(self, self._bot.bot_identifier) 209 log.info("Joined room %s.", self.room) 210 211 def leave(self, reason=None): 212 """ 213 Leave the room. 214 215 :param reason: 216 An optional string explaining the reason for leaving the room 217 """ 218 if reason is None: 219 reason = "" 220 221 self.connection.part(self.room, reason) 222 self._bot.callback_room_left(self, self._bot.bot_identifier) 223 log.info( 224 "Leaving room %s with reason %s.", 225 self.room, 226 reason if reason is not None else "", 227 ) 228 229 def create(self): 230 """ 231 Not supported on this back-end. Will join the room to ensure it exists, instead. 232 """ 233 logging.warning( 234 "IRC back-end does not support explicit creation, joining room instead to ensure it exists." 235 ) 236 self.join() 237 238 def destroy(self): 239 """ 240 Not supported on IRC, will raise :class:`~errbot.backends.base.RoomError`. 241 """ 242 raise RoomError("IRC back-end does not support destroying rooms.") 243 244 @property 245 def exists(self): 246 """ 247 Boolean indicating whether this room already exists or not. 248 249 :getter: 250 Returns `True` if the room exists, `False` otherwise. 251 """ 252 logging.warning( 253 "IRC back-end does not support determining if a room exists. " 254 "Returning the result of joined instead." 255 ) 256 return self.joined 257 258 @property 259 def joined(self): 260 """ 261 Boolean indicating whether this room has already been joined. 262 263 :getter: 264 Returns `True` if the room has been joined, `False` otherwise. 265 """ 266 return self.room in self._bot.conn.channels.keys() 267 268 @property 269 def topic(self): 270 """ 271 The room topic. 272 273 :getter: 274 Returns the topic (a string) if one is set, `None` if no 275 topic has been set at all. 276 """ 277 if not self.joined: 278 raise RoomNotJoinedError("Must join the room to get the topic.") 279 with self._topic_lock: 280 return self._topic 281 282 @topic.setter 283 def topic(self, topic): 284 """ 285 Set the room's topic. 286 287 :param topic: 288 The topic to set. 289 """ 290 if not self.joined: 291 raise RoomNotJoinedError("Must join the room to set the topic.") 292 self.connection.topic(self.room, topic) 293 294 @property 295 def occupants(self): 296 """ 297 The room's occupants. 298 299 :getter: 300 Returns a list of occupants. 301 :raises: 302 :class:`~MUCNotJoinedError` if the room has not yet been joined. 303 """ 304 occupants = [] 305 try: 306 for nick in self._bot.conn.channels[self.room].users(): 307 occupants.append(IRCRoomOccupant(nick, room=self.room)) 308 except KeyError: 309 raise RoomNotJoinedError("Must be in a room in order to see occupants.") 310 return occupants 311 312 def invite(self, *args): 313 """ 314 Invite one or more people into the room. 315 316 :*args: 317 One or more nicks to invite into the room. 318 """ 319 for nick in args: 320 self.connection.invite(nick, self.room) 321 log.info("Invited %s to %s.", nick, self.room) 322 323 def __eq__(self, other): 324 if not isinstance(other, IRCRoom): 325 log.warning( 326 "This is weird you are comparing an IRCRoom to a %s.", type(other) 327 ) 328 return False 329 return self.room == other.room 330 331 332class IRCConnection(SingleServerIRCBot): 333 def __init__( 334 self, 335 bot, 336 nickname, 337 server, 338 port=6667, 339 ssl=False, 340 bind_address=None, 341 ipv6=False, 342 password=None, 343 username=None, 344 nickserv_password=None, 345 private_rate=1, 346 channel_rate=1, 347 reconnect_on_kick=5, 348 reconnect_on_disconnect=5, 349 ): 350 self.use_ssl = ssl 351 self.use_ipv6 = ipv6 352 self.bind_address = bind_address 353 self.bot = bot 354 # manually decorate functions 355 if private_rate: 356 self.send_private_message = rate_limited(private_rate)( 357 self.send_private_message 358 ) 359 360 if channel_rate: 361 self.send_public_message = rate_limited(channel_rate)( 362 self.send_public_message 363 ) 364 self._reconnect_on_kick = reconnect_on_kick 365 self._pending_transfers = {} 366 self._rooms_lock = threading.Lock() 367 self._rooms = {} 368 self._recently_joined_to = set() 369 370 self.nickserv_password = nickserv_password 371 if username is None: 372 username = nickname 373 self.transfers = {} 374 super().__init__( 375 [(server, port, password)], 376 nickname, 377 username, 378 reconnection_interval=reconnect_on_disconnect, 379 ) 380 381 def connect(self, *args, **kwargs): 382 # Decode all input to UTF-8, but use a replacement character for 383 # unrecognized byte sequences 384 # (as described at https://pypi.python.org/pypi/irc) 385 self.connection.buffer_class.errors = "replace" 386 387 connection_factory_kwargs = {} 388 if self.use_ssl: 389 import ssl 390 391 connection_factory_kwargs["wrapper"] = ssl.wrap_socket 392 if self.bind_address is not None: 393 connection_factory_kwargs["bind_address"] = self.bind_address 394 if self.use_ipv6: 395 connection_factory_kwargs["ipv6"] = True 396 397 connection_factory = irc.connection.Factory(**connection_factory_kwargs) 398 self.connection.connect(*args, connect_factory=connection_factory, **kwargs) 399 400 def on_welcome(self, _, e): 401 log.info("IRC welcome %s", e) 402 403 # try to identify with NickServ if there is a NickServ password in the 404 # config 405 if self.nickserv_password: 406 msg = f"identify {self.nickserv_password}" 407 self.send_private_message("NickServ", msg) 408 409 # Must be done in a background thread, otherwise the join room 410 # from the ChatRoom plugin joining channels from CHATROOM_PRESENCE 411 # ends up blocking on connect. 412 t = threading.Thread(target=self.bot.connect_callback) 413 t.setDaemon(True) 414 t.start() 415 416 def _pubmsg(self, e, notice=False): 417 msg = Message(e.arguments[0], extras={"notice": notice}) 418 room_name = e.target 419 if room_name[0] != "#" and room_name[0] != "$": 420 raise Exception(f"[{room_name}] is not a room") 421 room = IRCRoom(room_name, self.bot) 422 msg.frm = IRCRoomOccupant(e.source, room) 423 msg.to = room 424 msg.nick = msg.frm.nick # FIXME find the real nick in the channel 425 self.bot.callback_message(msg) 426 427 possible_mentions = re.findall(IRC_NICK_REGEX, e.arguments[0]) 428 room_users = self.channels[room_name].users() 429 mentions = filter(lambda x: x in room_users, possible_mentions) 430 if mentions: 431 mentions = [self.bot.build_identifier(mention) for mention in mentions] 432 self.bot.callback_mention(msg, mentions) 433 434 def _privmsg(self, e, notice=False): 435 msg = Message(e.arguments[0], extras={"notice": notice}) 436 msg.frm = IRCPerson(e.source) 437 msg.to = IRCPerson(e.target) 438 self.bot.callback_message(msg) 439 440 def on_pubmsg(self, _, e): 441 self._pubmsg(e) 442 443 def on_privmsg(self, _, e): 444 self._privmsg(e) 445 446 def on_pubnotice(self, _, e): 447 self._pubmsg(e, True) 448 449 def on_privnotice(self, _, e): 450 self._privmsg(e, True) 451 452 def on_kick(self, _, e): 453 if not self._reconnect_on_kick: 454 log.info("RECONNECT_ON_KICK is 0 or None, won't try to reconnect") 455 return 456 log.info( 457 "Got kicked out of %s... reconnect in %d seconds... ", 458 e.target, 459 self._reconnect_on_kick, 460 ) 461 462 def reconnect_channel(name): 463 log.info("Reconnecting to %s after having beeing kicked.", name) 464 self.bot.query_room(name).join() 465 466 t = threading.Timer( 467 self._reconnect_on_kick, 468 reconnect_channel, 469 [ 470 e.target, 471 ], 472 ) 473 t.daemon = True 474 t.start() 475 476 def send_private_message(self, to, line): 477 try: 478 self.connection.privmsg(to, line) 479 except ServerNotConnectedError: 480 pass # the message will be lost 481 482 def send_public_message(self, to, line): 483 try: 484 self.connection.privmsg(to, line) 485 except ServerNotConnectedError: 486 pass # the message will be lost 487 488 def on_disconnect(self, connection, event): 489 self._rooms = {} 490 self.bot.disconnect_callback() 491 492 def send_stream_request( 493 self, identifier, fsource, name=None, size=None, stream_type=None 494 ): 495 # Creates a new connection 496 dcc = self.dcc_listen("raw") 497 msg_parts = map( 498 str, 499 ( 500 "SEND", 501 name, 502 irc.client.ip_quad_to_numstr(dcc.localaddress), 503 dcc.localport, 504 size, 505 ), 506 ) 507 msg = subprocess.list2cmdline(msg_parts) 508 self.connection.ctcp("DCC", identifier.nick, msg) 509 stream = Stream(identifier, fsource, name, size, stream_type) 510 self.transfers[dcc] = stream 511 512 return stream 513 514 def on_dcc_connect(self, dcc, event): 515 stream = self.transfers.get(dcc, None) 516 if stream is None: 517 log.error("DCC connect on a none registered connection") 518 return 519 log.debug("Start transfer for %s.", stream.identifier) 520 stream.accept() 521 self.send_chunk(stream, dcc) 522 523 def on_dcc_disconnect(self, dcc, event): 524 self.transfers.pop(dcc) 525 526 def on_part(self, connection, event): 527 """ 528 Handler of the part IRC Message/event. 529 530 The part message is sent to the client as a confirmation of a 531 /PART command sent by someone in the room/channel. 532 If the event.source contains the bot nickname then we need to fire 533 the :meth:`~errbot.backends.base.Backend.callback_room_left` event on the bot. 534 535 :param connection: Is an 'irc.client.ServerConnection' object 536 537 :param event: Is an 'irc.client.Event' object 538 The event.source contains the nickmask of the user that 539 leave the room 540 The event.target contains the channel name 541 """ 542 leaving_nick = event.source.nick 543 leaving_room = event.target 544 if self.bot.bot_identifier.nick == leaving_nick: 545 with self._rooms_lock: 546 self.bot.callback_room_left(self._rooms[leaving_room]) 547 log.info("Left room {}.", leaving_room) 548 549 def on_endofnames(self, connection, event): 550 """ 551 Handler of the enfofnames IRC message/event. 552 553 The endofnames message is sent to the client when the server finish 554 to send the list of names of the room ocuppants. 555 This usually happens when you join to the room. 556 So in this case, we use this event to determine that our bot is 557 finally joined to the room. 558 559 :param connection: Is an 'irc.client.ServerConnection' object 560 561 :param event: Is an 'irc.client.Event' object 562 the event.arguments[0] contains the channel name 563 """ 564 # The event.arguments[0] contains the channel name. 565 # We filter that to avoid a misfire of the event. 566 room_name = event.arguments[0] 567 with self._rooms_lock: 568 if room_name in self._recently_joined_to: 569 self._recently_joined_to.remove(room_name) 570 self.bot.callback_room_joined(self._rooms[room_name]) 571 572 def on_join(self, connection, event): 573 """ 574 Handler of the join IRC message/event. 575 Is in response of a /JOIN client message. 576 577 :param connection: Is an 'irc.client.ServerConnection' object 578 579 :param event: Is an 'irc.client.Event' object 580 the event.target contains the channel name 581 """ 582 # We can't fire the room_joined event yet, 583 # because we don't have the occupants info. 584 # We need to wait to endofnames message. 585 room_name = event.target 586 with self._rooms_lock: 587 if room_name not in self._rooms: 588 self._rooms[room_name] = IRCRoom(room_name, self.bot) 589 self._recently_joined_to.add(room_name) 590 591 def on_currenttopic(self, connection, event): 592 """ 593 When you Join a room with a topic set this event fires up to 594 with the topic information. 595 If the room that you join don't have a topic set, nothing happens. 596 Here is NOT the place to fire the :meth:`~errbot.backends.base.Backend.callback_room_topic` event for 597 that case exist on_topic. 598 599 :param connection: Is an 'irc.client.ServerConnection' object 600 601 :param event: Is an 'irc.client.Event' object 602 The event.arguments[0] contains the room name 603 The event.arguments[1] contains the topic of the room. 604 """ 605 room_name, current_topic = event.arguments 606 with self._rooms_lock: 607 self._rooms[room_name].cb_set_topic(current_topic) 608 609 def on_topic(self, connection, event): 610 """ 611 On response to the /TOPIC command if the room have a topic. 612 If the room don't have a topic the event fired is on_notopic 613 :param connection: Is an 'irc.client.ServerConnection' object 614 615 :param event: Is an 'irc.client.Event' object 616 The event.target contains the room name. 617 The event.arguments[0] contains the topic name 618 """ 619 room_name = event.target 620 current_topic = event.arguments[0] 621 with self._rooms_lock: 622 self._rooms[room_name].cb_set_topic(current_topic) 623 self.bot.callback_room_topic(self._rooms[room_name]) 624 625 def on_notopic(self, connection, event): 626 """ 627 This event fires ip when there is no topic set on a room 628 629 :param connection: Is an 'irc.client.ServerConnection' object 630 631 :param event: Is an 'irc.client.Event' object 632 The event.arguments[0] contains the room name 633 """ 634 room_name = event.arguments[0] 635 with self._rooms_lock: 636 self._rooms[room_name].cb_set_topic(None) 637 self.bot.callback_room_topic(self._rooms[room_name]) 638 639 @staticmethod 640 def send_chunk(stream, dcc): 641 data = stream.read(4096) 642 dcc.send_bytes(data) 643 stream.ack_data(len(data)) 644 645 def on_dccmsg(self, dcc, event): 646 stream = self.transfers.get(dcc, None) 647 if stream is None: 648 log.error("DCC connect on a none registered connection") 649 return 650 acked = struct.unpack("!I", event.arguments[0])[0] 651 if acked == stream.size: 652 log.info( 653 "File %s successfully transfered to %s", stream.name, stream.identifier 654 ) 655 dcc.disconnect() 656 self.transfers.pop(dcc) 657 elif acked == stream.transfered: 658 log.debug( 659 "Chunk for file %s successfully transfered to %s (%d/%d).", 660 stream.name, 661 stream.identifier, 662 stream.transfered, 663 stream.size, 664 ) 665 self.send_chunk(stream, dcc) 666 else: 667 log.debug( 668 "Partial chunk for file %s successfully transfered to %s (%d/%d), wait for more", 669 stream.name, 670 stream.identifier, 671 stream.transfered, 672 stream.size, 673 ) 674 675 def away(self, message=""): 676 """ 677 Extend the original implementation to support AWAY. 678 To set an away message, set message to something. 679 To cancel an away message, leave message at empty string. 680 """ 681 self.connection.send_raw(" ".join(["AWAY", message]).strip()) 682 683 684class IRCBackend(ErrBot): 685 aclpattern = "{nick}!{user}@{host}" 686 687 def __init__(self, config): 688 if hasattr(config, "IRC_ACL_PATTERN"): 689 IRCBackend.aclpattern = config.IRC_ACL_PATTERN 690 691 identity = config.BOT_IDENTITY 692 nickname = identity["nickname"] 693 server = identity["server"] 694 port = identity.get("port", 6667) 695 password = identity.get("password", None) 696 ssl = identity.get("ssl", False) 697 bind_address = identity.get("bind_address", None) 698 ipv6 = identity.get("ipv6", False) 699 username = identity.get("username", None) 700 nickserv_password = identity.get("nickserv_password", None) 701 702 compact = config.COMPACT_OUTPUT if hasattr(config, "COMPACT_OUTPUT") else True 703 enable_format("irc", IRC_CHRS, borders=not compact) 704 705 private_rate = getattr(config, "IRC_PRIVATE_RATE", 1) 706 channel_rate = getattr(config, "IRC_CHANNEL_RATE", 1) 707 reconnect_on_kick = getattr(config, "IRC_RECONNECT_ON_KICK", 5) 708 reconnect_on_disconnect = getattr(config, "IRC_RECONNECT_ON_DISCONNECT", 5) 709 710 self.bot_identifier = IRCPerson(nickname + "!" + nickname + "@" + server) 711 super().__init__(config) 712 self.conn = IRCConnection( 713 bot=self, 714 nickname=nickname, 715 server=server, 716 port=port, 717 ssl=ssl, 718 bind_address=bind_address, 719 ipv6=ipv6, 720 password=password, 721 username=username, 722 nickserv_password=nickserv_password, 723 private_rate=private_rate, 724 channel_rate=channel_rate, 725 reconnect_on_kick=reconnect_on_kick, 726 reconnect_on_disconnect=reconnect_on_disconnect, 727 ) 728 self.md = irc_md() 729 730 def set_message_size_limit(self, limit=510, hard_limit=510): 731 """ 732 IRC message size limit 733 """ 734 super().set_message_size_limit(limit, hard_limit) 735 736 def send_message(self, msg): 737 super().send_message(msg) 738 if msg.is_direct: 739 msg_func = self.conn.send_private_message 740 msg_to = msg.to.person 741 else: 742 msg_func = self.conn.send_public_message 743 msg_to = msg.to.room 744 745 body = self.md.convert(msg.body) 746 for line in body.split("\n"): 747 msg_func(msg_to, line) 748 749 def change_presence(self, status: str = ONLINE, message: str = "") -> None: 750 if status == ONLINE: 751 self.conn.away() # cancels the away message 752 else: 753 self.conn.away(f"[{status}] {message}") 754 755 def send_stream_request( 756 self, identifier, fsource, name=None, size=None, stream_type=None 757 ): 758 return self.conn.send_stream_request( 759 identifier, fsource, name, size, stream_type 760 ) 761 762 def build_reply(self, msg, text=None, private=False, threaded=False): 763 response = self.build_message(text) 764 if msg.is_group: 765 if private: 766 response.frm = self.bot_identifier 767 response.to = IRCPerson(str(msg.frm)) 768 else: 769 response.frm = IRCRoomOccupant(str(self.bot_identifier), msg.frm.room) 770 response.to = msg.frm.room 771 else: 772 response.frm = self.bot_identifier 773 response.to = msg.frm 774 return response 775 776 def serve_forever(self): 777 try: 778 self.conn.start() 779 except KeyboardInterrupt: 780 log.info("Interrupt received, shutting down") 781 finally: 782 self.conn.disconnect("Shutting down") 783 log.debug("Trigger disconnect callback") 784 self.disconnect_callback() 785 log.debug("Trigger shutdown") 786 self.shutdown() 787 788 def connect(self): 789 return self.conn 790 791 def build_message(self, text): 792 text = text.replace( 793 "", "*" 794 ) # there is a weird chr IRC is sending that we need to filter out 795 return super().build_message(text) 796 797 def build_identifier(self, txtrep): 798 log.debug("Build identifier from %s.", txtrep) 799 # A textual representation starting with # means that we are talking 800 # about an IRC channel -- IRCRoom in internal err-speak. 801 if txtrep.startswith("#"): 802 return IRCRoom(txtrep, self) 803 804 # Occupants are represented as 2 lines, one is the IRC mask and the second is the Room. 805 if "\n" in txtrep: 806 m, r = txtrep.split("\n") 807 return IRCRoomOccupant(m, IRCRoom(r, self)) 808 return IRCPerson(txtrep) 809 810 def shutdown(self): 811 super().shutdown() 812 813 def query_room(self, room): 814 """ 815 Query a room for information. 816 817 :param room: 818 The channel name to query for. 819 :returns: 820 An instance of :class:`~IRCMUCRoom`. 821 """ 822 with self.conn._rooms_lock: 823 if room not in self.conn._rooms: 824 self.conn._rooms[room] = IRCRoom(room, self) 825 return self.conn._rooms[room] 826 827 @property 828 def mode(self): 829 return "irc" 830 831 def rooms(self): 832 """ 833 Return a list of rooms the bot is currently in. 834 835 :returns: 836 A list of :class:`~IRCMUCRoom` instances. 837 """ 838 with self.conn._rooms_lock: 839 return self.conn._rooms.values() 840 841 def prefix_groupchat_reply(self, message, identifier): 842 super().prefix_groupchat_reply(message, identifier) 843 message.body = f"{identifier.nick}: {message.body}" 844