def send_msg(server_name, username, password, room_id, msg): client = MatrixClient(server_name) client.login(username=username, password=password) room = client.join_room(room_id) room.send_text(msg) client.logout()
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.enabled_scripts = self.config.get("tiny-matrix-bot", "enabled_scripts", fallback="").strip() self.path_lib = self.config.get("tiny-matrix-bot", "lib", fallback=os.path.join( _path_current, "scripts")).strip() logger.debug("path_lib = {}".format(self.path_lib)) if os.access(self.path_lib, os.R_OK): self.scripts = self.load_scripts(self.path_lib) else: logger.error("{} not readable".format(self.path_lib)) sys.exit(1) self.path_var = self.config.get("tiny-matrix-bot", "var", fallback=os.path.join( _path_current, "data")).strip() logger.debug("path_var = {}".format(self.path_var)) if os.access(self.path_var, os.W_OK): os.chdir(self.path_var) else: logger.error("{} not writeable".format(self.path_var)) sys.exit(1) 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): logger.debug("path_run = {}".format(self.path_run)) else: logger.debug("path_run = {}".format(self.path_run) + " (not writeable, disabling sockets)") self.path_run = False self.inviter_whitelist = self.config.get("tiny-matrix-bot", "inviter_whitelist", fallback="").strip() self.connect() 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( exception_handler=self.listener_exception_handler) self.client.add_invite_listener(self.on_invite) self.client.add_leave_listener(self.on_leave) while True: sleep(1) def connect(self): _host = self.config.get("tiny-matrix-bot", "host") _user = self.config.get("tiny-matrix-bot", "user") _pass = self.config.get("tiny-matrix-bot", "pass") try: self.client = MatrixClient(_host) self.client.login(username=_user, password=_pass, limit=0) logger.info("connected to {}".format(_host)) except Exception: logger.warning("connection to {} failed".format(_host) + ", retrying in 5 seconds...") sleep(5) self.connect() def listener_exception_handler(self, e): self.connect() def on_signal(self, signal, frame): if signal == 1: self.scripts = self.load_scripts(self.path_lib) elif signal in [2, 15]: self.client.logout() sys.exit(0) def load_scripts(self, path): _scripts = {} for _script in os.listdir(path): _script_path = os.path.join(path, _script) if self.enabled_scripts: if _script not in self.enabled_scripts: continue if (not os.access(_script_path, os.R_OK) or not os.access(_script_path, os.X_OK)): continue _regex = subprocess.Popen( [_script_path], env={ "CONFIG": "1" }, stdout=subprocess.PIPE, universal_newlines=True).communicate()[0].strip() if not _regex: continue _scripts[_regex] = _script_path logger.info("script load {}".format(_script)) logger.debug("script regex {}".format(_regex)) return _scripts def on_invite(self, room_id, state): _sender = "someone" for _event in state["events"]: if _event["type"] != "m.room.join_rules": continue _sender = _event["sender"] break logger.info("invited to {} by {}".format(room_id, _sender)) if self.inviter_whitelist: if not re.search(self.inviter_whitelist, _sender, re.IGNORECASE): logger.info( "no whitelist match, ignoring invite from {}".format( _sender)) return self.join_room(room_id) def join_room(self, room_id): logger.info("join {}".format(room_id)) _room = self.client.join_room(room_id) _room.add_listener(self.on_room_event) 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 _socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) _socket.bind(_socket_path) _socket.listen(1) logger.info("socket bind {}".format(_socket_path)) while True: _conn, _addr = _socket.accept() _recv = _conn.recv(4096).decode('utf-8').strip() logger.debug("socket recv {} {}".format(_socket_path, _recv)) room.send_text(_recv) def on_leave(self, room_id, state): _sender = "someone" for _event in state["timeline"]["events"]: if not _event["membership"]: continue _sender = _event["sender"] logger.info("kicked from {} by {}".format(room_id, _sender)) def on_room_event(self, room, event): if event["sender"] == self.client.user_id: return if event["type"] != "m.room.message": return if event["content"]["msgtype"] != "m.text": return _args = event["content"]["body"].strip() for _regex, _script in self.scripts.items(): if not re.search(_regex, _args, re.IGNORECASE): continue self.run_script(room, event, [_script, _args]) def run_script(self, room, event, args): logger.debug("script room_id {}".format(event["room_id"])) logger.debug("script sender {}".format(event["sender"])) logger.debug("script run {}".format(args)) _script = subprocess.Popen(args, env={ "MXROOMID": event["room_id"], "MXSENDER": event["sender"] }, stdout=subprocess.PIPE, universal_newlines=True) _output = _script.communicate()[0].strip() if _script.returncode != 0: logger.debug("script exit {}".format(_script.returncode)) return sleep(0.5) for _p in _output.split("\n\n"): for _l in _p.split("\n"): logger.debug("script output {}".format(_l)) room.send_text(_p) sleep(0.8)
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
add_result = db_helper.check_add_game(white, black, result, sender, comment) if add_result.startswith("fehler"): myroom.send_text(add_result) else: myroom.send_text(msg_result + "\n" + add_result) myroom.add_listener(on_message) client.start_listener_thread() myroom.send_text("Naelob returns!") try: get_input = raw_input except NameError: get_input = input #====== MAIN LOOP: ========== while True: msg = get_input() #TEST #time.sleep(0.3) if msg == "/quit": break client.stop_listener_thread() myroom.send_text("Naelob verabschiedet sich und geht offline.") client.logout()
class 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)
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)