This commit is contained in:
2019-02-27 01:05:13 +01:00
parent 95060e9cfb
commit 2b659e895f

View File

@@ -99,242 +99,6 @@ class MessageHandler():
reply = trim(method.__doc__) reply = trim(method.__doc__)
return reply return reply
class XMPPTelegram(ComponentXMPP):
"""
Main XMPPTelegram class.
"""
def __init__(self, config_dict):
"""
Transport initialization
:param config_dict:
"""
ComponentXMPP.__init__(self, config_dict['jid'], config_dict['secret'], config_dict['server'],
config_dict['port'])
self.auto_authorize = True
# self.auto_subscribe = True
self.config = config_dict
self.accounts = dict() # personal configuration per JID
self.tg_connections = dict()
self.tg_phones = dict()
self.tg_dialogs = dict()
self.contact_list = dict()
self.db_connection = self.init_database()
self.register_plugin('xep_0030') # Service discovery
self.register_plugin('xep_0054') # VCard-temp
self.register_plugin('xep_0172') # NickNames
self.add_event_handler('message', self.message)
self.add_event_handler('presence_unsubscribe', self.event_presence_unsub)
self.add_event_handler('presence_unsubscribed', self.event_presence_unsub)
self.add_event_handler('presence', self.event_presence)
self.add_event_handler('got_online', self.handle_online)
self.add_event_handler('got_offline', self.handle_offline)
self.add_event_handler('session_start', self.handle_start)
self.plugin['xep_0030'].add_identity(
category='gateway',
itype='telegram',
name=self.config['title'],
node=self.boundjid.node,
jid=self.boundjid.bare,
lang='no'
)
vcard = self.plugin['xep_0054'].make_vcard()
vcard['FN'] = self.config['title']
vcard['DESC'] = 'Send !help for information'
self.plugin['xep_0054'].publish_vcard(jid=self.boundjid.bare, vcard=vcard)
def __del__(self):
"""
Destructor
:return:
"""
self.db_connection.close()
def handle_start(self, arg):
"""
Successful connection to Jabber server
:param arg:
:return:
"""
users = self.db_connection.execute("SELECT * FROM accounts").fetchall()
for usr in users:
self.accounts[usr['jid']] = usr
self.send_presence(pto=usr['jid'], pfrom=self.boundjid.bare, ptype='probe')
def message(self, iq):
"""
Message from XMPP
:param iq:
:return:
"""
jid = iq['from'].bare
if iq['to'] == self.config['jid'] and iq['type'] == 'chat': # message to gateway
if iq['body'].startswith('!'):
self.process_command(iq)
else:
self.gate_reply_message(iq, 'Only commands accepted. Try !help for more info.')
elif iq['type'] == 'chat': # --- outgoing message ---
if jid in self.tg_connections and self.tg_connections[jid].is_user_authorized():
if iq['body'].startswith('!'): # it is command!
if iq['to'].bare.startswith( ('u', 'b') ):
self.process_chat_user_command(iq)
elif iq['to'].bare.startswith('g') or iq['to'].bare.startswith('s') or iq['to'].bare.startswith('c'):
self.process_chat_group_command(iq)
else:
self.gate_reply_message(iq, 'Error.')
else: # -- normal message --
try:
tg_id = int(iq['to'].node[1:])
except ValueError:
self.gate_reply_message(iq, 'Invalid JID')
tg_peer = None
msg = iq['body']
reply_mid = None
if msg.startswith('>'): # quoting check
msg_lines = msg.split('\n')
matched = re.match(r'>[ ]*(?P<mid>[\d]+)[ ]*', msg_lines[0]) #TODO: check regex
matched = matched.groupdict() if matched else {}
if 'mid' in matched: # citation
reply_mid = int(matched['mid'])
msg = '\n'.join(msg_lines[1:])
if iq['to'].bare.startswith( ('u', 'b') ): # normal user
tg_peer = InputPeerUser(tg_id, self.tg_dialogs[jid]['users'][tg_id].access_hash)
elif iq['to'].bare.startswith('g'): # generic group
tg_peer = InputPeerChat(tg_id)
elif iq['to'].bare.startswith( ('s', 'c') ): # supergroup
tg_peer = InputPeerChannel(tg_id, self.tg_dialogs[jid]['supergroups'][tg_id].access_hash)
# peer OK.
if tg_peer:
result = None
# detect media
if msg.startswith('http') and re.match(r'(?:http\:|https\:)?\/\/.*\.(?:' + self.config['media_external_formats'] + ')', msg):
urls = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:' + self.config['media_external_formats'] + ')', msg)
message = msg.replace(urls[0], '')
media = InputMediaPhotoExternal(urls[0], "Image")
try:
result = self.tg_connections[jid].invoke(SendMediaRequest(tg_peer, media, message, random_id = generate_random_long(), reply_to_msg_id = reply_mid))
except Exception:
print('Media upload failed.')
# media send failed. #
if not result:
result = self.tg_connections[jid].invoke(SendMessageRequest(tg_peer, msg, generate_random_long(), reply_to_msg_id=reply_mid))
# find sent message id and save it
if result and hasattr(result, 'id'): # update id
msg_id = result.id
self.tg_dialogs[jid]['messages'][tg_id] = {'id': msg_id, 'body': msg}
#self.send_message(mto=iq['from'], mfrom=iq['to'], mtype='chat', mbody='[Your MID:{}]'.format(msg_id))
def event_presence_unsub(self, presence):
return
def event_presence(self, presence):
"""
Presence handler
:param presence:
:return:
"""
ptype = presence['type']
# handle "online" to transport:
if ptype == 'available' and presence['to'].bare == self.boundjid.bare:
self.handle_online(presence, False) # handle online
elif ptype == 'subscribe':
self.send_presence(pto=presence['from'].bare, pfrom=presence['to'].bare, ptype='subscribed')
elif ptype == 'subscribed':
pass
elif ptype == 'unsubscribe':
pass
elif ptype == 'unsubscribed':
pass
elif ptype == 'probe':
self.send_presence(pto=presence['from'], pfrom=presence['to'], ptype='available')
elif ptype == 'unavailable':
pass
else:
# self.send_presence(pto=presence['from'], pfrom=presence['to'])
pass
def handle_online(self, event, sync_roster = True):
"""
Gateway's subscriber comes online
:param event:
:return:
"""
jid = event['from'].bare
to = event['to'].bare
# maybe if i'll ignore it — it will go ahead
if to != self.boundjid.bare:
return
if jid not in self.tg_connections:
result = self.db_connection.execute("SELECT * FROM accounts WHERE jid = ?", (jid,)).fetchone()
if result is not None:
self.spawn_tg_client(jid, result['tg_phone'])
else:
if not (self.tg_connections[jid].is_connected()):
self.tg_connections[jid].connect()
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=False))
self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected')
self.tg_process_dialogs(jid, sync_roster) # do not sync roster if we already have connection!
def handle_offline(self, event):
"""
Gateway's subscriber comes offline.
:param event:
:return:
"""
jid = event['from'].bare
# keep telegram online ?
if self.accounts[jid]['keep_online']:
return
if jid in self.tg_connections:
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=True))
self.tg_connections[jid].disconnect()
def handle_interrupt(self, signal, frame):
"""
Interrupted (Ctrl+C).
:param event:
:return:
"""
for jid in self.tg_connections:
print('Disconnecting: %s' % jid)
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=True))
self.tg_connections[jid].disconnect()
for contact_jid, contact_nickname in self.contact_list[jid].items():
self.send_presence(pto=jid, pfrom=contact_jid, ptype='unavailable')
self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='unavailable')
sys.exit(0)
class GateMessageHandler(MessageHandler): class GateMessageHandler(MessageHandler):
def configure(hndl, self): def configure(hndl, self):
"""Get config/set config options""" """Get config/set config options"""
@@ -571,39 +335,6 @@ class XMPPTelegram(ComponentXMPP):
'!roster - Lists yout TG roster\n') '!roster - Lists yout TG roster\n')
def process_command(self, msg):
"""
Commands to gateway, users or chats (starts with !)
:param iq:
:return:
"""
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 = "******* DEBUG MODE ACTIVE *********\n"
reply += "An Exception occured while executing this command:"
reply += traceback.format_exc()
else:
if isinstance(e, NotAuthorizedError):
reply = str(e)
elif isinstance(e, self.MessageHandler.WrongNumberOfArgsError):
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()
class ChatCommandHandler(MessageHandler): class ChatCommandHandler(MessageHandler):
def __init__(self, msg): def __init__(self, msg):
@@ -647,6 +378,272 @@ class XMPPTelegram(ComponentXMPP):
del(self.tg_dialogs[hndl.jid]['messages'][hndl.tg_id]) del(self.tg_dialogs[hndl.jid]['messages'][hndl.tg_id])
self.tg_connections[hndl.jid].invoke( DeleteMessagesRequest([msg_id], revoke = True) ) self.tg_connections[hndl.jid].invoke( DeleteMessagesRequest([msg_id], revoke = True) )
class XMPPTelegram(ComponentXMPP):
"""
Main XMPPTelegram class.
"""
def __init__(self, config_dict):
"""
Transport initialization
:param config_dict:
"""
ComponentXMPP.__init__(self, config_dict['jid'], config_dict['secret'], config_dict['server'],
config_dict['port'])
self.auto_authorize = True
# self.auto_subscribe = True
self.config = config_dict
self.accounts = dict() # personal configuration per JID
self.tg_connections = dict()
self.tg_phones = dict()
self.tg_dialogs = dict()
self.contact_list = dict()
self.db_connection = self.init_database()
self.register_plugin('xep_0030') # Service discovery
self.register_plugin('xep_0054') # VCard-temp
self.register_plugin('xep_0172') # NickNames
self.add_event_handler('message', self.message)
self.add_event_handler('presence_unsubscribe', self.event_presence_unsub)
self.add_event_handler('presence_unsubscribed', self.event_presence_unsub)
self.add_event_handler('presence', self.event_presence)
self.add_event_handler('got_online', self.handle_online)
self.add_event_handler('got_offline', self.handle_offline)
self.add_event_handler('session_start', self.handle_start)
self.plugin['xep_0030'].add_identity(
category='gateway',
itype='telegram',
name=self.config['title'],
node=self.boundjid.node,
jid=self.boundjid.bare,
lang='no'
)
vcard = self.plugin['xep_0054'].make_vcard()
vcard['FN'] = self.config['title']
vcard['DESC'] = 'Send !help for information'
self.plugin['xep_0054'].publish_vcard(jid=self.boundjid.bare, vcard=vcard)
def __del__(self):
"""
Destructor
:return:
"""
self.db_connection.close()
def handle_start(self, arg):
"""
Successful connection to Jabber server
:param arg:
:return:
"""
users = self.db_connection.execute("SELECT * FROM accounts").fetchall()
for usr in users:
self.accounts[usr['jid']] = usr
self.send_presence(pto=usr['jid'], pfrom=self.boundjid.bare, ptype='probe')
def message(self, iq):
"""
Message from XMPP
:param iq:
:return:
"""
jid = iq['from'].bare
if iq['to'] == self.config['jid'] and iq['type'] == 'chat': # message to gateway
if iq['body'].startswith('!'):
self.process_command(iq)
else:
self.gate_reply_message(iq, 'Only commands accepted. Try !help for more info.')
elif iq['type'] == 'chat': # --- outgoing message ---
if jid in self.tg_connections and self.tg_connections[jid].is_user_authorized():
if iq['body'].startswith('!'): # it is command!
if iq['to'].bare.startswith( ('u', 'b') ):
self.process_chat_user_command(iq)
elif iq['to'].bare.startswith('g') or iq['to'].bare.startswith('s') or iq['to'].bare.startswith('c'):
self.process_chat_group_command(iq)
else:
self.gate_reply_message(iq, 'Error.')
else: # -- normal message --
try:
tg_id = int(iq['to'].node[1:])
except ValueError:
self.gate_reply_message(iq, 'Invalid JID')
tg_peer = None
msg = iq['body']
reply_mid = None
if msg.startswith('>'): # quoting check
msg_lines = msg.split('\n')
matched = re.match(r'>[ ]*(?P<mid>[\d]+)[ ]*', msg_lines[0]) #TODO: check regex
matched = matched.groupdict() if matched else {}
if 'mid' in matched: # citation
reply_mid = int(matched['mid'])
msg = '\n'.join(msg_lines[1:])
if iq['to'].bare.startswith( ('u', 'b') ): # normal user
tg_peer = InputPeerUser(tg_id, self.tg_dialogs[jid]['users'][tg_id].access_hash)
elif iq['to'].bare.startswith('g'): # generic group
tg_peer = InputPeerChat(tg_id)
elif iq['to'].bare.startswith( ('s', 'c') ): # supergroup
tg_peer = InputPeerChannel(tg_id, self.tg_dialogs[jid]['supergroups'][tg_id].access_hash)
# peer OK.
if tg_peer:
result = None
# detect media
if msg.startswith('http') and re.match(r'(?:http\:|https\:)?\/\/.*\.(?:' + self.config['media_external_formats'] + ')', msg):
urls = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:' + self.config['media_external_formats'] + ')', msg)
message = msg.replace(urls[0], '')
media = InputMediaPhotoExternal(urls[0], "Image")
try:
result = self.tg_connections[jid].invoke(SendMediaRequest(tg_peer, media, message, random_id = generate_random_long(), reply_to_msg_id = reply_mid))
except Exception:
print('Media upload failed.')
# media send failed. #
if not result:
result = self.tg_connections[jid].invoke(SendMessageRequest(tg_peer, msg, generate_random_long(), reply_to_msg_id=reply_mid))
# find sent message id and save it
if result and hasattr(result, 'id'): # update id
msg_id = result.id
self.tg_dialogs[jid]['messages'][tg_id] = {'id': msg_id, 'body': msg}
#self.send_message(mto=iq['from'], mfrom=iq['to'], mtype='chat', mbody='[Your MID:{}]'.format(msg_id))
def event_presence_unsub(self, presence):
return
def event_presence(self, presence):
"""
Presence handler
:param presence:
:return:
"""
ptype = presence['type']
# handle "online" to transport:
if ptype == 'available' and presence['to'].bare == self.boundjid.bare:
self.handle_online(presence, False) # handle online
elif ptype == 'subscribe':
self.send_presence(pto=presence['from'].bare, pfrom=presence['to'].bare, ptype='subscribed')
elif ptype == 'subscribed':
pass
elif ptype == 'unsubscribe':
pass
elif ptype == 'unsubscribed':
pass
elif ptype == 'probe':
self.send_presence(pto=presence['from'], pfrom=presence['to'], ptype='available')
elif ptype == 'unavailable':
pass
else:
# self.send_presence(pto=presence['from'], pfrom=presence['to'])
pass
def handle_online(self, event, sync_roster = True):
"""
Gateway's subscriber comes online
:param event:
:return:
"""
jid = event['from'].bare
to = event['to'].bare
# maybe if i'll ignore it — it will go ahead
if to != self.boundjid.bare:
return
if jid not in self.tg_connections:
result = self.db_connection.execute("SELECT * FROM accounts WHERE jid = ?", (jid,)).fetchone()
if result is not None:
self.spawn_tg_client(jid, result['tg_phone'])
else:
if not (self.tg_connections[jid].is_connected()):
self.tg_connections[jid].connect()
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=False))
self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected')
self.tg_process_dialogs(jid, sync_roster) # do not sync roster if we already have connection!
def handle_offline(self, event):
"""
Gateway's subscriber comes offline.
:param event:
:return:
"""
jid = event['from'].bare
# keep telegram online ?
if self.accounts[jid]['keep_online']:
return
if jid in self.tg_connections:
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=True))
self.tg_connections[jid].disconnect()
def handle_interrupt(self, signal, frame):
"""
Interrupted (Ctrl+C).
:param event:
:return:
"""
for jid in self.tg_connections:
print('Disconnecting: %s' % jid)
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=True))
self.tg_connections[jid].disconnect()
for contact_jid, contact_nickname in self.contact_list[jid].items():
self.send_presence(pto=jid, pfrom=contact_jid, ptype='unavailable')
self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='unavailable')
sys.exit(0)
def process_command(self, msg):
"""
Commands to gateway, users or chats (starts with !)
:param iq:
:return:
"""
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 = GateMessageHandler(msg)._handler
try:
reply = str(handler(self))
except Exception as e:
if self.config["debug"]:
reply = "******* DEBUG MODE ACTIVE *********\n"
reply += "An Exception occured while executing this command:"
reply += traceback.format_exc()
else:
if isinstance(e, NotAuthorizedError):
reply = str(e)
elif isinstance(e, MessageHandler.WrongNumberOfArgsError):
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()
def process_chat_user_command(self, msg): def process_chat_user_command(self, msg):
logging.info("received command "+str(msg["body"])+" from "+str(msg["from"])+" for "+str(msg["to"])) logging.info("received command "+str(msg["body"])+" from "+str(msg["from"])+" for "+str(msg["to"]))
@@ -654,7 +651,7 @@ class XMPPTelegram(ComponentXMPP):
if is_command: if is_command:
command = msg["body"].split(" ")[0][1:] command = msg["body"].split(" ")[0][1:]
handler = self.ChatCommandHandler(msg)._handler handler = ChatCommandHandler(msg)._handler
try: try:
reply = str(handler(self)) reply = str(handler(self))
except Exception as e: except Exception as e:
@@ -665,7 +662,7 @@ class XMPPTelegram(ComponentXMPP):
else: else:
if isinstance(e, NotAuthorizedError): if isinstance(e, NotAuthorizedError):
reply = str(e) reply = str(e)
elif isinstance(e, self.MessageHandler.WrongNumberOfArgsError): elif isinstance(e, MessageHandler.WrongNumberOfArgsError):
reply = str(e) reply = str(e)
else: else:
logging.error("Exception in command from {}, command was '{}'".format(msg["from"],msg["body"])) logging.error("Exception in command from {}, command was '{}'".format(msg["from"],msg["body"]))
@@ -682,9 +679,7 @@ class XMPPTelegram(ComponentXMPP):
'!s/find/replace - Edit last message. Use empty `find` to edit whole message and empty `replace` to delete it.\n' '!s/find/replace - Edit last message. Use empty `find` to edit whole message and empty `replace` to delete it.\n'
'!block - Blacklists current user\n' '!block - Blacklists current user\n'
'!unblock - Unblacklists current user\n' '!unblock - Unblacklists current user\n'
'!remove - Removes history and contact from your contact list\n' '!remove - Removes history and contact from your contact list\n' )
)
def process_chat_group_command(self, iq): def process_chat_group_command(self, iq):
parsed = iq['body'].split(' ') parsed = iq['body'].split(' ')