class TinyMatrixtBot(): def __init__(self, hostname, username, password, displayname): signal.signal(signal.SIGTERM, self.on_signal) signal.signal(signal.SIGHUP, self.on_signal) self.current_path = os.path.dirname(os.path.realpath(__file__)) self.scripts_path = os.path.join(self.current_path, "scripts") self.sockets_path = os.path.join(self.current_path, "sockets") if not os.access(self.sockets_path, os.W_OK): self.sockets_path = None os.chdir(self.scripts_path) self.scripts = self.load_scripts(self.scripts_path) self.client = MatrixClient(hostname) self.client.login_with_password(username=username, password=password) self.user = self.client.get_user(self.client.user_id) self.user.set_display_name(displayname) for room_id in self.client.get_rooms(): self.join_room(room_id) self.client.start_listener_thread() self.client.add_invite_listener(self.on_invite) self.client.add_leave_listener(self.on_leave) while True: sleep(10) def on_signal(self, signal, frame): if signal == 1: self.scripts = self.load_scripts(self.scripts_path) if signal == 15: sys.exit() def load_scripts(self, path): scripts = {} for script in os.listdir(path): script = os.path.join(path, script) if not stat.S_IXUSR & os.stat(script)[stat.ST_MODE]: continue output = subprocess.Popen( [script], env={ "CONFIG": "1" }, stdout=subprocess.PIPE, universal_newlines=True).communicate()[0].strip() if not output: continue scripts[output] = script print("script {} {}".format(output, script)) return scripts def on_invite(self, room_id, state): print("invite {}".format(room_id)) self.join_room(room_id) def join_room(self, room_id): room = self.client.join_room(room_id) room.add_listener(self.on_room_event) print("join {}".format(room_id)) if self.sockets_path is not None: thread = Thread(target=self.create_socket, args=(room, )) thread.daemon = True thread.start() def create_socket(self, room): socket_name = re.search("^\!([a-z]+):", room.room_id, re.IGNORECASE).group(1) socket_path = os.path.join(self.sockets_path, socket_name) try: os.remove(socket_path) except OSError: pass sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(socket_path) sock.listen(1) print("bind {}".format(socket_path)) while True: conn, addr = sock.accept() recv = conn.recv(4096).decode('utf-8').strip() print("recv {} {}".format(socket_path, recv)) room.send_text(recv) def on_leave(self, room_id, state): print("leave {}".format(room_id)) def on_room_event(self, room, event): if event["sender"] == self.client.user_id: return if event["type"] == "m.room.message": if event["content"]["msgtype"] == "m.text": body = event["content"]["body"].strip() for regex, script in self.scripts.items(): if re.search(regex, body, re.IGNORECASE): self.run_script(room, script, body) def run_script(self, room, script, args): print("run {} {}".format(script, args)) output = subprocess.Popen( [script, args], stdout=subprocess.PIPE, universal_newlines=True).communicate()[0].strip() for line in output.split("\n\n"): sleep(1) print(line) room.send_text(line)
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
class TinyMatrixtBot(): def __init__(self, path_config): signal.signal(signal.SIGHUP, self.on_signal) signal.signal(signal.SIGINT, self.on_signal) signal.signal(signal.SIGTERM, self.on_signal) self.config = configparser.ConfigParser() self.config.read(path_config) path_current = os.path.dirname(os.path.realpath(__file__)) self.path_lib = self.config.get("tiny-matrix-bot", "lib", fallback=os.path.join( path_current, "scripts")).strip() print("SCRIPTS {}".format(self.path_lib)) if os.access(self.path_lib, os.R_OK): self.scripts = self.load_scripts(self.path_lib) else: print("ERROR {} is not readable".format(self.path_lib)) sys.exit(0) self.path_var = self.config.get("tiny-matrix-bot", "var", fallback=os.path.join( path_current, "data")).strip() print("DATA {}".format(self.path_var)) if os.access(self.path_var, os.W_OK): os.chdir(self.path_var) else: print("ERROR {} is not writeable".format(self.path_var)) sys.exit(0) self.path_run = self.config.get("tiny-matrix-bot", "run", fallback=os.path.join( path_current, "sockets")).strip() if os.access(self.path_run, os.W_OK): print("SOCKETS {}".format(self.path_run)) else: print("SOCKETS {} (not writeable, disabling sockets)".format( self.path_run)) self.path_run = False self.client = MatrixClient(self.config.get("tiny-matrix-bot", "host")) self.client.login_with_password( username=self.config.get("tiny-matrix-bot", "user"), password=self.config.get("tiny-matrix-bot", "pass")) self.user = self.client.get_user(self.client.user_id) self.user.set_display_name(self.config.get("tiny-matrix-bot", "name")) for room_id in self.client.get_rooms(): self.join_room(room_id) self.client.start_listener_thread() self.client.add_invite_listener(self.on_invite) self.client.add_leave_listener(self.on_leave) while True: sleep(1) def on_signal(self, signal, frame): if signal == 1: self.scripts = self.load_scripts(self.path_lib) elif signal in [2, 15]: sys.exit(0) def load_scripts(self, path): scripts = {} for script in os.listdir(path): script_path = os.path.join(path, script) if not os.access(script_path, os.R_OK) \ or not os.access(script_path, os.X_OK): continue output = subprocess.Popen( [script_path], env={ "CONFIG": "1" }, stdout=subprocess.PIPE, universal_newlines=True).communicate()[0].strip() if not output: continue scripts[output] = script_path print("LOAD {} {}".format(script, output)) return scripts def on_invite(self, room_id, state): print("INVITE {}".format(room_id)) self.join_room(room_id) def join_room(self, room_id): room = self.client.join_room(room_id) room.add_listener(self.on_room_event) print("JOIN {}".format(room_id)) if self.path_run is not False: thread = Thread(target=self.create_socket, args=(room, )) thread.daemon = True thread.start() def create_socket(self, room): socket_name = re.search("^\!([a-z]+):", room.room_id, re.IGNORECASE).group(1) socket_path = os.path.join(self.path_run, socket_name) try: os.remove(socket_path) except OSError: pass sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(socket_path) sock.listen(1) print("SOCKET {}".format(socket_path)) while True: conn, addr = sock.accept() recv = conn.recv(4096).decode('utf-8').strip() print("RECV {} {}".format(socket_path, recv)) room.send_text(recv) def on_leave(self, room_id, state): print("LEAVE {}".format(room_id)) def on_room_event(self, room, event): if event["sender"] == self.client.user_id: return if event["type"] == "m.room.message": if event["content"]["msgtype"] == "m.text": body = event["content"]["body"].strip() for regex, script in self.scripts.items(): if re.search(regex, body, re.IGNORECASE): self.run_script(room, event, [script, body]) def run_script(self, room, event, args): print("ROOM {}".format(event["room_id"])) print("SENDER {}".format(event["sender"])) print("RUN {}".format(args)) output = subprocess.Popen( args, env={ "MXROOMID": event["room_id"], "MXSENDER": event["sender"] }, stdout=subprocess.PIPE, universal_newlines=True).communicate()[0].strip() sleep(0.5) for p in output.split("\n\n"): for l in p.split("\n"): print("OUTPUT {}".format(l)) room.send_text(p) sleep(1)
class MatrixBackend(ErrBot): def __init__(self, config): super().__init__(config) identity = config.BOT_IDENTITY self.token = identity["token"] self.url = identity["url"] self.user = identity["user"] self._client = None def build_identifier(self, text_representation: str) -> None: """Return an object that idenfifies a matrix person or room.""" pass @staticmethod def parse_identfier_pieces(regex: str, text_rep: str): m = re.match(regex, text_rep) if m: data, domain = m.group() return data, domain return None, None @staticmethod def parse_identfier(text_rep): """Parse matrix identifiers into usable types. Expected formats are as follows: !<room>:<domain> #<room>:<domain> @<user>:<domain> """ room, domain, user = None, None, None room, domain = MatrixBackend.parse_identfier_pieces( r"[!#](.*):(.*)", text_rep) if not room or not domain: user, domain = MatrixBackend.parse_identfier_pieces( r"@:(.*):(.*)", text_rep) return room, domain, user def send_msg(self, room_id, msg): room = self._client.join_room(room_id) return room.send_text(msg) def send_file(self, room_id, file_path, filename): with open(file_path, "rb") as f: content = f.read() return self.send_stream_content(room_id, content, filename) def send_stream_content(self, room_id, content, filename): res = self._client.upload(content, 'application/octet-stream', filename) room = self._client.join_room(room_id) room.send_file(res, filename) return res def build_reply(self, msg, text=None, private=False, threaded=False): response = self.build_message(text) response.frm = self.bot_identifier self.send_msg(msg.extras['room_id'], text) if private: response.to = msg.frm else: response.to = msg.frm.room if isinstance(msg.frm, RoomOccupant) else msg.frm return response def change_presence(self): pass def mode(self): pass def query_room(self): pass def rooms(self): pass def invite_callback(self, *args, **kwargs): print(args, kwargs) def ephemeral_callback(self, *args, **kwargs): print(args, kwargs) def leave_callback(self, *args, **kwargs): print(args, kwargs) def presence_callback(self, *args, **kwargs): print(args, kwargs) def callback(self, *events): for event in events: log.debug("Saw event %s.", event) if event["type"] == "m.room.message": content = event["content"] sender = event["sender"] if content["msgtype"] == "m.text": msg = Message(content["body"], extras={'room_id': event["room_id"]}) msg.frm = MatrixPerson(self._client, sender) msg.to = self.bot_identifier self.callback_message(msg) def serve_once(self): self._client = MatrixClient(self.url, token=self.token, user_id=self.user) self._client.add_listener(self.callback) self._client.add_invite_listener(self.invite_callback) self._client.add_ephemeral_listener(self.ephemeral_callback) self._client.add_leave_listener(self.leave_callback) self._client.add_presence_listener(self.presence_callback) self.connect_callback() self.bot_identifier = MatrixPerson(self._client, self.user) self._client.listen_forever()
class MatrixBotLedger(threading.Thread): def __init__(self, homeserver: str, username: str, password: str, allowed_users: str, kill_event: threading.Event): super().__init__() logger.debug( f"__init__({homeserver}, {username}, {password}, {allowed_users}, {kill_event})" ) self.allowed_users = allowed_users.split(',') self.homeserver = homeserver self.kill_event = kill_event self.password = password self.sh_timeout = SH_TIMEOUT self.username = username self.client = MatrixClient(self.homeserver) self.allowed_users.append(self.username) logger.debug(f"allowed users: {self.allowed_users}") def run(self) -> None: logger.debug("run()") self.connect() for room_id in self.client.rooms: self.join_room(room_id) self.client.start_listener_thread( timeout_ms=5000, exception_handler=self.listener_exception_handler) self.client.add_invite_listener(self.on_invite) self.client.add_leave_listener(self.on_leave) self.kill_event.wait() logger.info("Logging out.") self.client.logout() def connect(self): logger.debug("connect()") if not self.kill_event.is_set(): try: self.client.login(username=self.username, password=self.password, limit=0) logger.info(f"connected to {self.homeserver}") except MatrixError as me: logger.warning( f"connection to {self.homeserver} failed, retrying in 5 seconds... ({me})" ) time.sleep(5) self.connect() def listener_exception_handler(self, e): logger.debug(f"listener_exception_handler({e})") self.connect() def on_invite(self, room_id, state): logger.debug(f"on_invite({room_id}, {state}") sender = "someone" for event in state["events"]: if event["type"] != "m.room.join_rules": continue sender = event["sender"] break logger.info(f"Invited to {room_id} by {sender}.") if sender not in self.allowed_users: logger.info(f"no whitelist match, ignoring invite from {sender}") return self.join_room(room_id) def join_room(self, room_id): logger.debug(f"join_room({room_id})") room = self.client.join_room(room_id) room.add_listener(self.on_room_event) logger.info(f"Joined room {room.display_name}.") # TODO: debug this def on_leave(self, room_id, state): logger.debug(f"on_leave({room_id}, {state})") sender = "someone" for event in state["timeline"]["events"]: if not event["membership"]: continue sender = event["sender"] logger.info(f"Kicked from {room_id} by {sender}.") def on_room_event(self, room: Room, event): logger.debug(f"on_room_event({room}, {event}") if event["sender"] == self.client.user_id \ or event["type"] != "m.room.message" \ or event["content"]["msgtype"] != "m.text": return content_body = event["content"]["body"] body, html = None, None if content_body.startswith('!echo '): message = content_body[6:] logger.info(f"Echoing message '{message}' to room {room}.") body = message elif content_body.startswith('!sh '): body, html = self.run_local_command('!sh ', content_body) elif content_body.startswith('!ledger '): body, html = self.run_local_command('!ledger ', content_body, keep_prefix=True) if body: self.safe_send_message(room, body, html) def run_local_command(self, prefix: str, command: str, keep_prefix: bool = False) -> tuple: logger.debug(f"run_local_command({prefix}, {command}, {keep_prefix})") command = command[len(prefix):] body, html = None, None if keep_prefix: command = prefix[1:] + command try: body = self.__sh(command) html = f"<pre>{body}</pre" except subprocess.CalledProcessError as cpe: logger.warning(cpe) body = f"command failed: {cpe}" except subprocess.TimeoutExpired as te: logger.warning(te) body = f"command timed out: {te}" return body, html def __sh(self, command) -> str: logger.debug(f"__sh({command})") cmd = ["/bin/sh", "-c", command] return subprocess.check_output(cmd, timeout=self.sh_timeout).decode() def safe_send_message(self, room: Room, body: str, html: str): logger.debug(f"safe_send_message({room}, {body}, {html})") members = room.get_joined_members() logger.debug(f"room joined members: {members}") for u in members: if u.user_id not in self.allowed_users: body = "I'm sorry, but not everyone in this room has clearance, so I'm not going to respond." html = None break try: room.send_html(html, body) except MatrixError as me: logger.error(me)
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 RssBot: def __init__(self, url, user_id, token): self.feeds = {} self.room_configs = {} self._known_guids = set() self.client = MatrixClient(url, user_id=user_id, token=token) self.client.add_invite_listener(self._handle_invite) self.client.add_leave_listener(self._handle_leave) self._fetch_cond = Condition() self._fetch_thread = Thread(target=self._fetch_loop) self._fetch_account_data() for room in self.client.rooms.values(): self._setup_room(room) def _fetch_account_data(self): account_data_filter = \ '{"presence":{"types":[]},\ "room":{"rooms":[]},\ "account_data":{"types":["%s"]}\ }' % ACCOUNT_DATA_TYPE # FIXME: might want to get this upstream sync = self.client.api.sync(filter=account_data_filter) account_data = sync['account_data'] for event in account_data['events']: if event['type'] == ACCOUNT_DATA_TYPE: known_guids = event['content']['known_guids'] self._known_guids = set(known_guids) def _setup_room(self, room): room.add_listener(self._handle_message, event_type='m.room.message') def on_state(event): self._handle_room_config(room, event['content']) room.add_state_listener(on_state, event_type=ROOM_EVENT_TYPE) try: # FIXME: might want to get this upstream config = self.client.api._send( "GET", "/rooms/" + room.room_id + "/state/" + ROOM_EVENT_TYPE) except MatrixRequestError as e: if e.code != 404: raise e config = None if config: self._handle_room_config(room, config) def _handle_invite(self, roomId, state): room = self.client.join_room(roomId) self._setup_room(room) def _handle_leave(self, room_id, room): if room_id in self.room_configs: del self.room_configs[room_id] self._update_feeds_config() def _handle_message(self, room, event): msg = str(event['content']['body']) if msg.startswith('!rss'): pass # maybe a command interface for easier use? def _handle_room_config(self, room, config): room_config = dict() for entry in config['feeds']: url = str(entry['url']) update_interval = int(entry['update_interval_secs']) room_config[url] = update_interval self.room_configs[room.room_id] = room_config self._update_feeds_config() def _update_feeds_config(self): feeds = dict() for room_config in self.room_configs.values(): for url, update_interval in room_config.items(): if url not in feeds or feeds[url] > update_interval: feeds[url] = update_interval self.feeds = {k: [v, 0] for k, v in feeds.items()} with self._fetch_cond: self._fetch_cond.notify() def _fetch_loop(self): while True: now = time.time() for url, times in self.feeds.items(): [interval, last_update] = times next_update = last_update + interval if next_update <= now: self._fetch_feed(url) times[1] = now with self._fetch_cond: if self.feeds: now = time.time() timeout = None for url, [interval, last_update] in self.feeds.items(): feed_timeout = last_update + interval - now if timeout is None or feed_timeout < timeout: timeout = feed_timeout if timeout > 0: self._fetch_cond.wait(timeout) else: # No feeds registered self._fetch_cond.wait() def get_rooms_for_feed(self, url): return [ self.client.rooms[room_id] for room_id, feeds in self.room_configs.items() if url in feeds ] def _fetch_feed(self, url): # FIXME: one site with slow response times can block all feeds print('Fetching updates from {}'.format(url)) try: feed = feedparser.parse(url) feed_title = feed.feed.title to_be_sent = [] any_knowns = False for entry in feed.entries: guid = entry.id if guid not in self._known_guids: self._known_guids.add(guid) to_be_sent.append(entry) else: any_knowns = True if not to_be_sent: return self.client.api.set_account_data( self.client.user_id, ACCOUNT_DATA_TYPE, {'known_guids': list(self._known_guids)}) if not any_knowns: return for entry in reversed(to_be_sent): html = '[<a href="{}">{}</a>] {}'\ .format(entry.link, feed_title, entry.title) raw = '[{}][{}] {}'\ .format(feed_title, entry.link, entry.title) print(raw) for room in self.get_rooms_for_feed(url): room.send_html(html, raw, 'm.notice') except Exception: print('Failed to parse feed {}: {}'.format(url, traceback.format_exc())) def run(self): self._fetch_thread.start() self.client.listen_forever()
class Matrix(): def __init__(self): self.rooms = {} self.matrixClient = {} def login(self): global theBot config = bot.theBot.config #variables self.matrixClient = MatrixClient(config.matrix["clienturl"]) if config.matrix["password"] == "": config.matrix["password"] = getpass.getpass(prompt='Password: '******'t find room:"+room) sys.exit(12) def handleException( self, exception ): print ( exception ) print ("An Error occured, ignoring") return def listen(self): global theBot config = bot.theBot.config #if success, start the command listener. for i in self.rooms: self.rooms[i].add_listener( self.listener ) self.matrixClient.add_listener( self.clientEvent ); self.matrixClient.add_leave_listener( self.leftRoom ); self.matrixClient.start_listener_thread( exception_handler = self.handleException ) def clientEvent( self, event ): if event["type"] == "m.room.power_levels": print("Damn it Hellbacon!") def leftRoom(self, room_id, event): #room_id, not to be mistaken for room name. #this shouldn't happen, so rejoin. room = "" time.sleep(10) try: for i in self.rooms: if self.rooms[i].room_id == room_id: room = i self.rooms[room].leave();#remove the room, before rejoining. self.rooms[room] = self.matrixClient.join_room(room) self.rooms[room].add_listener( self.listener ) 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:"+room) sys.exit(12) def listener(self, room, event): global theBot config = bot.theBot.config if event['type'] == "m.room.message": if event['content']['msgtype'] == "m.text": #print("{0}: {1}".format(event['sender'], event['content']['body'])) # ignore anything the bot might send to itself if(event['sender'] == "@"+config.matrix["username"]+":cclub.cs.wmich.edu"): return #built in auto response to mention. if ( config.matrix["username"] + ":" in event['content']['body']): room.send_text("Hi! I am a bot. If you want to know my commands type \""+config.prefix+"commands\" for available commands") # split the string to commands input = event['content']['body'].split(" ") # create responses for messages starting with prefix if len(input) > 0: if len(input[0]) > 0: ep = commandcenter.EventPackage() ep.body = input ep.room_id = event["room_id"] ep.sender = event["sender"] ep.event = event ep.command = input[0] if ep.command == config.prefix+"purge": self.purge(ep) else: output = bot.theBot.cc.run(ep) # if the command is in our dictionary of functions, use it (from commands.py) if len(output) > 0: room.send_text(output) else: print(event['type']) def purge(self, event ): return