diff --git a/xmpp_tg/xmpp.py b/xmpp_tg/xmpp.py index 2498516..2a54fa9 100644 --- a/xmpp_tg/xmpp.py +++ b/xmpp_tg/xmpp.py @@ -1,5 +1,6 @@ import re, sys, os, io, sqlite3, hashlib, time, datetime import xml.etree.ElementTree as ET +import logging, traceback from sleekxmpp.componentxmpp import ComponentXMPP from sleekxmpp import Presence, Message @@ -261,17 +262,277 @@ class XMPPTelegram(ComponentXMPP): self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='unavailable') sys.exit(0) - def process_command(self, iq): + class MessageHandler(): + _unknown_command_handler = lambda self: "Unknown command, for a list send '!help'" + _on_connect = lambda: None + + def __init__(self, msg): + self._command = msg["body"].split(" ")[0][1:] + self._handler = getattr(self, self._command, self._unknown_command_handler) + self.type = "groupchat" if msg["type"] == "groupchat" else "chat" + self.sender = msg["from"] + self.jid = msg["from"].bare + self.replyto = self.sender.full if self.type == "chat" else self.sender.bare + self.arguments = msg["body"].split(" ")[1:] + self.msg = msg + + def _update(self, text): + xmpp.send_message(mto=self.replyto, mtype=self.type, mbody=text) + + def debug(self, *args, **kwargs): + """Show debug info""" + return pprint.pformat(self.__dict__) + + def help(self, *args, **kwargs): + """List available commands""" + #taken from https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation + def trim(docstring): + if not docstring: + return '' + # Convert tabs to spaces (following the normal Python rules) + # and split into a list of lines: + lines = docstring.expandtabs().splitlines() + # Determine minimum indentation (first line doesn't count): + indent = 500 + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < 500: + for line in lines[1:]: + trimmed.append(line[indent:].rstrip()) + # Strip off trailing and leading blank lines: + while trimmed and not trimmed[-1]: + trimmed.pop() + while trimmed and not trimmed[0]: + trimmed.pop(0) + # Return a single string: + return '\n'.join(trimmed) + + if len(self.arguments) == 0: + methods = [func for func in dir(self) if not func.startswith("_") and callable(getattr(self, func))] + reply = "Available commands:" + for method in methods: + docstring = getattr(self, method).__doc__ + if docstring is None: + docstring = "No description available" + reply += "\n"+method+" ("+docstring.split("\n")[0]+")" + return reply + else: + method = getattr(self,self.arguments[0]) + reply = trim(method.__doc__) + return reply + + class GateMessageHandler(MessageHandler): + def configure(hndl, self): + """Get config/set config options""" + config_exclude = ['jid', 'tg_phone'] + + option = hndl.arguments[0] if len(hndl.arguments) >= 1 else None + value = hndl.arguments[1] if len(hndl.arguments) == 2 else None + + if value is not None and option not in config_exclude: + self.db_connection.execute("update accounts set {} = ? where jid = ?".format(option), (value,hndl.jid,) ) + self.accounts[hndl.jid] = self.db_connection.execute("SELECT * FROM accounts where jid = ?", (hndl.jid,) ).fetchone() + + message = "=== Your current configuration ===\n\n" + for param, value in self.accounts[hndl.jid].items(): + message = message + "<%s>: %s" % (param, value) + "\n" + message = message + "\nTo modify some option, please, send !configure param value" + return message + + def login(hndl, self): #1 arg + """Initiates Telegram session + + Usage: !login """ + if len(hndl.arguments) != 1: + return "Wrong number of arguments" + + phone_no = arguments[0] + + hndl._update("Please wait...") + self.spawn_tg_client(hndl.jid, phone_no) + + if self.tg_connections[hndl.jid].is_user_authorized(): + self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected') + return "You are already authenticated in Telegram" + else: + # remove old sessions for this JID # + self.db_connection.execute("DELETE from accounts where jid = ?", (hndl.jid, ) ) + self.tg_connections[hndl.jid].send_code_request(phone_no) + self._update('Gate is connected. Telegram should send SMS message to you.') + return 'Please enter one-time code via !code 12345.' + + def code(hndl, self): + + code = hndl.arguments[0] + jid = hndl.jid + + if not self.tg_connections[jid].is_user_authorized(): + try: + hndl._update('Trying authenticate...') + self.tg_connections[jid].sign_in(self.tg_phones[jid], code) + except SessionPasswordNeededError: + self.gate_reply_message(iq, 'Two-factor authentication detected.') + self.gate_reply_message(iq, 'Please enter your password via !password abc123.') + return + + if self.tg_connections[jid].is_user_authorized(): + self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected') + self.gate_reply_message(iq, 'Authentication successful. Initiating Telegram...') + self.db_connection.execute("INSERT INTO accounts(jid, tg_phone) VALUES(?, ?)", (jid, self.tg_phones[jid],)) + self.accounts[jid] = self.db_connection.execute("SELECT * FROM accounts where jid = ?", (jid,) ).fetchone() + self.init_tg(jid) + + else: + return 'Authentication failed.' + else: + return 'You are already authenticated. Please use !logout before new login.' + + def password(hndl, self): + password = hndl.arguments[1] + jid = self.jid + + if not self.tg_connections[jid].is_user_authorized(): + self.gate_reply_message(iq, 'Checking password...') + self.tg_connections[jid].sign_in(password=password) + + if self.tg_connections[jid].is_user_authorized(): + self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected') + self.gate_reply_message(iq, 'Authentication successful. Initiating Telegram...') + self.db_connection.execute("INSERT INTO accounts(jid, tg_phone) VALUES(?, ?)", (jid, self.tg_phones[jid],)) + self.accounts[jid] = self.db_connection.execute("SELECT * FROM accounts where jid = ?", (jid,) ).fetchone() + self.init_tg(jid) + + else: + self.gate_reply_message(iq, 'Authentication failed.') + else: + self.gate_reply_message(iq, 'You are already authenticated. Please use !logout before new login.') + + def list_sessions(hndl, self): + if not self.tg_connections[hndl.jid].is_user_authorized(): + return "Error" + + sessions = self.tg_connections[hndl.jid].invoke(GetAuthorizationsRequest()) + return str(sessions) + + def reload_dialogs(hndl, self): + if not self.tg_connections[hndl.jid].is_user_authorized(): + return "Error" + + self.tg_process_dialogs(hndl.jid) + return "Dialogs reloadad" + + def logout(hndl, self): + self.tg_connections[hndl.jid].log_out() + self.db_connection.execute("DELETE FROM accounts WHERE jid = ?", (hndl.jid,)) + return 'Your Telegram session was deleted' + + def add(self): #1 arg + result = self.tg_connections[jid].get_entity(parsed[1]) + if type(result) == User: + tg_peer = InputPeerUser( result.id, result.access_hash ) + result = self.tg_connections[jid].invoke( SendMessageRequest(tg_peer, 'Hello! I just want to add you in my contact list.', generate_random_long() ) ) + elif type(result) == Channel: + tg_peer = InputPeerChannel( result.id, result.access_hash ) + self.tg_connections[jid].invoke(JoinChannelRequest( InputPeerChannel(result.id, result.access_hash) ) ) + else: + self.gate_reply_message(iq, 'Sorry, nothing found.') + return + + self.tg_process_dialogs(jid) + + def join(self): #1 arg + link = parsed[1].split('/') # https://t.me/joinchat/HrCmckx_SkMbSGFLhXCvSg + self.tg_connections[jid].invoke(ImportChatInviteRequest(link[4])) + time.sleep(1) + self.tg_process_dialogs(jid) + + def group(self): #2 args + # group name? # + groupname = parsed[1] + + # group users? # + groupuser = self.tg_connections[jid].get_entity(parsed[2]) + + # we re ready to make group + self.tg_connections[jid].invoke(CreateChatRequest([groupuser], groupname)) + self.tg_process_dialogs(jid) + + def channel(self): #1 arg + groupname = parsed[1] + self.tg_connections[jid].invoke(CreateChannelRequest(groupname, groupname, broadcast = True)) + self.tg_process_dialogs(jid) + + def supergroup(self): #1 arg + groupname = parsed[1] + self.tg_connections[jid].invoke(CreateChannelRequest(groupname, groupname, megagroup = True)) + self.tg_process_dialogs(jid) + + def username(self): #1 arg + username = parsed[1] + self.tg_connections[jid].invoke(UpdateUsernameRequest(username)) + + def name(self): #1 or 2 args + firstname = parsed[1] + lastname = parsed[2] if len(parsed) > 2 else None + self.tg_connections[jid].invoke(UpdateProfileRequest(first_name = firstname, last_name = lastname)) + + def about(self): #>0 args + about = iq['body'][7:] + self.tg_connections[jid].invoke(UpdateProfileRequest(about = about)) + + def import_contact(self): #2 args + phone = parsed[1] + firstname = parsed[2] + lastname = parsed[3] if len(parsed) > 3 else None + + contact = InputPhoneContact(client_id=generate_random_long(), phone=phone, first_name=firstname, last_name=lastname) + self.tg_connections[jid].invoke(ImportContactsRequest([contact])) + self.tg_process_dialogs(jid) + + def roster(hndl, self): + response = "Telegram chats:\n" + for jid,tid in self.contact_list[hndl.jid].items(): + response += "{}: {}\n".format(tid, jid) + return response + + + def process_command(self, msg): """ Commands to gateway, users or chats (starts with !) :param iq: :return: """ - parsed = iq['body'].split(' ') - jid = iq['from'].bare + logging.info("received message "+str(msg["body"])+" from "+str(msg["from"])) + is_command = msg["body"].startswith("!") and msg["body"][1] != "_" + + if is_command: + command = msg["body"].split(" ")[0][1:] + handler = self.GateMessageHandler(msg)._handler + try: + reply = str(handler(self)) + except Exception as e: + if self.config["debug"]: + reply = traceback.format_exc() + else: + if isinstance(e, NotAuthorizedError): + reply = str(e) + else: + logging.error("Exception in command from {}, command was '{}'".format(msg["from"],msg["body"])) + traceback.print_exc() + reply = "Internal error, please contact Sysadmin" + if reply is not None: + self.gate_reply_message(msg, reply) + #msg.reply(reply).send() + + parsed = msg['body'].split(' ') + jid = msg['from'].bare if parsed[0] == '!help': - self.gate_reply_message(iq, '=== Available gateway commands ===:\n\n' + self.gate_reply_message(msg, '=== Available gateway commands ===:\n\n' '!help - Displays this text\n' '!login +123456789 - Initiates Telegram session\n' @@ -299,149 +560,6 @@ class XMPPTelegram(ComponentXMPP): '!roster - Lists yout TG roster\n' ) - elif parsed[0] == '!configure': - config_exclude = ['jid', 'tg_phone'] - if len(parsed) > 2 and parsed[1] not in config_exclude: - self.db_connection.execute("update accounts set {} = ? where jid = ?".format(parsed[1]), (parsed[2],jid,) ) - self.accounts[jid] = self.db_connection.execute("SELECT * FROM accounts where jid = ?", (jid,) ).fetchone() - - message = "=== Your current configuration ===\n\n" - for param, value in self.accounts[jid].items(): - message = message + "<%s>: %s" % (param, value) + "\n" - message = message + "\nTo modify some option, please, send !configure param value" - self.gate_reply_message(iq, message) - - - elif parsed[0] == '!login': # -------------------------------------------------- - self.gate_reply_message(iq, 'Please wait...') - self.spawn_tg_client(jid, parsed[1]) - - if self.tg_connections[jid].is_user_authorized(): - self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected') - self.gate_reply_message(iq, 'You are already authenticated in Telegram.') - else: - # remove old sessions for this JID # - self.db_connection.execute("DELETE from accounts where jid = ?", (jid, ) ) - self.tg_connections[jid].send_code_request(parsed[1]) - self.gate_reply_message(iq, 'Gate is connected. Telegram should send SMS message to you.') - self.gate_reply_message(iq, 'Please enter one-time code via !code 12345.') - - elif parsed[0] in ['!code', '!password']: # -------------------------------------------------- - if not self.tg_connections[jid].is_user_authorized(): - if parsed[0] == '!code': - try: - self.gate_reply_message(iq, 'Trying authenticate...') - self.tg_connections[jid].sign_in(self.tg_phones[jid], parsed[1]) - except SessionPasswordNeededError: - self.gate_reply_message(iq, 'Two-factor authentication detected.') - self.gate_reply_message(iq, 'Please enter your password via !password abc123.') - return - - if parsed[0] == '!password': - self.gate_reply_message(iq, 'Checking password...') - self.tg_connections[jid].sign_in(password=parsed[1]) - - if self.tg_connections[jid].is_user_authorized(): - self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected') - self.gate_reply_message(iq, 'Authentication successful. Initiating Telegram...') - self.db_connection.execute("INSERT INTO accounts(jid, tg_phone) VALUES(?, ?)", (jid, self.tg_phones[jid],)) - self.accounts[jid] = self.db_connection.execute("SELECT * FROM accounts where jid = ?", (jid,) ).fetchone() - self.init_tg(jid) - - else: - self.gate_reply_message(iq, 'Authentication failed.') - else: - self.gate_reply_message(iq, 'You are already authenticated. Please use !logout before new login.') - - elif parsed[0] == '!list_sessions': # -------------------------------------------------- - if not self.tg_connections[jid].is_user_authorized(): - self.gate_reply_message(iq, 'Error.') - return - - sessions = self.tg_connections[jid].invoke(GetAuthorizationsRequest()) - elif parsed[0] == '!reload_dialogs': - if not self.tg_connections[jid].is_user_authorized(): - self.gate_reply_message(iq, 'Error.') - return - self.tg_process_dialogs(jid) - self.gate_reply_message(iq, 'Dialogs reloaded.') - - elif parsed[0] == '!logout': # -------------------------------------------------- - self.tg_connections[jid].log_out() - self.db_connection.execute("DELETE FROM accounts WHERE jid = ?", (jid,)) - self.gate_reply_message(iq, 'Your Telegram session was deleted') - - elif parsed[0] == '!add': # add user - result = self.tg_connections[jid].get_entity(parsed[1]) - if type(result) == User: - tg_peer = InputPeerUser( result.id, result.access_hash ) - result = self.tg_connections[jid].invoke( SendMessageRequest(tg_peer, 'Hello! I just want to add you in my contact list.', generate_random_long() ) ) - elif type(result) == Channel: - tg_peer = InputPeerChannel( result.id, result.access_hash ) - self.tg_connections[jid].invoke(JoinChannelRequest( InputPeerChannel(result.id, result.access_hash) ) ) - else: - self.gate_reply_message(iq, 'Sorry, nothing found.') - return - - self.tg_process_dialogs(jid) - - elif parsed[0] == '!join': # join chat by link - link = parsed[1].split('/') # https://t.me/joinchat/HrCmckx_SkMbSGFLhXCvSg - self.tg_connections[jid].invoke(ImportChatInviteRequest(link[4])) - time.sleep(1) - self.tg_process_dialogs(jid) - - elif parsed[0] == '!group' and len(parsed) >= 3: # create new group - # group name? # - groupname = parsed[1] - - # group users? # - groupuser = self.tg_connections[jid].get_entity(parsed[2]) - - # we re ready to make group - self.tg_connections[jid].invoke(CreateChatRequest([groupuser], groupname)) - self.tg_process_dialogs(jid) - - elif parsed[0] == '!channel' and len(parsed) >= 2: - groupname = parsed[1] - self.tg_connections[jid].invoke(CreateChannelRequest(groupname, groupname, broadcast = True)) - self.tg_process_dialogs(jid) - - elif parsed[0] == '!supergroup' and len(parsed) >= 2: - groupname = parsed[1] - self.tg_connections[jid].invoke(CreateChannelRequest(groupname, groupname, megagroup = True)) - self.tg_process_dialogs(jid) - - elif parsed[0] == '!username' and len(parsed) >= 2: - username = parsed[1] - self.tg_connections[jid].invoke(UpdateUsernameRequest(username)) - - elif parsed[0] == '!name' and len(parsed) >= 2: - firstname = parsed[1] - lastname = parsed[2] if len(parsed) > 2 else None - self.tg_connections[jid].invoke(UpdateProfileRequest(first_name = firstname, last_name = lastname)) - - elif parsed[0] == '!about' and len(parsed) >= 2: - about = iq['body'][7:] - self.tg_connections[jid].invoke(UpdateProfileRequest(about = about)) - - elif parsed[0] == '!import' and len(parsed) >= 3: - phone = parsed[1] - firstname = parsed[2] - lastname = parsed[3] if len(parsed) > 3 else None - - contact = InputPhoneContact(client_id=generate_random_long(), phone=phone, first_name=firstname, last_name=lastname) - self.tg_connections[jid].invoke(ImportContactsRequest([contact])) - self.tg_process_dialogs(jid) - - elif parsed[0] == '!roster': # create new channel - response = "Telegram chats:\n" - for jid,tid in self.contact_list[jid].items(): - response += "{}: {}\n".format(tid, jid) - self.gate_reply_message(iq, response) - - else: # -------------------------------------------------- - self.gate_reply_message(iq, 'Unknown command. Try !help for list all commands.') def process_chat_user_command(self, iq): parsed = iq['body'].split(' ')