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] restore_room_topic = True # type: bool 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 send_messages_to_all_chat = True # type: bool disable_all_chat_room = False # type: bool send_presences_to_control = True # type: bool groupchat_mute_own_nick = True # type: bool disabled_jids = set() # type: Set[str] @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.load_config(config_file) self.matrix = MatrixClient(**self.matrix_server) self.xmpp = ClientXMPP(**self.xmpp_login, **self.xmpp_roster_options) self.matrix.login_with_password(**self.matrix_login) if self.disable_all_chat_room: self.send_messages_to_all_chat = False #should not be necessary (see load_config) if 'all_chat' in self.special_rooms: del self.special_rooms['all_chat'] if 'all_chat' in self.special_room_names: del self.special_room_names['all_chat'] # Prepare matrix special channels and their listeners for room in self.matrix.get_rooms().values(): room.update_room_topic() topic = room.topic if topic in self.special_rooms.keys(): logging.debug('Recovering special room: ' + topic) self.special_rooms[topic] = room elif topic.startswith(self.groupchat_flag): room_jid = topic[len(self.groupchat_flag):] self.groupchat_jids.append(room_jid) 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') if not self.disable_all_chat_room: 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) # Prepare xmpp listeners self.xmpp.add_event_handler('roster_update', self.xmpp_roster_update) self.xmpp.add_event_handler('message', self.xmpp_message) self.xmpp.add_event_handler('presence_available', self.xmpp_presence_available) self.xmpp.add_event_handler('presence_unavailable', self.xmpp_presence_unavailable) self.xmpp.add_event_handler('groupchat_message', self.xmpp_groupchat_message) # Connect to XMPP and start processing XMPP events self.xmpp.connect(self.xmpp_server) self.xmpp.process(block=False) # Rejoin group chats logging.debug('Rejoining group chats') for room_jid in self.groupchat_jids: self.xmpp.plugin['xep_0045'].joinMUC(room_jid, self.xmpp_groupchat_nick) logging.debug('Done with bot init') def load_config(self, path: str): with open(path, 'r') as conf_file: config = yaml.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'] if 'restore_room_topic' in config[ 'matrix']: # to be compatible to other config files self.restore_room_topic = config['matrix']['restore_room_topic'] 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.send_presences_to_control = config['send_presences_to_control'] self.send_messages_to_all_chat = config['send_messages_to_all_chat'] if not self.send_messages_to_all_chat and 'disable_all_chat_room' in config: self.disable_all_chat_room = config['disable_all_chat_room'] else: self.disable_all_chat_room = False self.groupchat_mute_own_nick = config['groupchat_mute_own_nick'] self.xmpp_roster_options = config['xmpp']['roster_options'] if 'disabled_jids' in config: self.disabled_jids = set(config['disabled_jids']) if 'xmpp_login_jid' in self.disabled_jids: self.disabled_jids.add(self.xmpp_login['jid']) self.disabled_jids.remove('xmpp_login_jid') def get_room_for_jid(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 logging.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 not name: #room without name is shown as the bot's name in clients like riot name = topic if topic in self.groupchat_jids: logging.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] logging.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 logging.info('Created mapped room with topic {} and id {}'.format( topic, str(room.room_id))) room.set_room_name(name) room.update_room_name( ) #room.name is not set automatically in all cases if self.restore_room_topic and room.name != name: room.set_room_name(name) return room 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() logging.debug('Unmapped room {} ({}) [{}]'.format( room.room_id, room.name, room.topic)) if room.topic is None or '@' not in room.topic: logging.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 logging.debug('matrix_control_message: {} {}'.format( room.room_id, str(event))) if event['content']['msgtype'] == 'm.text': message_body = event['content']['body'] logging.info('Matrix received control message: ' + message_body) message_parts = message_body.split() if len(message_parts) > 0: message_parts[0] = message_parts[0].lower() # what about a empty body? 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(): logging.info( 'Leaving room {r.room_id} ({r.name}) [{r.topic}]'. format(r=room)) if room.topic.startswith(self.groupchat_flag): room_jid = room.topic[len(self.groupchat_flag):] self.xmpp.plugin['xep_0045'].leaveMUC(room_jid) room.leave() elif len(message_parts) > 1: if message_parts[0] == 'joinmuc': room_jid = message_parts[1] logging.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': room_jid = message_parts[1] logging.info('XMPP MUC leave: {}'.format(room_jid)) self.xmpp.plugin['xep_0045'].leaveMUC( room_jid, self.xmpp_groupchat_nick) room = self.get_room_for_jid(self.groupchat_flag + room_jid) room.leave() def matrix_all_chat_message(self, room: MatrixRoom, event: Dict): """ Handle a message sent to Matrix all-chat room. Currently just sends a warning that nobody will hear your message. :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 logging.debug('matrix_all_chat_message: {} {}'.format( room.room_id, str(event))) room.send_notice('Don\'t talk in here! Nobody gets your messages.') 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(): logging.error('matrix_message called on special channel') logging.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' name = self.xmpp.jid_nick_map[jid] logging.info('Matrix received message to {} : {}'.format( jid, message_body)) self.xmpp.send_message(mto=jid, mbody=message_body, mtype=message_type) if self.send_messages_to_all_chat: 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: """ logging.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[from_jid] if from_jid in self.groupchat_jids: logging.warning( 'Normal chat message from a groupchat, ignoring...') return room = self.get_room_for_jid(from_jid) room.send_text(message['body']) if self.send_messages_to_all_chat: self.special_rooms['all_chat'].send_text('From {}: {}'.format( from_name, 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: """ logging.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_jid(self.groupchat_flag + from_jid) room.send_text(from_name + ': ' + message['body']) if self.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. """ logging.debug('XMPP received {} : (available)'.format( presence['from'].full)) jid = presence['from'].bare if jid in self.disabled_jids: return if jid not in self.xmpp.jid_nick_map.keys(): logging.error('JID NOT IN ROSTER!?') self.xmpp.get_roster() return if self.send_presences_to_control: name = self.xmpp.jid_nick_map[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. """ logging.debug('XMPP received {} : (unavailable)'.format( presence['from'].full)) jid = presence['from'].bare if jid in self.disabled_jids: return if self.send_presences_to_control: name = self.xmpp.jid_nick_map[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). """ logging.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: logging.warning('Skipping fake jid in roster: ' + jid) continue if jid in self.disabled_jids: continue name = info['name'] self.xmpp.jid_nick_map[jid] = name self.create_mapped_room(topic=jid, name=name) logging.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) logging.debug('######## Done with roster update #######')
class MatrigramClient(object): def __init__(self, server, tb, username): self.client = MatrixClient(server) self.tb = tb self.token = None self.server = server self.username = username self.focus_room_id = None self.msg_type_router = { 'm.image': self.forward_image_to_tb, 'm.audio': self.forward_voice_to_tb, 'm.video': self.forward_video_to_tb, 'm.emote': self.forward_emote_to_tb, } self.room_listener_uid = None self.ephemeral_listener_uid = None def login(self, username, password): try: self.token = self.client.login_with_password(username, password) logger.info('token = %s', self.token) rooms = self.get_rooms_aliases() if rooms: # set focus room to "first" room self.set_focus_room(rooms.keys()[0]) self.client.add_invite_listener(self.on_invite_event) self.client.add_leave_listener(self.on_leave_event) self.client.start_listener_thread() return True, "OK" except MatrixRequestError: return False, "Failed to login" except ConnectionError: return False, "Server is offline" def logout(self): self.client.logout() def on_event(self, _, event): logger.debug('entered with message %s', pprint_json(event)) sender = event['sender'].split(':')[0].encode('utf-8') # Prevent messages loopback if sender.startswith(u'@{}'.format(self.username)): return if event['type'] == "m.room.message": msgtype = event['content']['msgtype'] if msgtype != 'm.text': callback = self.msg_type_router.get(msgtype) if callback: callback(event) return content = event['content']['body'].encode('utf-8') self.tb.send_message(sender, content, self) elif event['type'] == "m.room.topic": topic = event['content']['topic'].encode('utf-8') self.tb.send_topic(sender, topic, self) def on_ephemeral_event(self, _, ee): logger.debug(pprint_json(ee)) if ee['type'] == 'm.typing': if ee['content']['user_ids']: self.tb.start_typing_thread(self) else: self.tb.stop_typing_thread(self) def on_leave_event(self, room_id, le): logger.debug(pprint_json(le)) if le['timeline']['events'][0]['sender'] != le['timeline']['events'][ 0]['state_key']: self.tb.send_kick(self._room_id_to_alias(room_id), self) def on_invite_event(self, _, ie): logger.debug('invite event %s', pprint_json(ie)) room_name = None for event in ie['events']: if event['type'] == 'm.room.name': room_name = event['content']['name'] if room_name: self.tb.send_invite(self, self._room_id_to_alias(room_name)) def join_room(self, room_id_or_alias): try: self.client.join_room(room_id_or_alias) self.set_focus_room(room_id_or_alias) return True except MatrixRequestError: return False def leave_room(self, room_id_or_alias): room = self.get_room_obj(room_id_or_alias) if not room: logger.error('cant find room') return False if self.focus_room_id == room.room_id: rooms = self.get_rooms_aliases() room_id = self._room_alias_to_id(room_id_or_alias) del rooms[room_id] new_focus_room = rooms.keys()[0] if rooms else None self.set_focus_room(new_focus_room) return room.leave() def set_focus_room(self, room_id_or_alias): if self._room_alias_to_id(room_id_or_alias) == self.focus_room_id: return # remove current focus room listeners if self.focus_room_id is not None: room_obj = self.get_room_obj(self.focus_room_id) room_obj.remove_listener(self.room_listener_uid) self.room_listener_uid = None room_obj.remove_ephemeral_listener(self.ephemeral_listener_uid) self.ephemeral_listener_uid = None logger.info("remove focus room %s", self.focus_room_id) self.focus_room_id = None # set new room on focus if room_id_or_alias is not None: self.focus_room_id = self._room_alias_to_id(room_id_or_alias) room_obj = self.get_room_obj(self.focus_room_id) self.room_listener_uid = room_obj.add_listener(self.on_event) self.ephemeral_listener_uid = room_obj.add_ephemeral_listener( self.on_ephemeral_event) logger.info("set focus room to %s", self.focus_room_id) def get_focus_room_alias(self): return self._room_id_to_alias(self.focus_room_id) def have_focus_room(self): return self.focus_room_id is not None def get_members(self): room_obj = self.get_room_obj(self.focus_room_id) rtn = room_obj.get_joined_members() return [ member['displayname'] for _, member in rtn.items() if member.get('displayname') ] def set_name(self, name): user = self.client.get_user(self.client.user_id) user.set_display_name(name) def emote(self, body): room_obj = self.get_room_obj(self.focus_room_id) room_obj.send_emote(body) def create_room(self, alias, is_public=False, invitees=()): try: room_obj = self.client.create_room(alias, is_public, invitees) room_obj.update_aliases() logger.debug('room_id %s', room_obj.room_id) logger.debug('room_alias %s', room_obj.aliases[0]) return (room_obj.room_id, room_obj.aliases[0]) except MatrixRequestError: logger.error('error creating room') return None, None def backfill_previous_messages(self, limit=10): room_obj = self.get_room_obj(self.focus_room_id) room_obj.backfill_previous_messages(limit=limit) def get_rooms_aliases(self): # returns a dict with id: room obj rooms = self._get_rooms_updated() if not rooms: return rooms logger.debug("rooms got from server are %s", rooms) # return dict with id: list of aliases or id (if no alias exists) return { key: val.aliases if val.aliases else [key] for (key, val) in rooms.items() } def get_room_obj(self, room_id_or_alias): """Get room object of specific id or alias. Args: room_id_or_alias (str): Room's id or alias. Returns (Room): Room object corresponding to room_id_or_alias. """ rooms = self._get_rooms_updated() room_id = self._room_alias_to_id(room_id_or_alias) return rooms.get(room_id) def send_message(self, msg): room_obj = self.get_room_obj(self.focus_room_id) if not room_obj: logger.error('cant find room') else: room_obj.send_text(msg) def send_photo(self, path): with open(path, 'rb') as f: mxcurl = self.client.upload(f.read(), mimetypes.guess_type(path)[0]) room_obj = self.get_room_obj(self.focus_room_id) if not room_obj: logger.error('cant find room') else: room_obj.send_image(mxcurl, os.path.split(path)[1]) def send_voice(self, path): with open(path, 'rb') as f: mxcurl = self.client.upload(f.read(), mimetypes.guess_type(path)[0]) room_obj = self.get_room_obj(self.focus_room_id) room_obj.send_audio(mxcurl, os.path.split(path)[1]) def send_video(self, path): with open(path, 'rb') as f: mxcurl = self.client.upload(f.read(), mimetypes.guess_type(path)[0]) room_obj = self.get_room_obj(self.focus_room_id) room_obj.send_video(mxcurl, os.path.split(path)[1]) def discover_rooms(self): res = requests.get('{}/_matrix/client/r0/publicRooms?limit=20'.format( self.server)) res_json = res.json() room_objs = res_json['chunk'] return [ room['aliases'][0] for room in room_objs if room.get('aliases') ] def download_from_event(self, event): mxcurl = event['content']['url'] link = self.client.api.get_download_url(mxcurl) media_id = mxcurl.split('/')[3] media_type = event['content']['info']['mimetype'].split('/')[1] path = os.path.join(self.tb.config['media_dir'], '{}.{}'.format(media_id, media_type)) download_file(link, path) return path def forward_image_to_tb(self, event): sender = event['sender'].split(':')[0].encode('utf-8') path = self.download_from_event(event) self.tb.send_photo(sender, path, self) def forward_voice_to_tb(self, event): sender = event['sender'].split(':')[0].encode('utf-8') path = self.download_from_event(event) self.tb.send_voice(sender, path, self) def forward_video_to_tb(self, event): sender = event['sender'].split(':')[0].encode('utf-8') path = self.download_from_event(event) self.tb.send_video(sender, path, self) def forward_emote_to_tb(self, event): sender = event['sender'].split(':')[0].encode('utf-8') content = event['content']['body'].encode('utf-8') self.tb.send_emote(sender, content, self) def _room_id_to_alias(self, id): """Convert room id to alias. Args: id (str): Room id. Returns (str): Room alias. """ if id is None: return None if id.startswith('#'): return id rooms = self.get_rooms_aliases() if id in rooms: return rooms[id][0] else: return None def _room_alias_to_id(self, alias): """Convert room alias to id. Args: alias (str): Room alias. Returns (str): Room id. """ if alias is None: return None if not alias.startswith('#'): return alias return self.client.api.get_room_id(alias) def _get_rooms_updated(self): """Return rooms dictionary with updated aliases. Returns (dict): Return rooms dictionary with updated aliases. """ rooms = self.client.get_rooms() for room in rooms.values(): room.update_aliases() return rooms
def start(stdscr): global server, base_url, username, access_token, password global size, room, rooms, all_rooms, lastEventRoom, room_keys curses.curs_set(0) curses.use_default_colors() size = stdscr.getmaxyx() stdscr.addstr(0, 0, "loading...") stdscr.refresh() loadCredentials("./credentials.json") client = MatrixClient(base_url, token=access_token, user_id='@{}:{}'.format(username, server)) if access_token is None: access_token = client.login_with_password(username, password, size[0]) rooms = client.get_rooms() all_rooms = "all rooms" rooms[all_rooms] = all_rooms room_keys = list(rooms.keys()) room = all_rooms curses.halfdelay(10) maxDisplayName = 24 displayNamestartingPos = 20 PAD_COMMENTS = True pause = False client.add_listener(processMessage) client.start_listener_thread() curses.echo() stdscr.keypad(True) inputBuffer = "" lastEventRoom = all_rooms the_room_to_post_to = None # store the last room we saw before we started typing while (True): size = stdscr.getmaxyx() maxChars = size[1] - 1 - len(username) - 3 stdscr.clear() line = "redpill v0.7" line += " · screen size: " + str(size) if isinstance(rooms[room], Room): line += " · chat size: " + str(len(rooms[room].events)) line += " · room: " # we want NAME aka ALIAS[0] (ROOM_ID) # or 2nd choice: ALIAS[0] (ROOM_ID) # or fallback: ROOM_ID if room == all_rooms: line += str(room) elif rooms[room].name: if len(rooms[room].aliases) > 0 and rooms[room].aliases[0] != room: line += rooms[room].name + " aka " + getFirstRoomAlias( rooms[room]) + " (" + str(room) + ")" elif rooms[room].name != room: line += rooms[room].name + " (" + str(room) + ")" else: line += str(room) elif len(rooms[room].aliases) > 0 and rooms[room].aliases[0] != room: line += rooms[room].aliases[0] + " (" + str(line) + ")" else: line += str(room) if isinstance(rooms[room], Room) and rooms[room].topic is not None: line += " · topic: " + rooms[room].topic line += " · variables: room: " + room + ", last: " + lastEventRoom stdscr.addstr(0, 0, line, curses.A_UNDERLINE) current = len(rooms[room].events) - 1 if isinstance(rooms[room], Room) else -1 if True: y = 1 if current >= 0: # TODO: something when the first event is a typing event currentLine = size[0] - 1 # input space = "" for i in range(size[1] - 1): space += " " stdscr.addstr(currentLine, 0, space, curses.A_DIM) stdscr.addstr(currentLine, 0, "<" + username + ">", curses.A_DIM) stdscr.addstr(currentLine - 1, 0, space, curses.A_UNDERLINE) for event in reversed(rooms[room].events): log(event) log("maxDisplayName: {}, size: {}, currentLine: {}".format( maxDisplayName, size, currentLine)) if event["type"] == "m.typing": #if True: continue # do something clever elif event["type"] == "m.presence": #if True: continue # do something clever elif event["type"] == "m.roomchange": room_id = event["room_id"] #lin = (str(rooms[room_id].name) + " aka " + getFirstRoomAlias(rooms[room_id]) + " (" + # rooms[room_id].room_id + ")") line = room_id if line == all_rooms: pass elif rooms[line].name is None: if len( rooms[room_id].aliases ) > 0 and rooms[room_id].aliases[0] != room_id: line = rooms[room_id].aliases[ 0] + " (" + line + ")" else: if len( rooms[room_id].aliases ) > 0 and rooms[room_id].aliases[0] != room_id: line = rooms[ room_id].name + " aka " + getFirstRoomAlias( rooms[room_id]) + " (" + line + ")" else: if rooms[room_id].name != room_id: line = rooms[ room_id].name + " (" + line + ")" #if rooms[room].topic is not None: # line += " · topic: " + rooms[room].topic currentLine -= 1 stdscr.addstr(currentLine, 0, "Event(s) from " + line, curses.A_DIM) else: #currentLine = size[0] - y #currentLine -= 1 if currentLine < 3: # how many lines we want to reserve break #if currentLine == 5: # currentLine -= 1 y += 1 if "origin_server_ts" in event: convertedDate = datetime.datetime.fromtimestamp( int(event["origin_server_ts"] / 1000)).strftime('%Y-%m-%d %H:%M:%S') # assumption: body == normal message length = len( event["sender"]) if "sender" in event else 0 log("length: {}, currentLine: {}".format( length, currentLine)) if "body" in event["content"]: rawText = event["content"][ "body"] # .encode('utf-8') if event["content"]["msgtype"] == "m.emote": if len(rawText) > 0 and rawText[0] == " ": rawText = rawText[1:] linesNeeded = 0 buf = "" lineByLineText = [] first = True bufSinceLastWord = "" for char in rawText: if True: #for char in line: bufSinceLastWord += char if char == '\n': linesNeeded += 1 buf += bufSinceLastWord if PAD_COMMENTS or first: linesNeeded += int( (displayNamestartingPos + maxDisplayName + 3 + len(buf)) / size[1]) else: linesNeeded += int( len(buf) / size[1]) first = False lineByLineText.append(buf) buf = "" bufSinceLastWord = "" else: if ((PAD_COMMENTS and (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1) or (not PAD_COMMENTS and (len(buf + bufSinceLastWord)) == size[1] - 1)): #if (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1: if len(buf) == 0: buf += bufSinceLastWord bufSinceLastWord = "" if char.isspace(): buf += bufSinceLastWord lineByLineText.append(buf) bufSinceLastWord = "" buf = "" else: lineByLineText.append(buf) buf = bufSinceLastWord bufSinceLastWord = "" linesNeeded += 1 if char.isspace(): buf += bufSinceLastWord bufSinceLastWord = "" # if (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1: if ((PAD_COMMENTS and (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1) or (not PAD_COMMENTS and (len(buf + bufSinceLastWord)) == size[1] - 1)): buf += bufSinceLastWord bufSinceLastWord = "" lineByLineText.append(buf) linesNeeded += 1 buf = "" #elif char == ' ': # skip all whitespace # self.X += 1 buf += bufSinceLastWord lineByLineText.append(buf) linesNeeded += int( (displayNamestartingPos + maxDisplayName + 3 + len(buf)) / size[1]) buf = "" if currentLine - linesNeeded < 2: # how many lines we want to reserve break if PAD_COMMENTS: pad = displayNamestartingPos + maxDisplayName + 3 linesNeeded += 1 currentLine -= linesNeeded for i in range(linesNeeded): buf = rawText[:size[1] - pad] rawText = rawText[size[1] - pad:] if currentLine + i == size[0] - 2: stdscr.addstr( currentLine + i, displayNamestartingPos + maxDisplayName + 3, lineByLineText[i], curses.A_BOLD + curses.A_UNDERLINE) else: try: stdscr.addstr( currentLine + i, displayNamestartingPos + maxDisplayName + 3, lineByLineText[i], curses.A_BOLD) except: e = sys.exc_info()[0] print( "Error: unable to start thread. " + str(e)) stdscr.addstr(1, 0, str(e)) else: # TODO: need to split this out to get proper underline currentLine -= linesNeeded if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 3, rawText, curses.A_BOLD + curses.A_UNDERLINE) else: stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 3, rawText, curses.A_BOLD) usern = event["sender"] if length > maxDisplayName: usern = usern[:maxDisplayName - 3] + "..." if event["content"]["msgtype"] == "m.emote": usern = "* " + usern if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern), curses.A_UNDERLINE + curses.A_BOLD) else: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern), curses.A_BOLD) else: usern = "<" + usern + ">" if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern), curses.A_UNDERLINE) else: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern)) if currentLine == size[0] - 2: stdscr.addstr(currentLine, 0, convertedDate, curses.A_UNDERLINE) else: stdscr.addstr(currentLine, 0, convertedDate) #if currentLine == size[1]: # last line # stdscr.addstr( # currentLine, displayNamestartingPos + # maxDisplayName + 3, buf[:size[1] - # (displayNamestartingPos + maxDisplayName + 4)], # curses.A_BOLD # ) #else: # stdscr.addstr( # currentLine, displayNamestartingPos + # maxDisplayName + 3, buf, # curses.A_BOLD # ) # membership == join/leave events elif "membership" in event["content"]: buf = " invited someone" if event["content"]["membership"] == "invite": if "state_key" in event: buf = " invited " + event["state_key"] elif event["content"]["membership"] == "join": buf = " has joined" elif event["content"]["membership"] == "leave": buf = " has left" currentLine -= 1 if length > maxDisplayName: if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + 1, str(event["sender"]), curses.A_DIM + curses.A_UNDERLINE) stdscr.addstr( currentLine, displayNamestartingPos + length + 1, buf, curses.A_DIM + curses.A_UNDERLINE) else: stdscr.addstr(currentLine, displayNamestartingPos + 1, str(event["sender"]), curses.A_DIM) stdscr.addstr( currentLine, displayNamestartingPos + length + 1, buf, curses.A_DIM) else: if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + 1 + maxDisplayName - length, str(event["sender"]), curses.A_DIM + curses.A_UNDERLINE) stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 1, buf, curses.A_DIM + curses.A_UNDERLINE) else: stdscr.addstr( currentLine, displayNamestartingPos + 1 + maxDisplayName - length, str(event["sender"]), curses.A_DIM) stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 1, buf, curses.A_DIM) current -= 1 if pause: stdscr.addstr( int(size[0] / 2) - 1, int(size[1] / 2), " ", curses.A_REVERSE) stdscr.addstr(int(size[0] / 2), int(size[1] / 2), " PAUSED ", curses.A_REVERSE) stdscr.addstr( int(size[0] / 2) + 1, int(size[1] / 2), " ", curses.A_REVERSE) try: stdscr.addstr(size[0] - 1, len(username) + 3, inputBuffer[-maxChars:]) except: e = sys.exc_info()[0] print("Error: unable to start thread. " + str(e)) stdscr.addstr(1, 0, str(e)) stdscr.refresh() # getInput(stdscr) #def getInput(stdscr): # if True: try: c = stdscr.getch(size[0] - 1, len(username) + 3) #c = stdscr.getkey(size[0] - 1, len(username) + 3) #stri = stdscr.getstr(size[0] - 1, len(username) + 3, 10) if c == -1: stdscr.addstr(1, 0, "timeout") else: if c <= 256 and c != 10 and c != 9: ## enter and tab inputBuffer += chr(c) if len(inputBuffer) == 1: # e.g. just started typing if lastEventRoom != all_rooms: the_room_to_post_to = lastEventRoom if c == 9: #stdscr.addstr(1, 0, "%s was pressed\n" % c) log("key pressed: 0x{:X}".format(c)) room = room_keys[(room_keys.index(room) + 1) % len(rooms)] the_room_to_post_to = None elif c == 10: # enter with open('redpill-sends.log', 'a') as the_file: the_file.write("the_room_to_post_to:" + str(the_room_to_post_to) + "\n") the_file.write("lastEventRoom: " + str(lastEventRoom) + "\n") the_file.write("room: " + str(room) + "\n") the_file.write("inputBuffer: " + str(inputBuffer) + "\n") the_file.write("---\n") if inputBuffer.startswith("/invite"): user_id = inputBuffer[7:].strip() rooms[room].invite_user(user_id) elif inputBuffer.startswith("/kick"): user_id = inputBuffer[5:].strip() reason = "no reason..." rooms[room].kick_user(user_id, reason) elif inputBuffer.startswith("/power"): user_id = inputBuffer[7:].strip() power_level = 50 rooms[room].set_power_level(user_id, power_level) elif inputBuffer.startswith("/op"): user_id = inputBuffer[2:].strip() rooms[room].set_power_level(user_id) elif inputBuffer.startswith("/ban"): # reason user_id = inputBuffer[4:].strip() reason = "sux" #FIXME rooms[room].ban(user_id, reason) elif inputBuffer.startswith( "/join"): # there's a /join that supports aliases room_alias = inputBuffer[5:].strip() client.join_room(room_alias) elif inputBuffer.startswith("/j"): room_alias = inputBuffer[2:].strip() client.join_room(room_alias) elif inputBuffer.startswith("/leave"): rooms[room].leave_room(room_id) elif inputBuffer.startswith("/create"): # create a new room is_public = True invitees = () # def create_room(self, alias=None, is_public=False, invitees=()): room_alias = inputBuffer[7:].strip() client.create_room(room_alias, is_public, invitees) elif inputBuffer.startswith("/topic"): # get or set topic new_topic = inputBuffer[6:].strip() if len(new_topic) > 0: rooms[room].topic = new_topic else: pass #rooms[room].topic = "fail" else: if room == all_rooms: if the_room_to_post_to is None: if lastEventRoom != all_rooms: the_room_to_post_to = lastEventRoom else: stdscr.addstr(1, 0, "No idea what room to post to!") stdscr.refresh() inputBuffer = "No idea what room to post to!" continue else: the_room_to_post_to = room if inputBuffer.startswith("/me"): rooms[the_room_to_post_to].send_emote(inputBuffer[3:]) else: rooms[the_room_to_post_to].send_text(inputBuffer) inputBuffer = "" the_room_to_post_to = None elif c == curses.KEY_DC: inputBuffer = "" the_room_to_post_to = None elif c == curses.KEY_BACKSPACE: if len(inputBuffer) > 0: inputBuffer = inputBuffer[:-1] if len(inputBuffer) == 0: the_room_to_post_to = None elif c == curses.KEY_IC: pause = not (pause) if pause: curses.nocbreak() curses.cbreak() stdscr.timeout(-1) stdscr.addstr( int(size[0] / 2) - 1, int(size[1] / 2), " ", curses.A_REVERSE) stdscr.addstr(int(size[0] / 2), int(size[1] / 2), " PAUSING ", curses.A_REVERSE) stdscr.addstr( int(size[0] / 2) + 1, int(size[1] / 2), " ", curses.A_REVERSE) stdscr.refresh() else: stdscr.addstr( int(size[0] / 2) - 1, int(size[1] / 2), " ", curses.A_REVERSE) stdscr.addstr(int(size[0] / 2), int(size[1] / 2), " RESUMING ", curses.A_REVERSE) stdscr.addstr( int(size[0] / 2) + 1, int(size[1] / 2), " ", curses.A_REVERSE) stdscr.refresh() curses.halfdelay(10) stdscr.timeout(1) elif c == 27: # need to test for alt combo or ESC curses.cbreak() curses.echo() #curses.curs_set(1) stdscr.keypad(0) curses.endwin() quit() elif c == curses.KEY_F2: PAD_COMMENTS = not PAD_COMMENTS #stdscr.addstr(2, 0, "time() == %s\n" % time.time()) finally: do_nothing = True
class MatrixEngine(object): chatapi = '' chatclient = '' chattoken = '' listener_thread_id = 0 new_msg_queue = [] new_msg_senders = [] def __init__(self, host=bothost, user=botuser, pwd=botpwd, room=botroom): print("Logging to matrix server...") self.chatclient = MatrixClient(host) try: self.chattoken = self.chatclient.login(username=user, password=pwd, sync=True) except MatrixRequestError as e: print(e) if e.code == 403: print("Bad username or password.") sys.exit(4) else: print("Check your sever details are correct.") sys.exit(2) except MissingSchema as e: print("Bad URL format.") print(e) sys.exit(3) self.chatapi = MatrixHttpApi(host, self.chattoken) print("Login OK") ### handle messages print("Setting up listener") try: room = self.chatclient.join_room(room) except MatrixRequestError as e: print(e) if e.code == 400: print("Room ID/Alias in the wrong format") sys.exit(11) else: print("Couldn't find room.") sys.exit(12) room.add_listener(self.on_message) self.listener_thread_id = self.chatclient.start_listener_thread() print('Listener set.. OK!') def sendmsg(self, msg, chatid=botroom): room = self.chatclient.join_room(chatid) response = self.chatapi.send_message(chatid, msg) def sendhtml(self, msg, chatid=botroom): room = self.chatclient.join_room(chatid) room.send_html(msg) def create_room(self, alias): ex_room = matrix_mongo.matrix_chats.find_one({"alias": alias}) if ex_room: room_id = ex_room['room_id'] room = self.chatclient.join_room(room_id) room.invite_user(ebuser) else: try: aldt = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d%H%M%S') new_room = self.chatclient.create_room(alias=alias + "_" + aldt, is_public=False, invitees=[ebuser]) room_id = new_room.room_id dtime = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S') chatdata = { "created_date": dtime, "alias": alias, "room_alias": alias + "_" + aldt, "room_id": room_id, "room_data": room } recordID = matrix_mongo.matrix_chats.insert_one(chatdata) except MatrixRequestError as e: print(str(e)) return room_id def sendfile(self, filename, chatid=botroom): room = self.chatclient.join_room(chatid) with open(filename, 'rb') as datafile: data = datafile.read() uri = self.chatclient.upload(content=data, content_type='image/jpg') print(uri) filedate = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S') room.send_image( uri, filedate + '_' + filename.replace('/', '_') + '.jpg') def on_message(self, room, event): try: # Called when a message is recieved. if event['type'] == "m.room.member": if event['membership'] == "join": # print("{0} joined".format(event['content']['displayname'])) dtime = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S') print(dtime + " new user joined to the room ") elif event['type'] == "m.room.message": if event['content']['msgtype'] == "m.text": # print("{0}: {1}".format(event['sender'], event['content']['body'])) dtime = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S') print(dtime + "| '" + event['content']['body'] + "' msg received from " + event['sender']) if event['sender'] != "@mybot:matrix.org": self.new_msg_senders.append(event['sender']) self.new_msg_queue.append(event['content']['body']) else: print('new event in room:' + event['type']) except Exception as e: print('error @ on_message: ' + str(e)) def parsemsg(self): for i in range(len(self.new_msg_queue)): origMsg = self.new_msg_queue[i] sender = self.new_msg_senders[i] msg = origMsg.lower() print("PARSER: '" + origMsg + "' msg received from " + sender) def doshit(): print('shit done') if msg == "test": doshit() elif msg == "msg": sendmsg("your msg responded!") elif msg == "html": sendhtml("your <b>html</b> message <h1>responded</h1>!") else: print("message not understood") self.new_msg_queue.remove(origMsg) self.new_msg_senders.remove(sender)
class Client: """ A Matrix client implementation. :param server_url: The Matrix server URL :type server_url: str :param UI: The user interface object :type UI: :class:`.ui.base.BaseUI` """ def __init__(self, server_url, UI): assert server_url, "Missing server URL" self.room = None self.client = MatrixClient(server_url) self.op_executor = OPExecutor(self._server_exception_handler) self.room_event_observer = RoomEventObserver(self) self.users = Users(self.client.api) commands = {} for name in dir(self): attr = getattr(self, name) if isinstance(attr, command.Command): commands[attr.cmd_type] = attr self.ui = UI(self.send_message, self.users, commands) self.users.set_modified_callback(self.ui.refresh_user_list) @property def connected(self): return self.client and self.client.should_listen def register(self, username, password): """ Register a new user on the server. :param username: The username to register :type username: str :param password: The password to register :type password: str :raises RegistrationException: """ assert username, "Missing username" assert password, "Missing password" LOG.info("Registering user {}".format(username)) try: self.client.register_with_password(username=username, password=password) except MatrixRequestError as exc: LOG.exception(exc) try: content = json.loads(exc.content) except json.decoder.JSONDecodeError as json_exc: raise exceptions.RegistrationException(str(json_exc)) try: if content["errcode"] in ("M_USER_IN_USE", "M_EXCLUSIVE"): raise exceptions.UsernameTaken(username) if content["errcode"] == "M_INVALID_USERNAME": raise exceptions.RegistrationException(content["error"]) if content["errcode"] == "M_UNKNOWN": if content["error"] == "Captcha is required.": raise exceptions.CaptchaRequired() except KeyError: pass raise exceptions.RegistrationUnknownError(exc) def login(self, username, password): """ Login to the server. If the login fails we try to register a new user using the same username and password. :param username: The username to login with :type username: str :param password: The password to login with :type password: str :raises LoginException: """ assert username, "Missing username" assert password, "Missing password" LOG.info("Login with username {}".format(username)) try: self.client.login_with_password(username=username, password=password) except MatrixRequestError as exc: LOG.exception(exc) try: content = json.loads(exc.content) except json.decoder.JSONDecodeError as json_exc: raise exceptions.LoginException(str(json_exc)) try: if content["errcode"] == "M_FORBIDDEN": raise exceptions.LoginFailed() except KeyError: pass raise exceptions.LoginUnknownError(exc) def create_room(self, room_alias): """ Create a new room on the server. :param room_alias: The alias of the room to create :type room_alias: str """ assert room_alias, "Missing room" LOG.info("Creating room {}".format(room_alias)) """ #room:host -> room """ room_alias_name = room_alias[1:].split(':')[0] self.room = self.client.create_room(room_alias_name) def join(self, room_alias): """ Join a room. If the room does not already exist on the server we try to automatically create it. :param room_alias: The alias of the room to join :type room_alias: str :raises JoinRoomException: """ assert room_alias, "Missing room" LOG.info("Joining room {}".format(room_alias)) try: self.room = self.client.join_room(room_alias) except MatrixRequestError as exc: LOG.exception(exc) try: content = json.loads(exc.content) except json.decoder.JSONDecodeError as json_exc: exceptions.JoinRoomException(json_exc) try: if content["errcode"] == "M_NOT_FOUND": raise exceptions.RoomNotFound() if content["errcode"] in ("M_FORBIDDEN", "M_UNKNOWN"): raise exceptions.JoinRoomException(content["error"]) except (KeyError, AttributeError): pass raise exceptions.JoinRoomUnknownError(exc) def run(self): """ Run the client. """ assert self.room, "You need to join a room before you run the client" self.room.add_listener(self.room_event_observer.on_room_event) self.op_executor.start() self.connect() self.ui.run() def stop(self): """ Stop the client. """ self.ui.stop() self.op_executor.stop() if self.connected: print("Waiting for server connection to close") print("Press ctrl+c to force stop") try: self.disconnect() except KeyboardInterrupt: pass def _server_exception_handler(self, exc): """ Exception handler for Matrix server errors. """ LOG.exception(exc) if isinstance(exc, ConnectionError): self.ui.draw_client_info("Server connection error") else: self.ui.draw_client_info("Unexpected server error: {}".format(exc)) if not settings.debug: self.ui.draw_client_info( "For more details enable debug mode, " "reproduce the issue and check the logs. " "Debug mode is enabled by setting the " "MATRIX_DEBUG environment variable") self.disconnect() def _populate_room(self): # Clear the users model from old user data. To avoid duplicates when # for example reconnecting self.users.clear() # Temporarily disable UI user list refresh callback when doing the # initial population of the users model. Especially important for large # rooms which would cause an excessive amount of re-draws. modified_callback = self.users.modified_callback self.users.set_modified_callback(lambda: None) users = self.room.get_joined_members().items() for user_id, user in users: self.users.add_user(user_id, nick=user["displayname"]) # Restore refresh callback self.users.set_modified_callback(modified_callback) # Execute an initial refresh modified_callback() @command.cmd(command.CONNECT, help_msg="Reconnect to the server") @op(require_connection=False) def connect(self): """ Connect to the server. Before the client starts syncing events with the server it retrieves the users currently in the room and backfills previous messages. The number of previous messages backfilled are decidede by :const:`HISTORY_LIMIT`. """ if self.connected: self.ui.draw_client_info("Already connected") return # Retrieve users currently in the room self._populate_room() # Get message history self.room.backfill_previous_messages(limit=HISTORY_LIMIT) self.client.start_listener_thread( timeout_ms=SERVER_TIMEOUT_MS, exception_handler=self._server_exception_handler) self.ui.draw_client_info("Connected to server") def disconnect(self): """ Disconnect from the server. This also causes the user to logout when the sync thread has closed. """ if self.connected: # Can't unfortunately cancel an in-flight request # https://github.com/kennethreitz/requests/issues/1353 self.client.should_listen = False self.ui.draw_client_info("Disconnected") # Wait here for the matrix client sync thread to exit before # joining so that we can interrupt using signals. while self.client.sync_thread.isAlive(): time.sleep(0.1) try: self.client.api.logout() except ConnectionError: pass @op def send_message(self, msg): """ Send a message to the room. :param msg: Message to send :type msg: str """ self.room.send_text(msg) @command.cmd(command.INVITE, help_msg=("Invite a user to the room " "(user_id syntax: @[mxid]:[server])")) @op def invite(self, user_id): """ Invite a user to the room. :param user_id: The MXID of the user you want to invite :type user_id: str """ try: self.client.api.invite_user(self.room.room_id, user_id) # self.room.invite_user(args[0]) except MatrixRequestError as exc: try: error_msg = json.loads(exc.content)["error"] except: error_msg = str(exc) self.ui.draw_client_info("Invite error: {}".format(error_msg)) @command.cmd(command.CHANGE_NICK, help_msg="Change nick") @op def change_nick(self, nick): """ Change your nick. :param nick: The displayname you want to change to. :type nick: str """ self.users.get_user(self.client.user_id).set_display_name(nick) @command.cmd(command.LEAVE, help_msg="Leave the room") def leave(self): """ Leave the room. """ # Stop listening for new events, when we leave the room we're forbidden # from interacting with the it anything anyway self.client.should_listen = False self.room.leave() self.ui.stop() @command.cmd(command.QUIT, help_msg="Exit the client") def quit(self): """ Exit the client. """ self.ui.stop() @command.cmd(command.HELP, help_msg="Show this") def show_help(self, cmd_type=None): """ Show a help message explaining all the available commands. :param cmd_type: Use cmd_type to display the help message of a specific command rather than all of them. :type cmd_type: int """ self.ui.draw_help(cmd_type)
def start(stdscr): global size, room, data, rooms, access_token, endTime, rooms, all_rooms, lastEventRoom, room_keys curses.curs_set(0) curses.use_default_colors() size = stdscr.getmaxyx() stdscr.addstr(0, 0, "loading...") stdscr.refresh() loadCredentials("./credentials.json") client = MatrixClient(server) access_token = client.login_with_password( username, password, size[0]) rooms = client.get_rooms() all_rooms = "all rooms" rooms[all_rooms] = Room(client, all_rooms) rooms[all_rooms].events = [] room_keys = list(rooms.keys()) room = all_rooms #room_keys[1] # "all_rooms" nextRoom = 1 endTime = client.end curses.halfdelay(10) maxDisplayName = 24 displayNamestartingPos = 20 PAD_COMMENTS = True pause = False client.add_listener(processMessage) client.start_listener_thread() curses.echo() stdscr.keypad(True) inputBuffer = "" lastEventRoom = all_rooms the_room_to_post_to = None # store the last room we saw before we started typing while(True): size = stdscr.getmaxyx() maxChars = size[1] - 1 - len(username) - 3 stdscr.clear() # we want NAME aka ALIAS[0] (ROOM_ID) # or 2nd choice: ALIAS[0] (ROOM_ID) # or fallback: ROOM_ID line = str(room) if line == all_rooms: pass elif rooms[room].name is None: if len(rooms[room].aliases) > 0 and rooms[room].aliases[0] != room: line = rooms[room].aliases[0] + " (" + line + ")" else: if len(rooms[room].aliases) > 0 and rooms[room].aliases[0] != room: line = rooms[room].name + " aka " + getFirstRoomAlias(rooms[room]) + " (" + line + ")" else: if rooms[room].name != room: line = rooms[room].name + " (" + line + ")" #line.encode("utf-8") if rooms[room].topic is not None: line += " · topic: " + rooms[room].topic stdscr.addstr( 0, 0, ( "redpill v0.7 · screen size: " + str(size) + " · chat size: " + str(len(rooms[room].events)) + " · room: " + str(line) + " the variables: room: " + room + " last: " + lastEventRoom ), curses.A_UNDERLINE ) current = len(rooms[room].events) - 1 if True: y = 1 if current >= 0: # TODO: something when the first event is a typing event currentLine = size[0] - 1 # input space = "" for i in range(size[1] - 1): space += " " stdscr.addstr(currentLine, 0, space, curses.A_DIM) stdscr.addstr(currentLine, 0, "<" + username + ">", curses.A_DIM) stdscr.addstr(currentLine - 1, 0, space, curses.A_UNDERLINE) for event in reversed(rooms[room].events): if event["type"] == "m.typing": #if True: continue # do something clever elif event["type"] == "m.presence": #if True: continue # do something clever elif event["type"] == "m.roomchange": room_id = event["room_id"] #lin = (str(rooms[room_id].name) + " aka " + getFirstRoomAlias(rooms[room_id]) + " (" + # rooms[room_id].room_id + ")") line = room_id if line == all_rooms: pass elif rooms[line].name is None: if len(rooms[room_id].aliases) > 0 and rooms[room_id].aliases[0] != room_id: line = rooms[room_id].aliases[0] + " (" + line + ")" else: if len(rooms[room_id].aliases) > 0 and rooms[room_id].aliases[0] != room_id: line = rooms[room_id].name + " aka " + getFirstRoomAlias(rooms[room_id]) + " (" + line + ")" else: if rooms[room_id].name != room_id: line = rooms[room_id].name + " (" + line + ")" #if rooms[room].topic is not None: # line += " · topic: " + rooms[room].topic currentLine -= 1 stdscr.addstr(currentLine, 0, "Event(s) from " + line, curses.A_DIM) else: #currentLine = size[0] - y currentLine -= 1 if currentLine < 3: # how many lines we want to reserve break #if currentLine == 5: # currentLine -= 1 y += 1 if "origin_server_ts" in event: convertedDate = datetime.datetime.fromtimestamp( int( event["origin_server_ts"] / 1000) ).strftime('%Y-%m-%d %H:%M:%S') # assumption: body == normal message length = 0 if "user_id" in event: length = len( event["user_id"] ) if "body" in event["content"]: rawText = event["content"]["body"].encode('utf-8') if event["content"]["msgtype"] == "m.emote": if len(rawText) > 0 and rawText[0] == " ": rawText = rawText[1:] linesNeeded = (displayNamestartingPos + maxDisplayName + 3 + len(rawText)) / size[1] lin = (displayNamestartingPos + maxDisplayName + 3 + len(rawText)) #if currentLine == size[0] - 2: # stdscr.addstr(currentLine, 0, str(lin) + " " + str(size[1]) + " " + str(linesNeeded) + " ", curses.A_UNDERLINE) #else: # stdscr.addstr(currentLine, 0, str(lin) + " " + str(size[1]) + " " + str(linesNeeded) + " ") linesNeeded = 0 buf = "" lineByLineText = [] first = True bufSinceLastWord = "" for char in rawText: if True: #for char in line: bufSinceLastWord += char if char == '\n': linesNeeded += 1 buf += bufSinceLastWord if PAD_COMMENTS or first: linesNeeded += (displayNamestartingPos + maxDisplayName + 3 + len(buf)) / size[1] else: linesNeeded += len(buf) / size[1] first = False lineByLineText.append(buf) buf = "" bufSinceLastWord = "" else: if ((PAD_COMMENTS and (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1) or (not PAD_COMMENTS and (len(buf + bufSinceLastWord)) == size[1] - 1)): #if (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1: if len(buf) == 0: buf += bufSinceLastWord bufSinceLastWord = "" if char.isspace(): buf += bufSinceLastWord lineByLineText.append(buf) bufSinceLastWord = "" buf = "" else: lineByLineText.append(buf) buf = bufSinceLastWord bufSinceLastWord = "" linesNeeded += 1 if char.isspace(): buf += bufSinceLastWord bufSinceLastWord = "" # if (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1: if ((PAD_COMMENTS and (displayNamestartingPos + maxDisplayName + 3 + len(buf + bufSinceLastWord)) == size[1] - 1) or (not PAD_COMMENTS and (len(buf + bufSinceLastWord)) == size[1] - 1)): buf += bufSinceLastWord bufSinceLastWord = "" lineByLineText.append(buf) linesNeeded += 1 buf = "" #elif char == ' ': # skip all whitespace # self.X += 1 buf += bufSinceLastWord lineByLineText.append(buf) linesNeeded += (displayNamestartingPos + maxDisplayName + 3 + len(buf)) / size[1] buf = "" currentLine -= linesNeeded if currentLine - linesNeeded < 2: # how many lines we want to reserve break if currentLine == size[0] - 2: stdscr.addstr(currentLine, 0, str(lin) + " " + str(size[1]) + " " + str(linesNeeded) + " ", curses.A_UNDERLINE) else: stdscr.addstr(currentLine, 0, str(lin) + " " + str(size[1]) + " " + str(linesNeeded) + " ") #for i in range(linesNeeded): if PAD_COMMENTS: pad = displayNamestartingPos + maxDisplayName + 3 #if linesNeeded == 0: linesNeeded += 1 for i in range(linesNeeded): buf = rawText[:size[1] - pad] rawText = rawText[size[1] - pad:] if currentLine + i == size[0] - 2: stdscr.addstr( currentLine + i, displayNamestartingPos + maxDisplayName + 3, lineByLineText[i], curses.A_BOLD + curses.A_UNDERLINE ) else: try: stdscr.addstr( currentLine + i, displayNamestartingPos + maxDisplayName + 3, lineByLineText[i], curses.A_BOLD ) except: e = sys.exc_info()[0] print("Error: unable to start thread. " + str(e)) stdscr.addstr(1, 0, str(e)) else: # TODO: need to split this out to get proper underline if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 3, rawText, curses.A_BOLD + curses.A_UNDERLINE ) else: stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 3, rawText, curses.A_BOLD ) usern = event["user_id"] if length > maxDisplayName: usern = usern[:maxDisplayName - 3] + "..." if event["content"]["msgtype"] == "m.emote": usern = "* " + usern if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern), curses.A_UNDERLINE + curses.A_BOLD ) else: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern), curses.A_BOLD ) else: usern = "<" + usern + ">" if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern), curses.A_UNDERLINE ) else: stdscr.addstr( currentLine, displayNamestartingPos + max(0, maxDisplayName - length), str(usern) ) if currentLine == size[0] - 2: stdscr.addstr(currentLine, 0, convertedDate, curses.A_UNDERLINE) else: stdscr.addstr(currentLine, 0, convertedDate) #if currentLine == size[1]: # last line # stdscr.addstr( # currentLine, displayNamestartingPos + # maxDisplayName + 3, buf[:size[1] - # (displayNamestartingPos + maxDisplayName + 4)], # curses.A_BOLD # ) #else: # stdscr.addstr( # currentLine, displayNamestartingPos + # maxDisplayName + 3, buf, # curses.A_BOLD # ) # membership == join/leave events elif "membership" in event["content"]: buf = " invited someone" if event["content"]["membership"] == "invite": if "state_key" in event: buf = " invited " + event["state_key"] elif event["content"]["membership"] == "join": buf = " has joined" elif event["content"]["membership"] == "leave": buf = " has left" if length > maxDisplayName: if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + 1, str(event["user_id"]), curses.A_DIM + curses.A_UNDERLINE ) stdscr.addstr( currentLine, displayNamestartingPos + length + 1, buf, curses.A_DIM + curses.A_UNDERLINE ) else: stdscr.addstr( currentLine, displayNamestartingPos + 1, str(event["user_id"]), curses.A_DIM ) stdscr.addstr( currentLine, displayNamestartingPos + length + 1, buf, curses.A_DIM ) else: if currentLine == size[0] - 2: stdscr.addstr( currentLine, displayNamestartingPos + 1 + maxDisplayName - length, str(event["user_id"]), curses.A_DIM + curses.A_UNDERLINE ) stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 1, buf, curses.A_DIM + curses.A_UNDERLINE ) else: stdscr.addstr( currentLine, displayNamestartingPos + 1 + maxDisplayName - length, str(event["user_id"]), curses.A_DIM ) stdscr.addstr( currentLine, displayNamestartingPos + maxDisplayName + 1, buf, curses.A_DIM ) current -= 1 if pause: stdscr.addstr( int(size[0] / 2) - 1, int(size[1] / 2), " ", curses.A_REVERSE ) stdscr.addstr( int(size[0] / 2), int(size[1] / 2), " PAUSED ", curses.A_REVERSE ) stdscr.addstr( int(size[0] / 2) + 1, int(size[1] / 2), " ", curses.A_REVERSE ) try: stdscr.addstr(size[0] - 1, len(username) + 3, inputBuffer[-maxChars:]) except: e = sys.exc_info()[0] print("Error: unable to start thread. " + str(e)) stdscr.addstr(1, 0, str(e)) stdscr.refresh() # getInput(stdscr) #def getInput(stdscr): # if True: try: c = stdscr.getch(size[0] - 1, len(username) + 3) #c = stdscr.getkey(size[0] - 1, len(username) + 3) #stri = stdscr.getstr(size[0] - 1, len(username) + 3, 10) if c == -1: stdscr.addstr(1, 0, "timeout") else: if c <= 256 and c != 10 and c != 9: ## enter and tab inputBuffer += chr(c) if len(inputBuffer) == 1: # e.g. just started typing if lastEventRoom != all_rooms: the_room_to_post_to = lastEventRoom if c == 9: #stdscr.addstr(1, 0, "%s was pressed\n" % c) room = room_keys[nextRoom] nextRoom = (nextRoom + 1) % len(rooms) the_room_to_post_to = None elif c == 10: # enter with open('sends.log', 'a') as the_file: the_file.write("the_room_to_post_to:" + str(the_room_to_post_to) + "\n") the_file.write("lastEventRoom: " + str(lastEventRoom) + "\n") the_file.write("room: " + str(room) + "\n") the_file.write("inputBuffer: " + str(inputBuffer) + "\n") the_file.write("---\n") if inputBuffer.startswith("/invite"): user_id = inputBuffer[7:].strip() rooms[room].invite_user(user_id) elif inputBuffer.startswith("/kick"): user_id = inputBuffer[5:].strip() reason = "no reason..." rooms[room].kick_user(user_id, reason) elif inputBuffer.startswith("/power"): user_id = inputBuffer[7:].strip() power_level = 50 rooms[room].set_power_level(user_id, power_level) elif inputBuffer.startswith("/op"): user_id = inputBuffer[2:].strip() rooms[room].set_power_level(user_id) elif inputBuffer.startswith("/ban"): # reason user_id = inputBuffer[4:].strip() reason = "sux" #FIXME rooms[room].ban(user_id, reason) elif inputBuffer.startswith("/join"): # there's a /join that supports aliases room_alias = inputBuffer[5:].strip() client.join_room(room_alias) elif inputBuffer.startswith("/j"): room_alias = inputBuffer[2:].strip() client.join_room(room_alias) elif inputBuffer.startswith("/leave"): rooms[room].leave_room(room_id) elif inputBuffer.startswith("/create"): # create a new room is_public = True invitees = () # def create_room(self, alias=None, is_public=False, invitees=()): room_alias = inputBuffer[7:].strip() client.create_room(room_alias, is_public, invitees) elif inputBuffer.startswith("/topic"): # get or set topic new_topic = inputBuffer[6:].strip() if len(new_topic) > 0: rooms[room].topic = new_topic else: pass #rooms[room].topic = "fail" else: if room == all_rooms: if the_room_to_post_to is None: if lastEventRoom != all_rooms: the_room_to_post_to = lastEventRoom else: stdscr.addstr(1, 0, "No idea what room to post to!") stdscr.refresh() inputBuffer = "No idea what room to post to!" continue else: the_room_to_post_to = room if inputBuffer.startswith("/me"): rooms[the_room_to_post_to].send_emote(inputBuffer[3:]) else: rooms[the_room_to_post_to].send_text(inputBuffer) inputBuffer = "" the_room_to_post_to = None elif c == curses.KEY_DC: inputBuffer = "" the_room_to_post_to = None elif c == curses.KEY_BACKSPACE: if len(inputBuffer) > 0: inputBuffer = inputBuffer[:-1] if len(inputBuffer) == 0: the_room_to_post_to = None elif c == curses.KEY_IC: pause = not(pause) if pause: curses.nocbreak() curses.cbreak() stdscr.timeout(-1) stdscr.addstr( int(size[0] / 2) - 1, int(size[1] / 2), " ", curses.A_REVERSE ) stdscr.addstr( int(size[0] / 2), int(size[1] / 2), " PAUSING ", curses.A_REVERSE ) stdscr.addstr( int(size[0] / 2) + 1, int(size[1] / 2), " ", curses.A_REVERSE ) stdscr.refresh() else: stdscr.addstr( int(size[0] / 2) - 1, int(size[1] / 2), " ", curses.A_REVERSE ) stdscr.addstr( int(size[0] / 2), int(size[1] / 2), " RESUMING ", curses.A_REVERSE ) stdscr.addstr( int(size[0] / 2) + 1, int(size[1] / 2), " ", curses.A_REVERSE ) stdscr.refresh() curses.halfdelay(10) stdscr.timeout(1) elif c == 27: # need to test for alt combo or ESC curses.cbreak() curses.echo() #curses.curs_set(1) stdscr.keypad(0) curses.endwin() quit() elif c == curses.KEY_F2: PAD_COMMENTS = not PAD_COMMENTS #stdscr.addstr(2, 0, "time() == %s\n" % time.time()) finally: do_nothing = True
class Navi: """ The Navi API """ def __init__(self, host_url, user_id, password, target_users, quiet=False): """ Starts up the bot. Connects to the homeserver and logs in. Args: base_url: Server url, e.g. https://example.com:8448 user_id: @user:example.com password: p4ssw0rd target_users: List of users (@user_id:example.com) to push messages to """ self.quiet = quiet self.target_users = target_users self.host_url = host_url self.user_id = user_id self.password = password try: self.client = MatrixClient(self.host_url) self._log("Connecting to {}...".format(self.host_url)) self.token = self.client.login_with_password( self.user_id, self.password) except MatrixRequestError as e: Navi._handle_matrix_exception(e) self._fetch_rooms() self._log("Current rooms:\n\t{}".format("\n\t".join( self.rooms.keys()))) def shutdown(self): """ Stops the bot """ self.client.logout() self._log("Connection closed.") def push_msg(self, msg): """ Push a message Args: msg: The message to push """ self._cleanup_rooms() self._create_rooms() self._log("Pushing message...") for r in self.rooms.values(): r.send_text(msg) def push_media(self, filepath): """ Push an image or video Args: filepath: The file path """ self._cleanup_rooms() self._create_rooms() self._log("Pushing media...") mime = filetype.guess(filepath).mime if "image" in mime: media_type = "image" elif "video" in mime: media_type = "video" else: return with open(filepath, "rb") as fld: contents = fld.read() mxc = self.client.upload(contents, mime) if media_type == "image": for r in self.rooms.values(): r.send_image(mxc, os.path.basename(filepath)) elif media_type == "video": for r in self.rooms.values(): r.send_video(mxc, os.path.basename(filepath)) def leave_all_rooms(self): """ Leaves all rooms """ self._log("Leaving all rooms..") for r in self.rooms.values(): r.leave() self._log("Left {} rooms".format(len(rooms))) def _cleanup_rooms(self): """ Leaves rooms that no target user is in """ for room_id in self.rooms.keys(): room = self.rooms[room_id] users = room.get_joined_members().keys() if any(u in users for u in self.target_users): continue self._log("Leaving room {} (Name: {})".format(room_id, room.name)) room.leave() self._fetch_rooms() def _create_rooms(self): """ Create rooms for users not found in any current room """ # Compile list of all users if self.rooms: current_users = reduce( lambda a, b: a & b, map(lambda r: set(r.get_joined_members().keys()), self.rooms.values())) else: current_users = set() missing_users = self.target_users - current_users for u in missing_users: try: self._log("Creating room for user {}...".format(u)) self.client.create_room(invitees=[u]) except MatrixRequestError as e: Navi._handle_matrix_exception(e) def _fetch_rooms(self): """ Fetches list of rooms """ self.rooms = self.client.get_rooms() def _log(self, msg): if not self.quiet: print(msg) @staticmethod def _handle_matrix_exception(e): """ Print exception and quit """ sys.stderr.write("Server Error {}: {}\n".format(e.code, e.content)) sys.exit(-1)
def send_post(post): client = MatrixClient("http://localhost:8008") # Existing user token = client.login_with_password(username="******", password="******") room = client.create_room("my_room_alias") room.send_text(f"{post.title}\n\n{post.text}\n\n{post.owner}")
class Application(QApplication): loggedIn = Signal(str) messageReceived = Signal(object, str, object, float) roomSwitched = Signal(object) roomJoined = Signal(object) roomUpdated = Signal(str, str, str) roomLeft = Signal(object) roomInvited = Signal(object) def __init__(self, *args, **kwargs): super(Application, self).__init__(*args, **kwargs) # setup the application name and icon self.setApplicationName('Qui') self.iconPath = os.path.dirname(__file__) self.iconPath = os.path.join(self.iconPath, 'icons/icon.png') self.setWindowIcon(QIcon(self.iconPath)) # setup the tray icon self.showNotifications = False self.tray = QSystemTrayIcon(QIcon(self.iconPath)) self.tray.show() self.tray.activated.connect(self.showWindow) # setup the tray icon menu self.menu = QMenu() self.menu.addAction('Quit').triggered.connect(self.quit) self.tray.setContextMenu(self.menu) # setup signals self.messageReceived.connect(self.receiveMessage) # show the window self.window = None self.showWindow(None) # try loading the login info self.client = None self.settings = QSettings('Qui', 'Qui') self.url = self.settings.value("url") self.token = self.settings.value("token") self.user = self.settings.value("user") invalid = self.url is None or self.url == "" or self.token is None or self.token == "" or self.user is None or self.user == "" if not invalid: try: self.client = MatrixClient(base_url=self.url, token=self.token, user_id=self.user) self.postLogin() except: invalid = True # show the login form if we can't login if invalid: self.loginForm = LoginForm() self.loginForm.loggedIn.connect(self.login) self.loginForm.show() def showWindow(self, reason): if self.window is None: # make a new window if we don't have one self.window = MainWindow() # setup signals self.loggedIn.connect(self.window.login) self.messageReceived.connect(self.window.receiveMessage) self.roomLeft.connect(self.window.roomLeft) self.roomJoined.connect(self.window.roomJoined) self.roomUpdated.connect(self.window.roomUpdated) self.window.createRoom.connect(self.createRoom) self.window.leaveRoom.connect(self.leaveRoom) self.window.joinRoom.connect(self.joinRoom) # show it self.window.show() else: # show it if it's minimized or something self.window.showNormal() def quit(self): sys.exit(0) def login(self, client, url): self.client = client self.settings.setValue('url', url) self.url = url self.settings.setValue('token', client.token) self.token = client.token self.settings.setValue('user', client.user_id) self.user = client.user_id self.postLogin() def postLogin(self): self.loggedIn.emit(self.user) #self.messages.userId = self.user self.client.add_listener(self.eventCallback) self.client.add_presence_listener(self.presenceCallback) self.client.add_invite_listener(self.inviteCallback) self.client.add_leave_listener(self.leaveCallback) self.client.start_listener_thread() for room, obj in self.client.get_rooms().items(): self.roomJoined.emit(obj) for event in obj.events: self.eventCallback(event) self.showNotifications = True def eventCallback(self, event): if 'type' in event and 'room_id' in event and 'content' in event and event[ 'type'] == 'm.room.message': room = Room(self.client, event['room_id']) self.messageReceived.emit(room, event['sender'], event['content'], time.time() - event['unsigned']['age']) if 'type' in event and 'room_id' in event and 'content' in event and event[ 'type'] == 'm.room.canonical_alias': self.roomUpdated.emit(event['room_id'], 'canonical_alias', event['content']['alias']) def presenceCallback(self, event): return print('presence: {}'.format(event)) def inviteCallback(self, roomId, state): return print('invite: {} {}'.format(roomId, state)) def leaveCallback(self, roomId, room): return print('leave: {} {}'.format(roomId, room)) def receiveMessage(self, room, sender, content, timestamp): if self.showNotifications: if self.window is None or not self.window.isActiveWindow(): self.tray.showMessage(sender, content['body']) def leaveRoom(self, room): self.client.api.leave_room(room.room_id) self.roomLeft.emit(room) def joinRoom(self, roomId): room = self.client.join_room(roomId) self.roomJoined.emit(room) def createRoom(self, roomId): room = self.client.create_room(roomId) self.roomJoined.emit(room)
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))