add_result = db_helper.check_add_game(white, black, result, sender, comment) if add_result.startswith("fehler"): myroom.send_text(add_result) else: myroom.send_text(msg_result + "\n" + add_result) myroom.add_listener(on_message) client.start_listener_thread() myroom.send_text("Naelob returns!") try: get_input = raw_input except NameError: get_input = input #====== MAIN LOOP: ========== while True: msg = get_input() #TEST #time.sleep(0.3) if msg == "/quit": break client.stop_listener_thread() myroom.send_text("Naelob verabschiedet sich und geht offline.") client.logout()
class SparseManager(object): def __init__(self): self.active_room = None self.active_listener_id = None self.active_start = None def login(self, url, username, password): self.client = MatrixClient(url) # New user # token = client.register_with_password(username="******", password="******") # Existing user create_path(FILENAME) token = self.client.login_with_password(username=username, password=password) print("Logged in with token: {token}".format(token=token)) data = {'token': token, 'user_id': self.client.user_id, 'url': url} with open(FILENAME, 'w') as f: json.dump(data, f, ensure_ascii=False) return data def login_with_token(self): with open(FILENAME, 'r') as f: data = json.load(f) self.client = MatrixClient(data["url"], user_id=data["user_id"], token=data["token"]) return data def get_rooms(self): self.rooms = self.client.get_rooms() ids = [{ "name": x.display_name, "topic": x.topic, "room_id": x.room_id, "has_unread_messages": x.has_unread_messages } for x in self.rooms.values()] return ids def enter_room(self, room_id): import threading print("Threads running: %s self: %s" % (len(threading.enumerate()), id(self))) if self.active_room and self.active_room.room_id == room_id: return if self.active_room: self.deactivate_room() # self.active_room = self.client.join_room(room_id) self.active_room = self.rooms[room_id] self.active_listener_id = self.active_room.add_listener( self.on_message) self.active_start = self.active_room.get_room_messages(limit=20) print("Message end: %s" % self.active_start) self.client.start_listener_thread() def get_next_messages(self): # TODO prepend at beginning self.active_start = self.active_room.get_room_messages( limit=20, start=self.active_start) return self.active_start def deactivate_room(self): self.active_room.remove_listener(self.active_listener_id) self.client.stop_listener_thread(blocking=False) self.active_room = None self.active_listener_id = None self.active_start = None def on_message(self, room, event): if event['type'] in ("m.room.message", "m.room.encrypted"): to_send = {} if "msgtype" in event["content"] and event["content"][ "msgtype"] == "m.image": to_send["image_url"] = self.client.api.get_download_url( event["content"]["url"]) to_send["msgtype"] = "image" if event["sender"] == self.client.user_id: user_id = self.client.user_id else: user_id = event["user_id"] if "body" in event["content"]: to_send["body"] = event["content"]["body"] elif "ciphertext" in event["content"]: to_send["body"] = "... encrypted ..." else: to_send["body"] = "... no message ..." if "redacted_because" in event: to_send["body"] = "... redacted ..." user = room._members.get(user_id) avatar_url = None displayname = None if user and user.avatar_url: avatar_url = user.avatar_url if user and user.displayname: displayname = user.displayname # XXX to expensive take up to 2 sec member and message # elif user and not user.avatar_url: # avatar_url = user.get_avatar_url() to_send["origin_server_ts"] = event["origin_server_ts"] to_send["time"] = datetime.datetime.fromtimestamp( event["origin_server_ts"] / 1000, datetime.timezone.utc) to_send["avatar_url"] = avatar_url to_send["displayname"] = displayname if displayname else event[ "sender"] pyotherside.send('r.room.message', {"event": to_send}) else: pass # print(event["type"]) # print(event) def send_text(self, text): self.active_room.send_text(text)
class MatrixProtocol(Protocol): # List of occupants in each room # Used to avoid having to re-request the list of members each time room_occupants = {} # Keep track of when we joined rooms this session # Used to filter out historical messages after accepting room invites join_timestamps = {} # called on bot init; the following are already created by __init__: # self.bot = SibylBot instance # self.log = the logger you should use def setup(self): self.rooms = {} self.bot.add_var("credentials",persist=True) # Incoming message queue - messageHandler puts messages in here and # process() looks here periodically to send them to sibyl self.msg_queue = Queue() # Create a client in setup() because we might use self.client before # connect() is called homeserver = self.opt('matrix.server') self.client = MatrixClient(homeserver) # @raise (ConnectFailure) if can't connect to server # @raise (AuthFailure) if failed to authenticate to server def connect(self): homeserver = self.opt('matrix.server') user = self.opt('matrix.username') pw = self.opt('matrix.password') self.log.debug("Connecting to %s" % homeserver) try: self.log.debug("Logging in as %s" % user) # Log in with the existing access token if we already have a token if(self.bot.credentials and self.bot.credentials[0] == user): self.client = MatrixClient(homeserver, user_id=user, token=self.bot.credentials[1]) # Otherwise, log in with the configured username and password else: token = self.client.login_with_password(user,pw) self.bot.credentials = (user, token) self.rooms = self.client.get_rooms() self.log.debug("Already in rooms: %s" % self.rooms) # Connect to Sibyl's message callback self.client.add_listener(self.messageHandler) self.client.add_invite_listener(self.inviteHandler) self.log.debug("Starting Matrix listener thread") self.client.start_listener_thread(exception_handler=self._matrix_exception_handler) except MatrixRequestError as e: if(e.code in [401, 403]): self.log.debug("Credentials incorrect! Maybe your access token is outdated?") raise self.AuthFailure else: if(self.opt('matrix.debug')): tb = traceback.format_exc() self.log.debug(tb) self.log.debug("Failed to connect to homeserver!") raise self.ConnectFailure except MatrixHttpLibError as e: self.log.error("Failed to connect to homeserver!") self.log.debug("Received error:" + str(e)) raise self.ConnectFailure def _matrix_exception_handler(self, e): self.msg_queue.put(e) # receive/process messages and call bot._cb_message() # must ignore msgs from myself and from users not in any of our rooms # @call bot._cb_message(Message) upon receiving a valid status or message # @raise (PingTimeout) if implemented # @raise (ConnectFailure) if disconnected # @raise (ServerShutdown) if server shutdown def process(self): while(not self.msg_queue.empty()): next = self.msg_queue.get() if(isinstance(next, Message)): self.log.debug("Placing message into queue: " + next.get_text()) self.bot._cb_message(next) elif(isinstance(next, MatrixHttpLibError)): self.log.debug("Received error from Matrix SDK, stopping listener thread: " + str(next)) self.client.stop_listener_thread() raise self.ConnectFailure("Connection error returned by requests library: " + str(next)) def messageHandler(self, msg): if(self.opt('matrix.debug')): self.log.debug(str(msg)) try: # Create a new Message to send to Sibyl u = self.new_user(msg['sender'], Message.GROUP) r = self.new_room(msg['room_id']) if(r in self.join_timestamps and datetime.datetime.fromtimestamp(msg['origin_server_ts']/1000, pytz.utc) < self.join_timestamps[r]): self.log.info('Message received in {} from before room join, ignoring'.format(msg['room_id'])) return None if('msgtype' in msg['content']): msgtype = msg['content']['msgtype'] if(msgtype == 'm.text'): m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP) self.log.debug('Handling m.text: ' + msg['content']['body']) self.msg_queue.put(m) elif(msgtype == 'm.emote'): m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP, emote=True) self.log.debug('Handling m.emote: ' + msg['content']['body']) self.msg_queue.put(m) elif(msgtype == 'm.image' or msgtype == 'm.audio' or msgtype == 'm.file' or msgtype == 'm.video'): media_url = urlparse(msg['content']['url']) http_url = self.client.api.base_url + "/_matrix/media/r0/download/{0}{1}".format(media_url.netloc, media_url.path) if(msgtype == 'm.image'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an image'), http_url) elif(msgtype == 'm.audio'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an audio file'), http_url) elif(msgtype == 'm.video'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a video file'), http_url) elif(msgtype == 'm.file'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a file'), http_url) m = Message(u, body, room=r, typ=Message.GROUP) self.log.debug("Handling " + msgtype + ": " + body) self.msg_queue.put(m) elif(msgtype == 'm.location'): body = "{0} sent a location: {1}".format(msg['sender'], msg['content']['geo_uri']) m = Message(u, body, room=r, typ=Message.GROUP) self.log.debug('Handling m.location: ' + body) self.msg_queue.put(m) else: self.log.debug('Not handling message, unknown msgtype') elif('membership' in msg): if(msg['membership'] == 'join'): self.room_occupants[r].add(self.new_user(msg['state_key'], Message.GROUP)) elif(msg['membership'] == 'leave'): self.room_occupants[r].remove(self.new_user(msg['state_key'], Message.GROUP)) except KeyError as e: self.log.debug("Incoming message did not have all required fields: " + e.message) def inviteHandler(self, room_id, state): join_on_invite = self.opt('matrix.join_on_invite') invite_events = [x for x in state['events'] if x['type'] == 'm.room.member' and x['state_key'] == str(self.get_user()) and x['content']['membership'] == 'invite'] if(len(invite_events) != 1): raise KeyError("Something's up, found more than one invite state event for " + room_id) inviter = invite_events[0]['sender'] inviter_domain = inviter.split(':')[1] my_domain = str(self.get_user()).split(':')[1] if(join_on_invite == 'accept' or (join_on_invite == 'domain' and inviter_domain == my_domain)): self.log.debug('Joining {} on invite from {}'.format(room_id, inviter)) self.join_room(MatrixRoom(self, room_id)) elif(join_on_invite == 'domain' and inviter_domain != my_domain): self.log.debug("Received invite for {} but inviter {} is on a different homeserver").format(room_id, inviter) else: self.log.debug("Received invite for {} from {} but join_on_invite is disabled".format(room_id, inviter)) # called when the bot is exiting for whatever reason # NOTE: sibylbot will already call part_room() on every room in get_rooms() def shutdown(self): pass # send a message to a user # @param mess (Message) message to be sent # @raise (ConnectFailure) if failed to send message # Check: get_emote() def send(self,mess): (text,to) = (mess.get_text(),mess.get_to()) try: if(mess.get_emote()): to.room.send_emote(text) else: to.room.send_text(text) except MatrixError as e: raise self.ConnectFailure # send a message with text to every user in a room # optionally note that the broadcast was requested by a specific User # @param mess (Message) the message to broadcast # @return (str,unicode) the text that was actually sent # Check: get_user(), get_users() def broadcast(self,mess): """send a message to every user in a room""" (text,room,frm) = (mess.get_text(),mess.get_to(),mess.get_user()) users = self.get_occupants(room)+(mess.get_users() or []) # Matrix has no built-in broadcast, so we'll just highlight everyone s = 'all: %s --- ' % text if frm: self.log.debug('Broadcast message from: ' + str(frm)) s += frm.get_name()+' --- ' me = self.get_user() names = [u.get_name() for u in users if (u!=me and (not frm or u!=frm))] s += ', '.join(set(names)) self.send(Message(self.get_user(),s,to=room)) return s # join the specified room using the specified nick and password # @param room (Room) the room to join # @call bot._cb_join_room_success(room) on successful join # @call bot._cb_join_room_failure(room,error) on failed join def join_room(self,room): try: res = self.client.join_room(room.room.room_id) self.bot._cb_join_room_success(room) self.join_timestamps[room] = datetime.datetime.now(pytz.utc) except MatrixError as e: self.bot._cb_join_room_failure(room, e.message) # part the specified room # @param room (Room) the room to leave def part_room(self,room): raise NotImplementedError # helper function for get_rooms() for protocol-specific flags # only needs to handle: FLAG_PARTED, FLAG_PENDING, FLAG_IN, FLAG_ALL # @param flag (int) one of Room.FLAG_* enums # @return (list of Room) rooms matching the flag def _get_rooms(self,flag): mxrooms = self.client.get_rooms() return [self.new_room(mxroom) for mxroom in mxrooms] # @param room (Room) the room to query # @return (list of User) the Users in the specified room def get_occupants(self,room): if(room in self.room_occupants): return list(self.room_occupants[room]) else: try: memberdict = room.room.get_joined_members() users = [ self.new_user(x) for x in memberdict ] self.room_occupants[room] = set(users) return users except MatrixError as e: raise self.ConnectFailure # @param room (Room) the room to query # @return (str) the nick name we are using in the specified room def get_nick(self,room): return self.get_user().get_name() # TODO: per-room nicknames # @param room (Room) the room to query # @param nick (str) the nick to examine # @return (User) the "real" User behind the specified nick/room def get_real(self,room,nick): raise NotImplementedError # @return (User) our username def get_user(self): return MatrixUser(self,self.opt('matrix.username'),Message.GROUP) # @param user (str) a user id to parse # @param typ (int) either Message.GROUP or Message.PRIVATE # @param real (User) [self] the "real" user behind this user # @return (User) a new instance of this protocol's User subclass def new_user(self,user,typ=None,real=None): return MatrixUser(self,user,typ,real) # @param name (object) the identifier for this Room # @param nick (str) [None] the nick name to use in this Room # @param pword (str) [None] the password for joining this Room # @return (Room) a new instance of this protocol's Room subclass def new_room(self,room_id_or_alias,nick=None,pword=None): return MatrixRoom(self,room_id_or_alias,nick,pword)
class MatrixBot: def __init__(self, username, password, server, allowed_rooms=None): self.username = username self._username_re = re.compile('@{}'.format(re.escape(username)), re.I) self.server = server self.handlers = [] self.rooms = {} self.client = MatrixClient(server) try: self.client.login_with_password(username, password) except MatrixRequestError as error: if error.code == 403: logger.error('Username and/or password mismatch') raise self._join_rooms(allowed_rooms) if not self.rooms: logger.info('No rooms given, listening for invitations') self.client.add_invite_listener(self.handle_invite) for room_id, room in self.client.rooms.items(): room.add_listener(self.handle_message) self._add_room(room_id, room) def _add_room(self, room_id, room): logger.debug('Adding room (ID: %s): %s', room_id, room) self.rooms[room_id] = room def _join_rooms(self, room_ids): for room_id in room_ids: room = self.client.join_room(room_id) room.add_listener(self.handle_message) self._add_room(room_id, room) def send(self, msg, room_ids=None): rooms = room_ids or self.rooms.keys() for room_id in rooms: room = self.rooms.get(room_id) if room: room.send_text(msg) def handle_message(self, room, event): if self._username_re.match(event['sender']): # Don't handle own messages return for handler in self.handlers: assert isinstance(handler, BaseHandler) if handler.should_run(room, event): try: handler(room, event) except Exception: logger.exception('Error while calling handler function') def handle_invite(self, room_id, state): logger.info('Invitation received to: %s', room_id) logger.info('Trying to join now...') room = self.client.join_room(room_id) room.add_listener(self.handle_message) self.rooms[room_id] = room def start(self): """Starts listening for messages in a new thread. If the calling script has nothing more to do, it can simply do: >>> bot = MatrixBot(username, password, server, allowed_rooms) >>> bot_thread = bot.start() >>> bot_thread.join() This will run forever. """ self.client.start_listener_thread() return self.client.sync_thread def stop(self): self.client.stop_listener_thread()
class MatrixProtocol(Protocol): # List of occupants in each room # Used to avoid having to re-request the list of members each time room_occupants = {} # Keep track of when we joined rooms this session # Used to filter out historical messages after accepting room invites join_timestamps = {} # called on bot init; the following are already created by __init__: # self.bot = SibylBot instance # self.log = the logger you should use def setup(self): self.rooms = {} self.bot.add_var("credentials",persist=True) # Incoming message queue - messageHandler puts messages in here and # process() looks here periodically to send them to sibyl self.msg_queue = Queue() # Create a client in setup() because we might use self.client before # connect() is called homeserver = self.opt('matrix.server') self.client = MatrixClient(homeserver) # @raise (ConnectFailure) if can't connect to server # @raise (AuthFailure) if failed to authenticate to server def connect(self): homeserver = self.opt('matrix.server') user = self.opt('matrix.username') pw = self.opt('matrix.password') self.log.debug("Connecting to %s" % homeserver) try: self.log.debug("Logging in as %s" % user) # Log in with the existing access token if we already have a token if(self.bot.credentials and self.bot.credentials[0] == user): self.client = MatrixClient(homeserver, user_id=user, token=self.bot.credentials[1]) # Otherwise, log in with the configured username and password else: token = self.client.login_with_password(user,pw) self.bot.credentials = (user, token) self.rooms = self.client.get_rooms() self.log.debug("Already in rooms: %s" % self.rooms) # Connect to Sibyl's message callback self.client.add_listener(self.messageHandler) self.client.add_invite_listener(self.inviteHandler) self.log.debug("Starting Matrix listener thread") self.client.start_listener_thread(exception_handler=self._matrix_exception_handler) except MatrixRequestError as e: if(e.code in [401, 403]): self.log.debug("Credentials incorrect! Maybe your access token is outdated?") raise self.AuthFailure else: if(self.opt('matrix.debug')): tb = traceback.format_exc() self.log.debug(tb) self.log.debug("Failed to connect to homeserver!") raise self.ConnectFailure except MatrixHttpLibError as e: self.log.error("Failed to connect to homeserver!") self.log.debug("Received error:" + str(e)) raise self.ConnectFailure def _matrix_exception_handler(self, e): self.msg_queue.put(e) # receive/process messages and call bot._cb_message() # must ignore msgs from myself and from users not in any of our rooms # @call bot._cb_message(Message) upon receiving a valid status or message # @raise (PingTimeout) if implemented # @raise (ConnectFailure) if disconnected # @raise (ServerShutdown) if server shutdown def process(self): while(not self.msg_queue.empty()): next = self.msg_queue.get() if(isinstance(next, Message)): self.log.debug("Placing message into queue: " + next.get_text()) self.bot._cb_message(next) elif(isinstance(next, MatrixHttpLibError)): self.log.debug("Received error from Matrix SDK, stopping listener thread: " + str(next)) self.client.stop_listener_thread() raise self.ConnectFailure("Connection error returned by requests library: " + str(next)) def messageHandler(self, msg): if(self.opt('matrix.debug')): self.log.debug(str(msg)) try: # Create a new Message to send to Sibyl u = self.new_user(msg['sender'], Message.GROUP) r = self.new_room(msg['room_id']) if(r in self.join_timestamps and datetime.datetime.fromtimestamp(msg['origin_server_ts']/1000, pytz.utc) < self.join_timestamps[r]): self.log.info('Message received in {} from before room join, ignoring'.format(msg['room_id'])) return None if('msgtype' in msg['content']): msgtype = msg['content']['msgtype'] if(msgtype == 'm.text'): m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP) self.log.debug('Handling m.text: ' + msg['content']['body']) self.msg_queue.put(m) elif(msgtype == 'm.emote'): m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP, emote=True) self.log.debug('Handling m.emote: ' + msg['content']['body']) self.msg_queue.put(m) elif(msgtype == 'm.image' or msgtype == 'm.audio' or msgtype == 'm.file' or msgtype == 'm.video'): media_url = urlparse(msg['content']['url']) http_url = self.client.api.base_url + "/_matrix/media/r0/download/{0}{1}".format(media_url.netloc, media_url.path) if(msgtype == 'm.image'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an image'), http_url) elif(msgtype == 'm.audio'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an audio file'), http_url) elif(msgtype == 'm.video'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a video file'), http_url) elif(msgtype == 'm.file'): body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a file'), http_url) m = Message(u, body, room=r, typ=Message.GROUP) self.log.debug("Handling " + msgtype + ": " + body) self.msg_queue.put(m) elif(msgtype == 'm.location'): body = "{0} sent a location: {1}".format(msg['sender'], msg['content']['geo_uri']) m = Message(u, body, room=r, typ=Message.GROUP) self.log.debug('Handling m.location: ' + body) self.msg_queue.put(m) else: self.log.debug('Not handling message, unknown msgtype') elif('membership' in msg): if(msg['membership'] == 'join'): self.room_occupants[r].add(self.new_user(msg['state_key'], Message.GROUP)) elif(msg['membership'] == 'leave'): self.room_occupants[r].remove(self.new_user(msg['state_key'], Message.GROUP)) except KeyError as e: self.log.debug("Incoming message did not have all required fields: " + e.message) def inviteHandler(self, room_id, state): join_on_invite = self.opt('matrix.join_on_invite') invite_events = [x for x in state['events'] if x['type'] == 'm.room.member' and x['state_key'] == str(self.get_user()) and x['content']['membership'] == 'invite'] if(len(invite_events) != 1): raise KeyError("Something's up, found more than one invite state event for " + room_id) inviter = invite_events[0]['sender'] inviter_domain = inviter.split(':')[1] my_domain = str(self.get_user()).split(':')[1] if(join_on_invite == 'accept' or (join_on_invite == 'domain' and inviter_domain == my_domain)): self.log.debug('Joining {} on invite from {}'.format(room_id, inviter)) self.join_room(MatrixRoom(self, room_id)) elif(join_on_invite == 'domain' and inviter_domain != my_domain): self.log.debug("Received invite for {} but inviter {} is on a different homeserver").format(room_id, inviter) else: self.log.debug("Received invite for {} from {} but join_on_invite is disabled".format(room_id, inviter)) # called when the bot is exiting for whatever reason # NOTE: sibylbot will already call part_room() on every room in get_rooms() def shutdown(self): pass # send a message to a user # @param mess (Message) message to be sent # @raise (ConnectFailure) if failed to send message # Check: get_emote() def send(self,mess): (text,to) = (mess.get_text(),mess.get_to()) if(mess.get_emote()): to.room.send_emote(text) else: to.room.send_text(text) # send a message with text to every user in a room # optionally note that the broadcast was requested by a specific User # @param mess (Message) the message to broadcast # @return (str,unicode) the text that was actually sent # Check: get_user(), get_users() def broadcast(self,mess): """send a message to every user in a room""" (text,room,frm) = (mess.get_text(),mess.get_to(),mess.get_user()) users = self.get_occupants(room)+(mess.get_users() or []) # Matrix has no built-in broadcast, so we'll just highlight everyone s = 'all: %s --- ' % text if frm: self.log.debug('Broadcast message from: ' + str(frm)) s += frm.get_name()+' --- ' me = self.get_user() names = [u.get_name() for u in users if (u!=me and (not frm or u!=frm))] s += ', '.join(set(names)) self.send(Message(self.get_user(),s,to=room)) return s # join the specified room using the specified nick and password # @param room (Room) the room to join # @call bot._cb_join_room_success(room) on successful join # @call bot._cb_join_room_failure(room,error) on failed join def join_room(self,room): try: res = self.client.join_room(room.room.room_id) self.bot._cb_join_room_success(room) self.join_timestamps[room] = datetime.datetime.now(pytz.utc) except MatrixRequestError as e: self.bot._cb_join_room_failure(room, e.message) # part the specified room # @param room (Room) the room to leave def part_room(self,room): raise NotImplementedError # helper function for get_rooms() for protocol-specific flags # only needs to handle: FLAG_PARTED, FLAG_PENDING, FLAG_IN, FLAG_ALL # @param flag (int) one of Room.FLAG_* enums # @return (list of Room) rooms matching the flag def _get_rooms(self,flag): mxrooms = self.client.get_rooms() return [self.new_room(mxroom) for mxroom in mxrooms] # @param room (Room) the room to query # @return (list of User) the Users in the specified room def get_occupants(self,room): if(room in self.room_occupants): return list(self.room_occupants[room]) else: memberdict = room.room.get_joined_members() users = [ self.new_user(x) for x in memberdict ] self.room_occupants[room] = set(users) return users # @param room (Room) the room to query # @return (str) the nick name we are using in the specified room def get_nick(self,room): return self.get_user().get_name() # TODO: per-room nicknames # @param room (Room) the room to query # @param nick (str) the nick to examine # @return (User) the "real" User behind the specified nick/room def get_real(self,room,nick): raise NotImplementedError # @return (User) our username def get_user(self): return MatrixUser(self,self.opt('matrix.username'),Message.GROUP) # @param user (str) a user id to parse # @param typ (int) either Message.GROUP or Message.PRIVATE # @param real (User) [self] the "real" user behind this user # @return (User) a new instance of this protocol's User subclass def new_user(self,user,typ=None,real=None): return MatrixUser(self,user,typ,real) # @param name (object) the identifier for this Room # @param nick (str) [None] the nick name to use in this Room # @param pword (str) [None] the password for joining this Room # @return (Room) a new instance of this protocol's Room subclass def new_room(self,room_id_or_alias,nick=None,pword=None): return MatrixRoom(self,room_id_or_alias,nick,pword)
class BridgeBot: xmpp = None # type: ClientXMPP matrix = None # type: MatrixClient topic_room_id_map = None # type: Dict[str, str] special_rooms = None # type: Dict[str, MatrixRoom] special_room_names = None # type: Dict[str, str] groupchat_flag = None # type: str groupchat_jids = None # type: List[str] users_to_invite = None # type: List[str] matrix_room_topics = None # type: Dict[str, str] matrix_server = None # type: Dict[str, str] matrix_login = None # type: Dict[str, str] xmpp_server = None # type: Tuple[str, int] xmpp_login = None # type: Dict[str, str] xmpp_roster_options = None # type: Dict[str, bool] xmpp_groupchat_nick = None # type: str default_actions = None # type: Dict[str, bool] jid_actions = None # type: Dict[str, Dict[str, bool]] groupchat_mute_own_nick = True # type: bool groupchat_send_messages_to_all_chat = True # type: bool inbound_xmpp = None # type: Queue exception = None # type: Exception or None @property def bot_id(self) -> str: return self.matrix_login['username'] def __init__(self, config_file: str=CONFIG_FILE): self.groupchat_jids = [] self.topic_room_id_map = {} self.special_rooms = { 'control': None, 'all_chat': None, } self.special_room_names = { 'control': 'XMPP Control Room', 'all_chat': 'XMPP All Chat', } self.xmpp_roster_options = {} self.inbound_xmpp = Queue() self.load_config(config_file) self.matrix = MatrixClient(**self.matrix_server) self.xmpp = ClientXMPP(self.inbound_xmpp, **self.xmpp_login, **self.xmpp_roster_options) self.matrix.login_with_password(**self.matrix_login) # Recover existing matrix rooms for room in list(self.matrix.get_rooms().values()): room.update_room_topic() topic = room.topic if topic in self.special_rooms.keys(): logger.debug('Recovering special room: ' + topic) self.special_rooms[topic] = room elif topic is None: room.leave() elif topic.startswith(self.groupchat_flag): room_jid = topic[len(self.groupchat_flag):] self.groupchat_jids.append(room_jid) elif not self.jid_actions.get(topic, self.default_actions)['send_messages_to_jid_rooms']: logger.info('Room ' + topic + ' is not needed due to send_messages_to_jid_rooms setting, leaving!') room.leave() # Prepare matrix special rooms and their listeners for topic, room in self.special_rooms.items(): if room is None: room = self.matrix.create_room() self.setup_special_room(room, topic) self.special_rooms['control'].add_listener(self.matrix_control_message, 'm.room.message') self.special_rooms['all_chat'].add_listener(self.matrix_all_chat_message, 'm.room.message') # Invite users to special rooms for room in self.special_rooms.values(): for user_id in self.users_to_invite: room.invite_user(user_id) # Connect to XMPP and start processing XMPP events self.xmpp.connect(self.xmpp_server) self.xmpp.process(block=False) # Rejoin group chats logger.debug('Rejoining group chats') for room_jid in self.groupchat_jids: self.xmpp.plugin['xep_0045'].joinMUC(room_jid, self.xmpp_groupchat_nick) # Listen for Matrix events def exception_handler(e: Exception): self.exception = e self.matrix.start_listener_thread(exception_handler=exception_handler) logger.debug('Done with bot init') def shutdown(self): self.matrix.stop_listener_thread() self.xmpp.disconnect() def handle_inbound_xmpp(self): while self.exception is None: event = self.inbound_xmpp.get() if isinstance(event, sleekxmpp.Presence): handler = { 'available': self.xmpp_presence_available, 'unavailable': self.xmpp_presence_unavailable, }.get(event.get_type(), self.xmpp_unrecognized_event) elif isinstance(event, sleekxmpp.Message): handler = { 'normal': self.xmpp_message, 'chat': self.xmpp_message, 'groupchat': self.xmpp_groupchat_message, }.get(event.get_type(), self.xmpp_unrecognized_event) elif isinstance(event, sleekxmpp.Iq) and event.get_query() == 'jabber:iq:roster': handler = self.xmpp_roster_update else: handler = self.xmpp_unrecognized_event handler(event) raise self.exception def load_config(self, path: str): with open(path, 'r') as conf_file: config = yaml.safe_load(conf_file) self.users_to_invite = config['matrix']['users_to_invite'] self.matrix_room_topics = config['matrix']['room_topics'] self.groupchat_flag = config['matrix']['groupchat_flag'] self.matrix_server = config['matrix']['server'] self.matrix_login = config['matrix']['login'] self.xmpp_server = (config['xmpp']['server']['host'], config['xmpp']['server']['port']) self.xmpp_login = config['xmpp']['login'] self.xmpp_groupchat_nick = config['xmpp']['groupchat_nick'] self.default_actions = {k: config[k] for k in ('send_messages_to_all_chat', 'send_messages_to_jid_rooms', 'send_presences_to_control')} self.jid_actions = {} for group in config['jid_groups']: group_data = group.copy() group_jids = group_data.pop('jids') for jid in group['jids']: self.jid_actions.setdefault(jid, self.default_actions.copy()) self.jid_actions[jid].update(group_data) self.groupchat_mute_own_nick = config['groupchat_mute_own_nick'] self.groupchat_send_messages_to_all_chat = config['groupchat_send_messages_to_all_chat'] self.xmpp_roster_options = config['xmpp']['roster_options'] def get_room_for_topic(self, jid: str) -> MatrixRoom: """ Return the room corresponding to the given XMPP JID :param jid: bare XMPP JID, should not include the resource :return: Matrix room object for chatting with that JID """ room_id = self.topic_room_id_map[jid] return self.matrix.get_rooms()[room_id] def get_unmapped_rooms(self) -> List[MatrixRoom]: """ Returns a list of all Matrix rooms which are not a special room (e.g., the control room) and do not have a corresponding entry in the topic -> room map. :return: List of unmapped, non-special Matrix room objects. """ special_room_ids = [r.room_id for r in self.special_rooms.values()] valid_room_ids = [v for v in self.topic_room_id_map.values()] + special_room_ids unmapped_rooms = [room for room_id, room in self.matrix.get_rooms().items() if room_id not in valid_room_ids] return unmapped_rooms def get_empty_rooms(self) -> List[MatrixRoom]: """ Returns a list of all Matrix rooms which are occupied by only one user (the bot itself). :return: List of Matrix rooms occupied by only the bot. """ empty_rooms = [room for room in self.matrix.get_rooms().values() if len(room.get_joined_members()) < 2] return empty_rooms def setup_special_room(self, room, topic: str): """ Sets up a Matrix room with the requested topic and adds it to the self.special_rooms map. If a special room with that topic already exists, it is replaced in the special_rooms map by the new room. :param room: Room to set up :param topic: Topic for the room """ room.set_room_topic(topic) room.set_room_name(self.special_room_names[topic]) self.special_rooms[topic] = room logger.debug('Set up special room with topic {} and id'.format( str(room.topic), room.room_id)) def create_mapped_room(self, topic: str, name: str=None) -> MatrixRoom or None: """ Create a new room and add it to self.topic_room_id_map. :param topic: Topic for the new room :param name: (Optional) Name for the new room :return: Room which was created """ if topic in self.groupchat_jids: logger.debug('Topic {} is a groupchat without its flag, ignoring'.format(topic)) return None elif topic in self.topic_room_id_map.keys(): room_id = self.topic_room_id_map[topic] room = self.matrix.get_rooms()[room_id] logger.debug('Room with topic {} already exists!'.format(topic)) else: room = self.matrix.create_room() room.set_room_topic(topic) self.topic_room_id_map[topic] = room.room_id logger.info('Created mapped room with topic {} and id {}'.format(topic, str(room.room_id))) room.add_listener(self.matrix_message, 'm.room.message') if room.name != name: if name != "": room.set_room_name(name) room.set_user_profile(displayname=name) else: room.set_room_name(topic.split('@')[0]) return room def leave_mapped_room(self, topic: str) -> bool: """ Leave an existing, mapped room and remove it from self.topic_room_id_map. :param topic: Topic for room to leave :retrun: True if the room was left, False if the room was not found. """ if topic in self.groupchat_jids: logger.debug('Topic {} is a groupchat without its flag, ignoring'.format(topic)) return False if topic not in self.topic_room_id_map.keys(): err_msg = 'Room with topic {} isn\'t mapped or doesn\'t exist'.format(topic) logger.warning(err_msg) return False if topic.startswith(self.groupchat_flag): # Leave the groupchat room_jid = topic[len(self.groupchat_flag):] if room_jid in self.groupchat_jids: self.groupchat_jids.remove(room_jid) logger.info('XMPP MUC leave: {}'.format(room_jid)) self.xmpp.plugin['xep_0045'].leaveMUC(room_jid, self.xmpp_groupchat_nick) room = self.get_room_for_topic(topic) del self.topic_room_id_map[topic] room.leave() logger.info('Left mapped room with topic {}'.format(topic)) return True def map_rooms_by_topic(self): """ Add unmapped rooms to self.topic_room_id_map, and listen to messages from those rooms. Rooms whose topics are empty or do not contain an '@' symbol are assumed to be special rooms, and will not be mapped. """ unmapped_rooms = self.get_unmapped_rooms() for room in unmapped_rooms: room.update_room_topic() logger.debug('Unmapped room {} ({}) [{}]'.format(room.room_id, room.name, room.topic)) if room.topic is None or '@' not in room.topic: logger.debug('Leaving it as-is (special room, topic does not contain @)') else: self.topic_room_id_map[room.topic] = room.room_id room.add_listener(self.matrix_message, 'm.room.message') def matrix_control_message(self, room: MatrixRoom, event: Dict): """ Handle a message sent to the control room. Does nothing unless a valid command is received: refresh Probes the presence of all XMPP contacts, and updates the roster. purge Leaves any ((un-mapped and non-special) or empty) Matrix rooms. joinmuc [email protected] Joins a muc leavemuc [email protected] Leaves a muc :param room: Matrix room object representing the control room :param event: The Matrix event that was received. Assumed to be an m.room.message . """ # Always ignore our own messages if event['sender'] == self.bot_id: return logger.debug('matrix_control_message: {} {}'.format(room.room_id, str(event))) if event['content']['msgtype'] == 'm.text': message_body = event['content']['body'] logger.info('Matrix received control message: ' + message_body) message_parts = message_body.split() if len(message_parts) < 1: logger.warning('Received empty control message, ignoring') return if message_parts[0] == 'refresh': for jid in self.topic_room_id_map.keys(): self.xmpp.send_presence(pto=jid, ptype='probe') self.xmpp.send_presence() self.xmpp.get_roster() elif message_parts[0] == 'purge': self.special_rooms['control'].send_text('Purging unused rooms') # Leave from unwanted rooms for room in self.get_unmapped_rooms() + self.get_empty_rooms(): logger.info('Leaving room {r.room_id} ({r.name}) [{r.topic}]'.format(r=room)) if room.topic in self.topic_room_id_map.keys(): self.leave_mapped_room(room.topic) else: room.leave() elif message_parts[0] == 'joinmuc': if len(message_parts) < 2: logger.warning('joinmuc command didn\'t specify a room, ignoring') return room_jid = message_parts[1] logger.info('XMPP MUC join: {}'.format(room_jid)) self.create_groupchat_room(room_jid) self.xmpp.plugin['xep_0045'].joinMUC(room_jid, self.xmpp_groupchat_nick) elif message_parts[0] == 'leavemuc': if len(message_parts) < 2: logger.warning('leavemuc command didn\'t specify a room, ignoring') return room_jid = message_parts[1] room_topic = self.groupchat_flag + room_jid success = self.leave_mapped_room(room_topic) if not success: msg = 'Groupchat {} isn\'t mapped or doesn\'t exist'.format(room_jid) else: msg = 'Left groupchat {}'.format(room_jid) self.special_rooms['control'].send_notice(msg) def matrix_all_chat_message(self, room: MatrixRoom, event: Dict): """ Handle a message sent to Matrix all-chat room. Allows manual sending of xmpp messages: "/m target_jid your message here". Sends a notice with the expected format if it isn't there by default. :param room: Matrix room object representing the all-chat room :param event: The Matrix event that was received. Assumed to be an m.room.message . """ # Always ignore our own messages if event['sender'] == self.bot_id: return logger.debug('matrix_all_chat_message: {} {}'.format(room.room_id, str(event))) if event['content']['msgtype'] == 'm.text': message_body = event['content']['body'] message_parts = message_body.split() if message_parts[0] == '/m': jid = message_parts[1] payload = message_body[message_body.find(jid) + len(jid) + 1:] logger.info('sending manual message to '+ jid + ' : ' + payload) self.xmpp.send_message(mto=jid, mbody=payload, mtype='chat') else: room.send_notice('Expected message format: "/m DEST_JID your message here"') def matrix_message(self, room: MatrixRoom, event: Dict): """ Handle a message sent to a mapped Matrix room. Sends the message to the xmpp handle specified by the room's topic. :param room: Matrix room object representing the room in which the message was received. :param event: The Matrix event that was received. Assumed to be an m.room.message . """ if event['sender'] == self.bot_id: return if room.topic in self.special_rooms.keys(): logger.error('matrix_message called on special channel') logger.debug('matrix_message: {} {}'.format(room.room_id, event)) if event['content']['msgtype'] == 'm.text': message_body = event['content']['body'] if room.topic.startswith(self.groupchat_flag): jid = room.topic[len(self.groupchat_flag):] message_type = 'groupchat' else: jid = room.topic message_type = 'chat' logger.info('Matrix received message to {} : {}'.format(jid, message_body)) self.xmpp.send_message(mto=jid, mbody=message_body, mtype=message_type) # Possible that we're in a room that wasn't mapped if jid not in self.xmpp.jid_nick_map: logger.error('Received message in matrix room with topic {},'.format(jid) + 'which wasn\'t in the jid_nick_map') name = self.xmpp.jid_nick_map.get(jid, jid) send_message = self.jid_actions.get(jid, self.default_actions)['send_messages_to_all_chat'] if send_message: self.special_rooms['all_chat'].send_notice('To {} : {}'.format(name, message_body)) def xmpp_message(self, message: Dict): """ Handle a message received by the XMPP client. Sends the message to the relevant mapped Matrix room, as well as the Matrix all-chat room. :param message: The message that was received. :return: """ logger.info('XMPP received {} : {}'.format(message['from'].full, message['body'])) if message['type'] in ('normal', 'chat'): from_jid = message['from'].bare from_name = self.xmpp.jid_nick_map.get(from_jid, from_jid) send_message2all = self.jid_actions.get(from_jid, self.default_actions)['send_messages_to_all_chat'] if send_message2all: self.special_rooms['all_chat'].send_text('From ({})\n{}: {}'.format(from_jid, from_name, message['body'])) send_message2room = self.jid_actions.get(from_jid, self.default_actions)['send_messages_to_jid_rooms'] if send_message2room: if from_jid not in self.xmpp.jid_nick_map.keys(): logger.error('xmpp_message: JID {} NOT IN ROSTER!?'.format(from_jid)) self.xmpp.get_roster(block=True) room = self.get_room_for_topic(from_jid) room.send_text(message['body']) def xmpp_groupchat_message(self, message: Dict): """ Handle a groupchat message received by the XMPP client. Sends the message to the relevant mapped Matrix room, as well as the Matrix all-chat room. :param message: The message that was received. :return: """ logger.info('XMPP MUC received {} : {}'.format(message['from'].full, message['body'])) if message['type'] == 'groupchat': from_jid = message['from'].bare from_name = message['mucnick'] if self.groupchat_mute_own_nick and from_name == self.xmpp_groupchat_nick: return room = self.get_room_for_topic(self.groupchat_flag + from_jid) room.send_text(from_name + ': ' + message['body']) if self.groupchat_send_messages_to_all_chat: self.special_rooms['all_chat'].send_text( 'Room {}, from {}: {}'.format(from_jid, from_name, message['body'])) def create_groupchat_room(self, room_jid: str): room = self.create_mapped_room(topic=self.groupchat_flag + room_jid) if room_jid not in self.groupchat_jids: self.groupchat_jids.append(room_jid) for user_id in self.users_to_invite: room.invite_user(user_id) def xmpp_presence_available(self, presence: Dict): """ Handle a presence of type "available". Sends a notice to the control channel. :param presence: The presence that was received. """ logger.debug('XMPP received {} : (available)'.format(presence['from'].full)) jid = presence['from'].bare if jid not in self.xmpp.jid_nick_map.keys(): logger.error('xmpp_presence_available: JID {} NOT IN ROSTER!?'.format(jid)) self.xmpp.get_roster(block=True) send_presence = self.jid_actions.get(jid, self.default_actions)['send_presences_to_control'] if send_presence: name = self.xmpp.jid_nick_map.get(jid, jid) self.special_rooms['control'].send_notice('{} available ({})'.format(name, jid)) def xmpp_presence_unavailable(self, presence): """ Handle a presence of type "unavailable". Sends a notice to the control channel. :param presence: The presence that was received. """ logger.debug('XMPP received {} : (unavailable)'.format(presence['from'].full)) jid = presence['from'].bare if jid not in self.xmpp.jid_nick_map.keys(): logger.error('xmpp_presence_unavailable: JID {} NOT IN ROSTER!?'.format(jid)) self.xmpp.get_roster(block=True) send_presence = self.jid_actions.get(jid, self.default_actions)['send_presences_to_control'] if send_presence: name = self.xmpp.jid_nick_map.get(jid, jid) self.special_rooms['control'].send_notice('{} unavailable ({})'.format(name, jid)) def xmpp_roster_update(self, _event): """ Handle an XMPP roster update. Maps all existing Matrix rooms, creates a new mapped room for each JID in the roster which doesn't have one yet, and invites the users specified in the config in to all the rooms. :param _event: The received roster update event (unused). """ logger.debug('######### ROSTER UPDATE ###########') rjids = [jid for jid in self.xmpp.roster] if len(rjids) > 1: raise Exception('Not sure what to do with more than one roster...') roster0 = self.xmpp.roster[rjids[0]] self.xmpp.roster_dict = {jid: roster0[jid] for jid in roster0} roster = self.xmpp.roster_dict self.map_rooms_by_topic() # Create new rooms where none exist for jid, info in roster.items(): if '@' not in jid: logger.warning('Skipping fake jid in roster: ' + jid) continue name = info['name'] self.xmpp.jid_nick_map[jid] = name # Check if we need to create a room if self.jid_actions.get(jid, self.default_actions)['send_messages_to_jid_rooms']: self.create_mapped_room(topic=jid, name=name) logger.debug('Sending invitations..') # Invite to all rooms for room in self.matrix.get_rooms().values(): users_in_room = room.get_joined_members() for user_id in self.users_to_invite: if user_id not in users_in_room: room.invite_user(user_id) logger.debug('######## Done with roster update #######') def xmpp_unrecognized_event(self, event): logger.error('Unrecognized event: {} || {}'.format(type(event), event))