Files
tg4xmpp/xmpp_tg/xmpp.py
2019-03-10 17:25:20 +01:00

645 lines
27 KiB
Python

import re, sys, os, io, sqlite3, hashlib, time, datetime
import xml.etree.ElementTree as ET
import logging, traceback, pprint
from sleekxmpp.componentxmpp import ComponentXMPP
from sleekxmpp import Presence, Message
from sleekxmpp.exceptions import XMPPError
from telethon.tl.functions.messages import GetDialogsRequest, SendMessageRequest, SendMediaRequest, EditMessageRequest, DeleteMessagesRequest, ImportChatInviteRequest, GetFullChatRequest, AddChatUserRequest, DeleteChatUserRequest, CreateChatRequest, DeleteHistoryRequest
from telethon.tl.functions.account import UpdateStatusRequest, GetAuthorizationsRequest, UpdateProfileRequest, UpdateUsernameRequest
from telethon.tl.functions.contacts import DeleteContactRequest, BlockRequest, UnblockRequest, ImportContactsRequest
from telethon.tl.functions.channels import JoinChannelRequest, LeaveChannelRequest, InviteToChannelRequest, EditBannedRequest, CreateChannelRequest, DeleteMessagesRequest as DeleteMessagesChannel
from telethon.tl.types import InputPeerEmpty, InputPeerUser, InputPeerChat, InputPeerChannel, InputPhoneContact, InputMediaPhotoExternal
from telethon.tl.types import User, Chat, Channel
from telethon.tl.types import PeerChannel, PeerChat, PeerUser, Chat, ChatForbidden, Channel, ChannelForbidden, ChannelBannedRights
from telethon.tl.types import UserStatusOnline, UserStatusRecently, UserStatusOffline
from telethon.tl.types import Updates, UpdateShortSentMessage, UpdateMessageID
from telethon.tl.types.messages import Dialogs, DialogsSlice
from telethon.helpers import generate_random_long
from telethon.errors import SessionPasswordNeededError
from xmpp_tg.mtproto import TelegramGateClient
from xmpp_tg.utils import var_dump, display_tg_name, get_contact_jid, localtime
import xmpp_tg.monkey # monkeypatch
from xmpp_tg.message_handlers import *
# modified by adnidor
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.register_plugin('xep_0066') # OOB
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['type'] == 'normal':
raise XMPPError(text="Sorry, this type of message is not supported")
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 --
self.send_tg_message(iq)
def send_tg_message(self, iq):
jid = iq['from'].bare
try:
tg_id = int(iq['to'].node[1:])
except ValueError:
raise XMPPError(text="Invalid Telegram-ID")
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)
else:
raise XMPPError(text="Invalid Telegram-ID")
# peer OK.
if tg_peer:
result = None
# detect media
if "oob" in iq:
logging.debug("Found OOB content in message")
if "url" in iq['oob']:
url = iq['oob']['url']
logging.debug("Found OOB URL: {}".format(url))
if url.split('.')[-1] in self.config['media_external_formats']:
logging.debug("Media found")
media = InputMediaPhotoExternal(url, "Image")
try:
result = self.tg_connections[jid].invoke(SendMediaRequest(tg_peer, media, "Image", 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 = GateCommandHandler(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, 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):
logging.info("received command "+str(msg["body"])+" from "+str(msg["from"])+" for "+str(msg["to"]))
is_command = msg["body"].startswith("!") and msg["body"][1] != "_"
if is_command:
command = msg["body"].split(" ")[0][1:]
handler = ChatCommandHandler(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:\n"
reply += traceback.format_exc()
else:
if isinstance(e, NotAuthorizedError):
reply = str(e)
elif isinstance(e, 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_group_command(self, msg):
logging.info("received command "+str(msg["body"])+" from "+str(msg["from"])+" for "+str(msg["to"]))
is_command = msg["body"].startswith("!") and msg["body"][1] != "_"
if is_command:
command = msg["body"].split(" ")[0][1:]
handler = GroupchatCommandHandler(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:\n"
reply += traceback.format_exc()
else:
if isinstance(e, NotAuthorizedError):
reply = str(e)
elif isinstance(e, 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 spawn_tg_client(self, jid, phone):
"""
Spawns Telegram client
:param jid:
:param phone:
:return:
"""
client = TelegramGateClient('a_'+phone, int(self.config['tg_api_id']), self.config['tg_api_hash'], self, jid, phone)
if 'tg_server_ip' in self.config and 'tg_server_dc' in self.config and 'tg_server_port' in self.config:
client.session.set_dc(self.config['tg_server_dc'], self.config['tg_server_ip'], self.config['tg_server_port'])
client.connect()
self.tg_connections[jid] = client
self.tg_phones[jid] = phone
if client.is_user_authorized():
self.init_tg(jid)
self.send_presence(pto=jid, pfrom=self.boundjid.bare, ptype='online', pstatus='connected')
def init_tg(self, jid):
"""
Initialize
:param jid:
:return:
"""
# Set status = Online
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=False))
# Process Telegram contact list
self.tg_process_dialogs(jid, sync_roster = False)
# Register Telegrap updates handler
self.tg_connections[jid].add_update_handler(self.tg_connections[jid].xmpp_update_handler)
def roster_exchange(self, tojid, contacts):
message = Message()
message['from'] = self.boundjid.bare
message['to'] = tojid
rawxml = "<x xmlns='http://jabber.org/protocol/rosterx'>"
for jid, nick in contacts.items():
c = "<item action='add' jid='%s' name='%s'><group>Telegram</group></item>" % (jid, nick)
rawxml = rawxml + c
rawxml = rawxml + "</x>"
message.appendxml(ET.fromstring(rawxml))
self.send(message)
def roster_fill(self, tojid, contacts):
for jid, nick in contacts.items():
presence = Presence()
presence['from'] = jid
presence['to'] = tojid
presence['type'] = 'subscribe'
presence.appendxml(ET.fromstring("<nick xmlns='http://jabber.org/protocol/nick'>%s</nick>" % nick))
self.send(presence)
def tg_process_dialogs(self, jid, sync_roster = True):
print('Processing dialogs...')
# dialogs dictonaries
self.tg_dialogs[jid] = dict()
self.tg_dialogs[jid]['raw'] = list()
self.tg_dialogs[jid]['users'] = dict()
self.tg_dialogs[jid]['groups'] = dict()
self.tg_dialogs[jid]['supergroups'] = dict()
self.tg_dialogs[jid]['messages'] = dict()
# offsets
last_peer = InputPeerEmpty()
last_msg_id = 0
last_date = None
# roster exchange #
self.contact_list[jid] = dict()
while True:
dlgs = self.tg_connections[jid].invoke(GetDialogsRequest(offset_date=last_date, offset_id=last_msg_id,
offset_peer=last_peer, limit=100))
self.tg_dialogs[jid]['raw'].append(dlgs)
for usr in dlgs.users:
self.tg_dialogs[jid]['users'][usr.id] = usr
for cht in dlgs.chats:
if type(cht) in [Chat, ChatForbidden]: # normal group
self.tg_dialogs[jid]['groups'][cht.id] = cht
elif type(cht) in [Channel, ChannelForbidden]: # supergroup
self.tg_dialogs[jid]['supergroups'][cht.id] = cht
for dlg in dlgs.dialogs:
if type(dlg.peer) is PeerUser:
usr = self.tg_dialogs[jid]['users'][dlg.peer.user_id]
vcard = self.plugin['xep_0054'].make_vcard()
u_jid = get_contact_jid(usr, self.boundjid.bare)
# make vcard #
vcard['JABBERID'] = u_jid
if usr.deleted:
rostername = "Deleted Account"
vcard['FN'] = 'Deleted account'
vcard['DESC'] = 'This user no longer exists in Telegram'
else:
rostername = display_tg_name(usr)
rostername = '[B] ' + rostername if usr.bot else rostername
vcard['FN'] = display_tg_name(usr)
vcard['DESC'] = ''
if usr.first_name:
vcard['N']['GIVEN'] = usr.first_name
if usr.last_name:
vcard['N']['FAMILY'] = usr.last_name
if usr.username:
vcard['DESC'] = 'Telegram Username: @' + usr.username
if usr.phone:
vcard['DESC'] += "\n" + 'Phone number: ' + usr.phone
vcard['NICKNAME'] = vcard['FN']
# add photo to VCard #
photo, photosha1hash = self.get_peer_photo(jid, usr) if sync_roster else (None, None)
if photo:
vcard['PHOTO']['TYPE'] = 'image/jpeg'
vcard['PHOTO']['BINVAL'] = photo
self.plugin['xep_0054'].publish_vcard(jid=u_jid, vcard=vcard)
self.plugin['xep_0172'].publish_nick(nick=vcard['FN'], ifrom=u_jid)
self.publish_photo(jid, u_jid, photosha1hash) if photosha1hash else None
# add it to contect list & avatar download queue #
self.contact_list[jid][u_jid] = rostername
if usr.bot:
self.send_presence(pto=jid, pfrom=u_jid, pshow = 'chat', pstatus='Bot')
else:
if type(usr.status) is UserStatusOnline:
self.send_presence(pto=jid, pfrom=u_jid, pstatus = 'Online' )
elif type(usr.status) is UserStatusRecently:
self.send_presence(pto=jid, pfrom=u_jid, pshow='dnd', pstatus='Last seen recently')
elif type(usr.status) is UserStatusOffline:
phow = 'away' if datetime.datetime.utcnow() - usr.status.was_online < datetime.timedelta(hours = self.accounts[jid]['status_xa_interval'] ) else 'xa'
self.send_presence(pto=jid, pfrom=u_jid, pshow=phow, pstatus=localtime(usr.status.was_online).strftime('Last seen at %H:%M %d/%m/%Y') )
else:
self.send_presence(pto=jid, pfrom=u_jid, ptype='unavailable', pstatus='Last seen a long time ago')
if type(dlg.peer) in [PeerChat, PeerChannel]:
cht = None
if type(dlg.peer) is PeerChat: # old group
cht = self.tg_connections[jid].invoke(GetFullChatRequest(dlg.peer.chat_id))
cht = cht.chats[0]
if cht.deactivated or cht.left:
cht = None
elif type(dlg.peer) is PeerChannel: # supergroup
cht = self.tg_dialogs[jid]['supergroups'][dlg.peer.channel_id]
if cht and cht.id:
rostername = display_tg_name(cht)
u_jid = get_contact_jid(cht, self.boundjid.bare)
vcard = self.plugin['xep_0054'].make_vcard()
vcard['FN'] = rostername
vcard['NICKNAME'] = rostername
vcard['JABBERID'] = u_jid
# add photo to VCard #
photo, photosha1hash = self.get_peer_photo(jid, cht) if sync_roster else (None, None)
if photo:
vcard['PHOTO']['TYPE'] = 'image/jpeg'
vcard['PHOTO']['BINVAL'] = photo
self.plugin['xep_0054'].publish_vcard(jid=u_jid, vcard=vcard)
self.plugin['xep_0172'].publish_nick(nick=vcard['FN'], ifrom=u_jid)
self.publish_photo(jid, u_jid, photosha1hash) if photosha1hash else None
self.contact_list[jid][u_jid] = rostername
self.send_presence(pto=jid, pfrom=u_jid, pshow = 'chat', pstatus = cht.title)
if len(dlgs.dialogs) == 0: # all dialogs was received.
if sync_roster and 'use_roster_exchange' in self.accounts[jid] and self.accounts[jid]['use_roster_exchange'] == 'true':
self.roster_exchange(jid, self.contact_list[jid])
elif sync_roster:
self.roster_fill(jid, self.contact_list[jid])
break
else: # get next part of dialogs.
last_msg_id = dlgs.dialogs[-1].top_message # we fucking need last msg id!
last_peer = dlgs.dialogs[-1].peer
last_date = next(msg for msg in dlgs.messages # find date
if type(msg.to_id) is type(last_peer) and msg.id == last_msg_id).date
if type(last_peer) is PeerUser: # user/bot
access_hash = self.tg_dialogs[jid]['users'][last_peer.user_id].access_hash
last_peer = InputPeerUser(last_peer.user_id, access_hash)
elif type(last_peer) in [Chat, ChatForbidden]: # normal group
last_peer = InputPeerChat(last_peer.chat_id)
elif type(last_peer) in [Channel, ChannelForbidden]: # supergroup/channel
access_hash = self.tg_dialogs[jid]['supergroups'][last_peer.channel_id].access_hash
last_peer = InputPeerChannel(last_peer.channel_id, access_hash)
def tg_process_unread_messages(self):
pass
def gate_reply_message(self, iq, msg):
"""
Reply to message to gate.
:param iq:
:param msg:
:return:
"""
self.send_message(mto=iq['from'], mfrom=self.config['jid'], mtype='chat', mbody=msg)
def get_peer_photo(self, jid, peer):
# we are able to disable this shit #
if not 'enable_avatars' in self.accounts[jid] or self.accounts[jid]['enable_avatars'] != 'true':
return (None, None)
data = io.BytesIO()
self.tg_connections[jid].download_profile_photo(peer, file = data)
data.flush()
if isinstance(data, io.BytesIO) and data.getbuffer().nbytes > 0:
image = data.getvalue()
image_sha1 = hashlib.sha1(image).hexdigest()
return (image, image_sha1)
else:
return (None, None)
def edit_message(self, jid, tg_id, message):
# get last message to this peer
if not tg_id in self.tg_dialogs[jid]['messages']:
return (None, None)
msg_id = self.tg_dialogs[jid]['messages'][tg_id]["id"]
msg_body = self.tg_dialogs[jid]['messages'][tg_id]["body"]
# edit this message
pattern = message.split('/')
replace = ' ' if pattern[2] == '' else '/'.join(pattern[2:]) # no empty regexp — replace with whitespace
edited = re.sub(r'%s' % pattern[1], replace, msg_body, re.I) if pattern[1] != '' else replace # if no pattern specified — edit whole message
return (msg_id, edited)
def publish_photo(self, jid, fromjid, photo):
presence = Presence()
presence['to'] = jid
presence['from'] = fromjid
presence.appendxml(ET.fromstring("<x xmlns='vcard-temp:x:update'><photo>%s</photo></x>" % photo))
self.send(presence)
def init_database(self):
"""
Database initialization
:return:
"""
def dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
conn = sqlite3.connect(self.config['db_connect'], isolation_level=None, check_same_thread=False)
conn.row_factory = dict_factory
conn.execute("CREATE TABLE IF NOT EXISTS accounts(jid VARCHAR(255), tg_phone VARCHAR(25), use_roster_exchange BOOLEAN default false, keep_online BOOLEAN default false, status_update_interval INTEGER default 30, status_xa_interval INTEGER default 24, enable_avatars BOOLEAN default false)")
return conn