class MatrixHandler(): def __init__(self, username, password, room, gotMsgCallback=blackhole): try: from matrix_client.client import MatrixClient except ImportError: print("ERR:matrix_client import fail: Please exec:") print("cd ./matrix-python-sdk") print("pip install ./") raise RuntimeError("Failed to import matrix_client sdk") self.matrix = MatrixClient("https://matrix.org") self.matrix.login_with_password(username=username, password=password) self.matrix.start_listener_thread() self.room = self.matrix.join_room(room) self.room.send_text("Hello!") self.room.add_listener(gotMsgCallback) #self.gotMsgCallback=gotMsgCallback def sendMsg(self, msgData): res = self.room.send_text(msgData) return res def sendImg(self, imgdir, content_type="image/jpeg"): with open(imgdir, mode="rb") as f: uri = self.matrix.upload(f.read(), content_type) self.room.send_image(uri, 'wximg') f.close() def sendAudio(self, fdir, content_type="audio/mp3"): with open(fdir, mode="rb") as f: uri = self.matrix.upload(f.read(), content_type) self.room.send_audio(uri, 'wxaudio') f.close() def sendHtml(self, htmlData): res = self.room.send_html(htmlData) return res '''
def main(): tmp_dir = conf.zbx_matrix_tmp_dir if tmp_dir == "/tmp/" + conf.zbx_tg_prefix: print_message( "WARNING: it is strongly recommended to change `zbx_tg_tmp_dir` variable in config!!!" ) print_message( "https://github.com/ableev/Zabbix-in-Telegram/wiki/Change-zbx_tg_tmp_dir-in-settings" ) tmp_cookie = tmp_dir + "/cookie.py.txt" tmp_uids = tmp_dir + "/uids.txt" tmp_need_update = False # do we need to update cache file with uids or not rnd = random.randint(0, 999) ts = time.time() hash_ts = str(ts) + "." + str(rnd) log_file = conf.log_path args = sys.argv settings = { "zbxtg_itemid": "0", # itemid for graph "zbxtg_title": None, # title for graph "zbxtg_image_period": None, "zbxtg_image_age": "3600", "zbxtg_image_width": "900", "zbxtg_image_height": "200", "tg_method_image": False, # if True - default send images, False - send text "is_debug": False, "is_channel": False, "disable_web_page_preview": False, "location": None, # address "lat": 0, # latitude "lon": 0, # longitude "is_single_message": False, "markdown": False, "html": False, "signature": None, "signature_disable": False, "graph_buttons": False, "extimg": None, "to": None, "to_group": None, "forked": False, } settings_description = { "itemid": { "name": "zbxtg_itemid", "type": "list", "help": "script will attach a graph with that itemid (could be multiple)", "url": "Graphs" }, "title": { "name": "zbxtg_title", "type": "str", "help": "title for attached graph", "url": "Graphs" }, "graphs_period": { "name": "zbxtg_image_period", "type": "int", "help": "graph period", "url": "Graphs" }, "graphs_age": { "name": "zbxtg_image_age", "type": "str", "help": "graph period as age", "url": "Graphs" }, "graphs_width": { "name": "zbxtg_image_width", "type": "int", "help": "graph width", "url": "Graphs" }, "graphs_height": { "name": "zbxtg_image_height", "type": "int", "help": "graph height", "url": "Graphs" }, "graphs": { "name": "tg_method_image", "type": "bool", "help": "enables graph sending", "url": "Graphs" }, "chat": { "name": "tg_chat", "type": "bool", "help": "deprecated, don't use it, see 'group'", "url": "How-to-send-message-to-the-group-chat" }, "group": { "name": "tg_group", "type": "bool", "help": "sends message to a group", "url": "How-to-send-message-to-the-group-chat" }, "debug": { "name": "is_debug", "type": "bool", "help": "enables 'debug'", "url": "How-to-test-script-in-command-line" }, "channel": { "name": "is_channel", "type": "bool", "help": "sends message to a channel", "url": "Channel-support" }, "disable_web_page_preview": { "name": "disable_web_page_preview", "type": "bool", "help": "disable web page preview", "url": "Disable-web-page-preview" }, "location": { "name": "location", "type": "str", "help": "address of location", "url": "Location" }, "lat": { "name": "lat", "type": "str", "help": "specify latitude (and lon too!)", "url": "Location" }, "lon": { "name": "lon", "type": "str", "help": "specify longitude (and lat too!)", "url": "Location" }, "single_message": { "name": "is_single_message", "type": "bool", "help": "do not split message and graph", "url": "Why-am-I-getting-two-messages-instead-of-one" }, "markdown": { "name": "markdown", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML" }, "html": { "name": "html", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML" }, "signature": { "name": "signature", "type": "str", "help": "bot's signature", "url": "Bot-signature" }, "signature_disable": { "name": "signature_disable", "type": "bool", "help": "enables/disables bot's signature", "url": "Bot-signature" }, "graph_buttons": { "name": "graph_buttons", "type": "bool", "help": "activates buttons under graph, could be using in ZbxTgDaemon", "url": "Interactive-bot" }, "external_image": { "name": "extimg", "type": "str", "help": "should be url; attaches external image from different source", "url": "External-image-as-graph" }, "to": { "name": "to", "type": "str", "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group" }, "to_group": { "name": "to_group", "type": "str", "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group" }, "forked": { "name": "forked", "type": "bool", "help": "internal variable, do not use it. Ever.", "url": "" }, } if len(args) < 4: do_not_exit = False if "--features" in args: print(("List of available settings, see {0}/Settings\n---".format( url_wiki_base))) for sett, proprt in list(settings_description.items()): print(("{0}: {1}\ndoc: {2}/{3}\n--".format( sett, proprt["help"], url_wiki_base, proprt["url"]))) elif "--show-settings" in args: do_not_exit = True print_message("Settings: " + str(json.dumps(settings, indent=2))) else: print(( "Hi. You should provide at least three arguments.\n" "zbxtg.py [TO] [SUBJECT] [BODY]\n\n" "1. Read main page and/or wiki: {0} + {1}\n" "2. Public Telegram group (discussion): {2}\n" "3. Public Telegram channel: {3}\n" "4. Try dev branch for test purposes (new features, etc): {0}/tree/dev" .format(url_github, url_wiki_base, url_tg_group, url_tg_channel))) if not do_not_exit: sys.exit(0) zbx_to = args[1] zbx_subject = args[2] zbx_body = args[3] zbx = ZabbixWeb(server=conf.zbx_server, username=conf.zbx_api_user, password=conf.zbx_api_pass) client = MatrixClient(conf.server) token = None try: token = client.login_with_password(username=conf.username, password=conf.password) except MatrixRequestError as e: print(e) if e.code == 403: print_message("Bad username or password.") sys.exit(4) else: print_message("Check your sever details are correct.") sys.exit(2) except MissingSchema as e: print_message("Bad URL format.") print(e) sys.exit(3) except: print_message("ERROR (unknown) login_with_password()!") sys.exit(5) room = None try: room = client.join_room(zbx_to) except MatrixRequestError as e: print(e) if e.code == 400: print_message("Room ID/Alias in the wrong format") sys.exit(11) else: print_message("Couldn't find room.") sys.exit(12) except: print_message("ERROR (unknown) join_room()!") sys.exit(13) zbx.tmp_dir = tmp_dir # workaround for Zabbix 4.x zbx_version = 3 try: zbx_version = conf.zbx_server_version except: pass # if conf.proxy_to_zbx: # zbx.proxies = { # "http": "http://{0}/".format(conf.proxy_to_zbx), # "https": "https://{0}/".format(conf.proxy_to_zbx) # } # https://github.com/ableev/Zabbix-in-Telegram/issues/55 try: if conf.zbx_basic_auth: zbx.basic_auth_user = conf.zbx_basic_auth_user zbx.basic_auth_pass = conf.zbx_basic_auth_pass except: pass try: zbx_api_verify = conf.zbx_api_verify zbx.verify = zbx_api_verify except: pass zbxtg_body = (zbx_subject + "\n" + zbx_body).splitlines() zbxtg_body_text = [] ## seems redundant but necessary to avoid errors of using these variables before declaration... shouldn't happen tg_method_image = bool(settings["tg_method_image"]) disable_web_page_preview = bool(settings["disable_web_page_preview"]) is_single_message = bool(settings["is_single_message"]) for line in zbxtg_body: if line.find(conf.zbx_tg_prefix) > -1: setting = re.split("[\s:=]+", line, maxsplit=1) key = setting[0].replace(conf.zbx_tg_prefix + ";", "") if key not in settings_description: # if "--debug" in args: room.send_text( str("[ERROR] There is no '{0}' method, use --features to get help" .format(key))) # continue if settings_description[key]["type"] == "list": value = setting[1].split(",") elif len(setting) > 1 and len(setting[1]) > 0: value = setting[1] elif settings_description[key]["type"] == "bool": value = True else: value = settings[settings_description[key]["name"]] if key in settings_description: settings[settings_description[key]["name"]] = value if key == "graphs" and value is True: tg_method_image = True else: zbxtg_body_text.append(line) if conf.DEBUG: is_debug = True zbx.debug = True log_file = tmp_dir + ".debug." + hash_ts + ".log" print_message(log_file) if not os.path.isdir(tmp_dir): if is_debug: print_message("Tmp dir doesn't exist, creating new one...") try: os.makedirs(tmp_dir) os.chmod(tmp_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) except: tmp_dir = "/tmp" if is_debug: print_message("Using {0} as a temporary dir".format(tmp_dir)) done_all_work_in_the_fork = False # issue75 if done_all_work_in_the_fork: sys.exit(0) # replace text with emojis internal_using_emoji = False # I hate that, but... https://github.com/ableev/Zabbix-in-Telegram/issues/152 if hasattr(conf, "emoji_map"): zbxtg_body_text_emoji_support = [] for l in zbxtg_body_text: l_new = l for k, v in list(conf.emoji_map.items()): l_new = l_new.replace("{{" + k + "}}", v) zbxtg_body_text_emoji_support.append(l_new) if len("".join(zbxtg_body_text)) - len( "".join(zbxtg_body_text_emoji_support)): internal_using_emoji = True zbxtg_body_text = zbxtg_body_text_emoji_support if not is_single_message and tg_method_image is False: text = """%(zbx_subject)s %(zbx_body)s """ % { "zbx_subject": zbx_subject, "zbx_body": zbx_body } room.send_text(text) if settings["zbxtg_image_age"]: age_sec = age2sec(settings["zbxtg_image_age"]) if age_sec > 0 and age_sec > 3600: settings["zbxtg_image_period"] = age_sec message_id = 0 if tg_method_image is True: zbx.login() room.send_text(settings["zbxtg_title"] + "\n" + str(zbxtg_body_text[0])) if not zbx.cookie: text_warn = "Login to Zabbix web UI has failed (web url, user or password are incorrect), "\ "unable to send graphs check manually" room.send_text([text_warn]) print_message(text_warn) else: if not settings["extimg"]: zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"], settings["zbxtg_title"], settings["zbxtg_image_width"], settings["zbxtg_image_height"], version=zbx_version) else: zbxtg_file_img = external_image_get(settings["extimg"], tmp_dir=zbx.tmp_dir) zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200) if not zbxtg_file_img: text_warn = "Can't get graph image, check script manually, see logs, or disable graphs" room.send_text([text_warn]) print_message(text_warn) else: if not is_single_message: zbxtg_body_text = "" else: if is_modified: text_warn = "probably you will see MEDIA_CAPTION_TOO_LONG error, "\ "the message has been cut to 200 symbols, "\ "https://github.com/ableev/Zabbix-in-Telegram/issues/9"\ "#issuecomment-166895044" print_message(text_warn) in_file = open(zbxtg_file_img, "rb") uploaddata = in_file.read( ) # if you only wanted to read 512 bytes, do .read(512) in_file.close() response = client.upload(uploaddata, "image/png") room.send_image(str(response), zbxtg_file_img) os.remove(zbxtg_file_img) if "--show-settings" in args: print_message("Settings: " + str(json.dumps(settings, indent=2)))
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 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 MatrixHandler(object): """Handling matrix connection and bot integration""" __slots__ = [ 'client', 'config', 'db', 'giphy_api_key', 'hostname', 'openweathermap_api_key', 'password', 'stored_msg', 'sync_process', 'uid', 'username', ] def __init__(self, config: Dict[str, str]) -> None: self.config = config self.hostname = config['hostname'] self.username = config['username'] self.password = config['password'] self.uid = config['uid'] self.db = get_db() self.stored_msg = Query() self.giphy_api_key = config['giphy_api_key'] self.openweathermap_api_key = config['openweathermap_api_key'] def on_message(self, room: Room, event: Dict) -> None: """Callback for recieved messages Gets events and checks if something can be triggered. """ logger.debug(event) logger.info('stores msg in db') self.store_msg(event) if event['content'].get('msgtype') == 'm.text' and event['sender'] != \ self.uid: # add config to event event['config'] = self.config # gives event to mossbot and watching out for a return message msg = MOSS.serve(event) if msg and msg.data: if msg.type == 'text': logger.info('sending text msg...') room.send_text(msg.data) elif msg.type == 'notice': logger.info('sending notice msg...') room.send_notice(msg.data) elif msg.type == 'html': logger.info('sending html msg...') room.send_html(msg.data) elif msg.type == 'image': logger.info('sending image msg...') self.write_media('image', room, msg.data) else: logger.error('could not recognize msg type "%s"', msg[0]) elif msg and msg.type == 'skip': logger.info('skipping msg...') else: logger.debug('no matching in event') def on_invite(self, room_id, state): """Callback for recieving invites""" logger.info('got invite for room %s', room_id) self.client.join_room(room_id) def listen_forever(self, timeout_ms: int = 30000) -> None: """Loop to run _sync in a process""" while True: try: # pylint: disable=protected-access self.client._sync(timeout_ms) except BaseException as e: logger.exception('problem with sync: %s', e) time.sleep(10) time.sleep(0.1) def start_listener_process(self, timeout_ms: int = 30000) -> None: """Create sync process.""" self.sync_process = Process( target=self.listen_forever, args=(timeout_ms, ), daemon=True, ) self.sync_process.start() def connect(self) -> None: """Connection handler.""" while True: try: logger.info('create matrix client') self.client = MatrixClient(self.hostname) logger.info('login with password') self.client.login_with_password(self.username, self.password) for room_id in self.client.get_rooms(): logger.info('join room %s', room_id) room = self.client.join_room(room_id) room.add_listener(self.on_message) self.client.add_invite_listener(self.on_invite) self.start_listener_process() start_time = pendulum.now() while True: if pendulum.now() >= start_time.add(minutes=10): logger.info('planed reconnect') self.sync_process.terminate() break time.sleep(0.1) except KeyboardInterrupt: logger.info('GoodBye') self.sync_process.terminate() sys.exit() except BaseException as e: logger.exception('problem while try to connect: %s', e) time.sleep(10) def write_media(self, media_type: str, room: Room, url: str) -> None: """Get media, upload it and post to room """ # image is the only media type supported right now if media_type != 'image': logger.error('%s as media type is not supported', media_type) return None # getting image and analyze it logger.info('download %s', url) image_data = get_image(url) logger.debug('got image_data: %s', image_data) if not image_data: logger.error('got no image_data') return # analyze image file and create image info dict media_info = {} # type: Dict[str, Union[str, int, BytesIO, None]] # getting mimetype media_info['mimetype'] = image_data.get('content-type') if not media_info['mimetype']: media_info['mimetype'] = mimetypes.guess_type(url)[0] # getting name name = urlsplit(url).path.split('/')[-1] # image size media_info['h'] = image_data.get('height') media_info['w'] = image_data.get('width') logger.debug('media_info content: %s', media_info) # upload it to homeserver logger.info('upload file') uploaded = self.client.upload(image_data['image'], media_info['mimetype']) logger.debug('upload: %s', uploaded) # send image to room logger.info('send media: %s', name) room.send_image(uploaded, name, **media_info) def store_msg(self, event: Dict) -> None: """Store msgs in a db""" logger.debug('got event to store: %s', str(event)) try: msgs_table = self.db.table('msgs') if event['content']['msgtype'] == 'm.text': all_sender_msgs = msgs_table.search( self.stored_msg.sender == event['sender']) while len(all_sender_msgs) >= 10: msgs_table.remove(doc_ids=[all_sender_msgs[0].doc_id]) all_sender_msgs = msgs_table.search( self.stored_msg.sender == event['sender']) msgs_table.insert({ 'sender': event['sender'], 'body': event['content']['body'], }) except BaseException: logger.exception('could not store msg') return None
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 MatrixBot: def connect(self): try: logger.info("connecting to {}".format(self.server)) self.client = MatrixClient(self.server) self.client.login_with_password(self.username, self.password) logger.info("connection established") # Store empty list of handlers self.handlers = [] # listen for invites and automatically accept them self.client.add_invite_listener(self.handle_invite) self.rooms = [] # Add all rooms we're currently in to self.rooms and add their callbacks for room_id, room in self.client.get_rooms().items(): room.add_listener(self.handle_message) self.rooms.append(room_id) except Exception: logger.warning("connection to {} failed".format(self.server) + ", retrying in 5 seconds...") sleep(5) self.connect() # username - Matrix username # password - Matrix password # server - Matrix server url : port # rooms - List of rooms ids to operate in, or None to accept all rooms def __init__(self, username, password, server, rooms=None): self.username = username self.password = password self.server = server self.rooms = rooms self.connect() def add_handler(self, handler): self.handlers.append(handler) def handle_message(self, room, event): # Make sure we didn't send this message if re.match("@" + self.username, event['sender']): return try: if event['type'] == "m.room.message": rx_msg = event['content']['body'] if re.search(r"#book", rx_msg): book_url = self.get_book_url(rx_msg) print('found book: ' + book_url) if (book_url): img_info = self.get_book_img(book_url) img = requests.get(img_info['url']) mxc_url = self.client.upload(img.content, 'image/jpeg') #html = "<a href='"+book_url+"'><img src='"+mxc_url+"'/></a>" #print(html) room.send_image(mxc_url, img_info['title']) room.send_text(book_url) except (TypeError): pass def handle_invite(self, room_id, state): print("Got invite to room: " + str(room_id)) print("Joining...") room = self.client.join_room(room_id) # Add message callback for this room room.add_listener(self.handle_message) print("Joined!") # Add room to list self.rooms.append(room) def get_book_url(self, search_string): search_words = search_string.split() filtered_search_words = filter(lambda w: not re.search(r"#book", w), search_words) query = '+'.join(filtered_search_words) page = requests.get("https://www.goodreads.com/search?q=" + query) soup = BeautifulSoup(page.content, 'html.parser') results = soup.find_all('a', class_='bookTitle') if (len(results) == 0): return None url = 'https://goodreads.com' + results[0]['href'].split('-')[0].split( '.')[0] return url def get_book_img(self, book_url): page = requests.get(book_url) soup = BeautifulSoup(page.content, 'html.parser') img_tag = soup.find("img", {"id": "coverImage"}) h1_tag = soup.find("h1", {"id": "bookTitle"}) return {'title': h1_tag.text, 'url': img_tag['src']} def start_polling(self): # Starts polling for messages self.client.start_listener_thread( exception_handler=lambda e: self.connect()) return self.client.sync_thread
class Matrix: _bridge = {} _slack = None _cache = {} _cache_file = 'matrix_cache.json' def __init__(self, user_id: str, http_matrix_server: str, token: str) -> None: super().__init__() self.user_id = user_id self._client = MatrixClient(http_matrix_server, token=token, user_id=user_id) self.__load_cache() def __load_cache(self): if os.path.isfile(self._cache_file): with open(self._cache_file, 'r') as jsf: self._cache: json = json.load(jsf) if 'rooms' not in self._cache: self._cache['rooms'] = {} if 'uploaded_avatars' not in self._cache: self._cache['uploaded_avatars'] = {} def __save_cache(self): with open(self._cache_file, 'w') as file: file.write(json.dumps(self._cache)) def set_slack(self, slack): self._slack = slack def bridge_slack_room(self, matrix_room_id, slack_room_id): self._bridge[matrix_room_id] = slack_room_id def send_message(self, room_id: str, text: str, name: str = None, avatar_url: str = None, file_url: str = None, file_name: str = None, file_mimetype: str = None, file_authorization: str = None): room = Room(self._client, room_id) current_avatar_url = None current_name = None avatar_uri = None if room_id in self._cache['rooms']: current_name = self._cache['rooms'][room_id]['name'] current_avatar_url = self._cache['rooms'][room_id]['avatar_url'] else: self._cache['rooms'][room_id] = {} if avatar_url is not None and avatar_url != current_avatar_url: if avatar_url in self._cache['uploaded_avatars']: avatar_uri = self._cache['uploaded_avatars'][avatar_url] print("Use cache avatar for an user " + avatar_uri + " (" + avatar_url + ")") else: avatar_content = request.urlopen(avatar_url).read() avatar_uri = self._client.upload(avatar_content, 'image/png') self._cache['uploaded_avatars'][avatar_url] = avatar_uri print("Uploaded a new avatar for an user " + avatar_uri + " (" + avatar_url + ")") if (name is not None and name is not current_name) or avatar_uri is not None: room.set_user_profile(displayname=name, avatar_url=avatar_uri) self._cache['rooms'][room_id]['name'] = name self._cache['rooms'][room_id]['avatar_url'] = avatar_url self.__save_cache() if file_url is not None and file_mimetype is not None and file_name is not None: rq = Request(file_url) rq.add_header('Authorization', file_authorization) file_content = urlopen(rq).read() file_uri = self._client.upload(file_content, file_mimetype) if file_mimetype in ['image/png', 'image/jpeg']: room.send_image(file_uri, file_name) else: room.send_file(file_uri, file_name) if text is not None: room.send_text(text) def start_listening(self): self._client.add_listener(self.__on_event) # room.add_listener(on_message) # client.add_listener(on_message) self._client.start_listener_thread() def __on_event(self, event): print(event) if event['type'] == "m.room.message" and event[ 'sender'] != self.user_id and event['room_id'] in self._bridge: if event['content']['msgtype'] == "m.text": self._slack.send_message(self._bridge[event['room_id']], event['content']['body']) if event['content']['msgtype'] in ["m.image", 'm.file']: image_url = self._client.api.get_download_url( event['content']['url']) self._slack.send_file(self._bridge[event['room_id']], file_url=image_url, file_title=event['content']['body'])