From bca68601595174afcdcaa6763f96993d942ef002 Mon Sep 17 00:00:00 2001 From: annelin Date: Tue, 19 Jun 2018 05:09:38 +0000 Subject: [PATCH] =?UTF-8?q?[SVN]=20initial=20commit=20after=20SVN=20rebirt?= =?UTF-8?q?h;=20[SVN]=20bump=20to=20version=202.0=20[UPD]=20now=20transpor?= =?UTF-8?q?t=20working=20with=20telethon=200.15.5=20and=20sleekxmpp=201.3.?= =?UTF-8?q?2=20[FIX]=20fixed=20everlasting=20authorization=20requests.=20i?= =?UTF-8?q?f=20you=20got=20deauth=20message=20=E2=80=94=20ignore=20it,=20F?= =?UTF-8?q?ROM=20subscription=20is=20enough.=20[ADD]=20implemented=20roste?= =?UTF-8?q?r=20exchange=20via=20XEP-0144=20[ADD]=20we=20will=20send=20auth?= =?UTF-8?q?orization=20request=20when=20unknown=20contact=20sent=20us=20a?= =?UTF-8?q?=20message=20[ADD]=20correct=20presence=20handling=20for=20tran?= =?UTF-8?q?sport=20and=20users=20[ADD]=20fixed=20presence=20spam=20(by=20d?= =?UTF-8?q?efault,=20we=20updating=20presence=20once=20for=2060=20seconds?= =?UTF-8?q?=20--=20look=20at=20`status=5Fupdate=5Finterval`=20in=20mtproto?= =?UTF-8?q?.py)=20[ADD]=20we=20will=20automatically=20connect=20to=20all?= =?UTF-8?q?=20actual=20sessions=20after=20transport=20start?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL | 11 + LICENSE | 13 + README | 5 + config_example.py | 23 ++ requirements.txt | 3 + start.py | 73 ++++++ xmpp_tg/__init__.py | 3 + xmpp_tg/monkey.py | 22 ++ xmpp_tg/mtproto.py | 499 ++++++++++++++++++++++++++++++++++++ xmpp_tg/utils.py | 63 +++++ xmpp_tg/xmpp.py | 605 ++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1320 insertions(+) create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 README create mode 100644 config_example.py create mode 100644 requirements.txt create mode 100644 start.py create mode 100644 xmpp_tg/__init__.py create mode 100644 xmpp_tg/monkey.py create mode 100644 xmpp_tg/mtproto.py create mode 100644 xmpp_tg/utils.py create mode 100644 xmpp_tg/xmpp.py diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..5eecc03 --- /dev/null +++ b/INSTALL @@ -0,0 +1,11 @@ +INSTALLATION +============ + +- Only unix systems officially supported by this software +- Please be sure that you have Jabber server with XEP-100/XEP-114 support and Python 3 interpreter in your system +- Install dependencies from 'requirement.txt' file (you can use pip/venv for this operation) +- We recommends use Telethon library from this repository because pip/upstream versions may have some issues +- Configure your Jabber server to use an external components +- Rename 'config_example.py' to 'config.py' and setup all config variables +- Transport runs by starting 'start.py' via configured python interpreter/virtual environment +- All information outputs to STDOUT/STDERR streams and log file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..867b94e --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017, Narayana OU + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README b/README new file mode 100644 index 0000000..b6ec7e9 --- /dev/null +++ b/README @@ -0,0 +1,5 @@ +XMPP <-> Telegram Gateway +========================= + +This is Telegram to Jabber (XMPP) transport written in Python 3 using Telethon and SleekXMPP libraries. For +installation info please read file INSTALL. This software licensed under Apache 2.0 licence (see LICENSE file). diff --git a/config_example.py b/config_example.py new file mode 100644 index 0000000..acd70b1 --- /dev/null +++ b/config_example.py @@ -0,0 +1,23 @@ +# Rename to config.py + +CONFIG = { + 'title': 'XMPP <-> Telegram Gate', + + 'debug': True, + + 'jid': 'tlgrm.localhost', + 'secret': 'secret', + 'server': 'localhost', + 'port': '8889', + + 'tg_api_id': '17349', # Telegram Desktop (GitHub) + 'tg_api_hash': '344583e45741c457fe1862106095a5eb', + + 'db_connect': 'db.sqlite', + + 'media_web_link_prefix': 'http://tlgrm.localhost/media/', + 'media_store_path': '/var/tg4xmpp/media/', + 'media_max_download_size': 1024 * 1024 * 100, # in bytes + + 'messages_max_max_cache_size': 300, # for quotes +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eeadea0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +sleekxmpp==1.3.2 +Telethon==0.15.5 + diff --git a/start.py b/start.py new file mode 100644 index 0000000..1d5a041 --- /dev/null +++ b/start.py @@ -0,0 +1,73 @@ +import xmpp_tg +import logging +import logging.handlers +import os +import sys +import signal +from config import CONFIG +import telethon +import sleekxmpp + +xmpp_logger = logging.getLogger('sleekxmpp') + + +class StreamToLogger: + """ + Прикидывается файловым объектом. Нужен для перехвата стандартных потоков ввода-вывода. + """ + def __init__(self, logger, level=logging.INFO, old_out=None): + self.logger = logger + self.level = level + self.old_out = old_out + self.linebuf = [] + self._buffer = '' + self._prev = None + + def write(self, buf): + if self._prev == buf == '\n': # Надо на буфер переделывать + self._prev = buf + buf = '' + else: + self._prev = buf + if buf != '\n': + self.logger.log(self.level, buf) + + if self.old_out: + self.old_out.write(buf) + + def flush(self): + pass + + +# Настраиваем логгирование +logging.basicConfig( + level=logging.DEBUG if CONFIG['debug'] else logging.INFO, + format='%(asctime)s :: %(levelname)s:%(name)s :: %(message)s', + datefmt='%m/%d/%Y %I:%M:%S %p', + handlers=[logging.handlers.RotatingFileHandler(filename='gate.log'), logging.StreamHandler(sys.stdout)] +) + +# Создаем логгеры и перехватчики для STDOUT/STDERR +logger_stdout = logging.getLogger('__stdout') +sys.stdout = StreamToLogger(logger_stdout, logging.INFO) + +logger_stderr = logging.getLogger('__stderr') +sys.stderr = StreamToLogger(logger_stderr, logging.ERROR) + +logging.getLogger().log(logging.INFO, '~'*81) +logging.getLogger().log(logging.INFO, ' RESTART '*9) +logging.getLogger().log(logging.INFO, '~'*81) +print('----------------------------------------------------------------------') +print('--- Telegram (MTProto) <---> XMPP Gateway ---') +print('----------------------------------------------------------------------') +print() +print('Starting...') +print('Gate build: rev{}'.format(xmpp_tg.__version__)) +print('Process pid: {}'.format(os.getpid())) +print('Using Telethon v{} and SleekXMPP v{}'.format(telethon.TelegramClient.__version__, sleekxmpp.__version__)) +print() + +gate = xmpp_tg.XMPPTelegram(CONFIG) +signal.signal(signal.SIGINT, gate.handle_interrupt) +gate.connect() +gate.process() diff --git a/xmpp_tg/__init__.py b/xmpp_tg/__init__.py new file mode 100644 index 0000000..54bdddc --- /dev/null +++ b/xmpp_tg/__init__.py @@ -0,0 +1,3 @@ +from xmpp_tg.xmpp import XMPPTelegram + +__version__ = 15 diff --git a/xmpp_tg/monkey.py b/xmpp_tg/monkey.py new file mode 100644 index 0000000..4e053bd --- /dev/null +++ b/xmpp_tg/monkey.py @@ -0,0 +1,22 @@ +from sleekxmpp.plugins.xep_0054 import XEP_0054 +from sleekxmpp import Iq +from sleekxmpp.exceptions import XMPPError + + +def patched_handle_get_vcard(self, iq): + if iq['type'] == 'result': + self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp']) + return + elif iq['type'] == 'get': + vcard = self.api['get_vcard'](iq['to'].bare) + if isinstance(vcard, Iq): + vcard.send() + else: + iq = iq.reply() + iq.append(vcard) + iq.send() + elif iq['type'] == 'set': + raise XMPPError('service-unavailable') + +# Грязно патчим баг в библиотеке +XEP_0054._handle_get_vcard = patched_handle_get_vcard diff --git a/xmpp_tg/mtproto.py b/xmpp_tg/mtproto.py new file mode 100644 index 0000000..42a17d5 --- /dev/null +++ b/xmpp_tg/mtproto.py @@ -0,0 +1,499 @@ +from telethon import TelegramClient +from telethon.utils import get_extension +from telethon.tl.types import UpdateShortMessage, UpdateShortChatMessage, UpdateEditMessage, UpdateDeleteMessages, \ + UpdateNewMessage, UpdateUserStatus, UpdateShort, Updates, UpdateNewChannelMessage,\ + UpdateChannelTooLong, UpdateDeleteChannelMessages, UpdateEditChannelMessage,\ + UpdateUserName +from telethon.tl.types import InputPeerChat, InputPeerUser, InputPeerChannel, InputUser +from telethon.tl.types import MessageMediaDocument, MessageMediaPhoto, MessageMediaUnsupported, MessageMediaContact,\ + MessageMediaGeo, MessageMediaEmpty, MessageMediaVenue +from telethon.tl.types import DocumentAttributeAnimated, DocumentAttributeAudio, DocumentAttributeFilename,\ + DocumentAttributeSticker, DocumentAttributeVideo, DocumentAttributeHasStickers +from telethon.tl.types import MessageService, MessageActionChannelCreate, MessageActionChannelMigrateFrom,\ + MessageActionChatCreate, MessageActionChatAddUser, MessageActionChatDeleteUser,\ + MessageActionChatEditTitle, MessageActionChatJoinedByLink, MessageActionChatMigrateTo,\ + MessageActionPinMessage +from telethon.tl.types import UserStatusOnline, UserStatusOffline, UserStatusRecently +from telethon.tl.types import User, Chat, Channel +from telethon.tl.types import PeerUser, PeerChat, PeerChannel +from telethon.tl.functions.users import GetFullUserRequest +from telethon.tl.functions.messages import ReadHistoryRequest, GetFullChatRequest +from telethon.tl.functions.channels import ReadHistoryRequest as ReadHistoryChannel, GetParticipantRequest +from telethon.tl.functions.updates import GetDifferenceRequest +from telethon.tl.functions.contacts import ResolveUsernameRequest + +import hashlib +import os +import queue +import threading +import time +from xmpp_tg.utils import display_tg_name + +from .utils import var_dump +import traceback + + +class TelegramGateClient(TelegramClient): + def __init__(self, session, api_id, api_hash, xmpp_gate, jid, phone, proxy=None): + super().__init__(session, api_id, api_hash, proxy=proxy, update_workers = 4) + + self.me = None + + self.xmpp_gate = xmpp_gate + self.jid = jid + self.phone = phone + self.user_options = {'nl_after_info': True, 'status_update_interval': 60} + + self._media_queue = queue.Queue() + self._media_thread = threading.Thread(name='MediaDownloaderThread', target=self.media_thread_downloader) + self._media_thread.start() + + self._groups_users = dict() + self._message_cache_users = dict() + self._message_cache_groups = dict() + self._message_cache_supergroups = dict() + + self._status_last = dict() + + self._del_pts = 0 + + + def xmpp_update_handler(self, obj): + print('new update for ' + self.jid) + print(type(obj), obj.__dict__) + + if not self.me: + self.me = self.get_me() + + ''' + Боты + Сделать запоминание ростера в бд + Сделать лучше хендлинг ошибок + Доделать все типы информационных сообщений + Сделать джойны по линкам в чаты/каналы + Сделать поиск и добавление пользователей + Сделать листание истории + Сделать отправку всех непрочтенных сообщений при входе + ''' + + # Здесь будет очень длинный пиздец ^__^ + + nl = '\n' if self.user_options['nl_after_info'] else '' + + try: + + # message from normal chat # + if type(obj) in [UpdateShortMessage] and not obj.out: + + fwd_from = self._process_forward_msg(obj) if obj.fwd_from else '' # process forward messages + self.gate_send_message( mfrom='u' + str(obj.user_id), mbody = '[MSG {}] {}{}'.format(obj.id, fwd_from, obj.message) ) + + if obj.user_id in self.xmpp_gate.tg_dialogs[self.jid]['users']: # make as read + usr = self.xmpp_gate.tg_dialogs[self.jid]['users'][obj.user_id] + self.invoke(ReadHistoryRequest( InputPeerUser(usr.id, usr.access_hash), obj.id )) + + # message from normal group # + if type(obj) in [UpdateShortChatMessage] and not obj.out: + fwd_from = self._process_forward_msg(obj) if obj.fwd_from else '' # process forward messages + nickname = '' + + # get sender information from chat info + if obj.from_id not in self._groups_users: + chat_info = self.invoke(GetFullChatRequest(obj.chat_id)) + + for usr in chat_info.users: + self._groups_users[usr.id] = usr + + nickname = display_tg_name(self._groups_users[obj.from_id].first_name, self._groups_users[obj.from_id].last_name) + + # send message + self.gate_send_message(mfrom='g' + str(obj.chat_id), mbody ='[MSG {}] [User: {}] {}{}'.format(obj.id, nickname, fwd_from, obj.message) ) + self.invoke(ReadHistoryRequest(InputPeerChat(obj.chat_id), obj.id)) + + + # message from supergroup or media message # + if type(obj) in [UpdateNewMessage, UpdateNewChannelMessage, UpdateEditMessage, UpdateEditChannelMessage] and not obj.message.out: + + cid = None + mid = obj.message.id + msg = obj.message.message + fwd_from = '' + + # detect message type + is_user = type(obj.message.to_id) is PeerUser + is_group = type(obj.message.to_id) is PeerChat + is_supergroup = type(obj.message.to_id) is PeerChannel + + # is forwarded? + if obj.message.fwd_from: + fwd_from = self._process_forward_msg(obj.message) + + # detect from id + if is_user: + cid = obj.message.from_id + usr = self.xmpp_gate.tg_dialogs[self.jid]['users'][cid] + prefix = 'u' + elif is_group: + cid = obj.message.to_id.chat_id + prefix = 'g' + elif is_supergroup: + cid = obj.message.to_id.channel_id + access_hash = self.xmpp_gate.tg_dialogs[self.jid]['supergroups'][cid].access_hash if cid in self.xmpp_gate.tg_dialogs[self.jid]['supergroups'] else None + prefix = 's' + + # maybe its channel? # + if obj.message.post: + prefix = 'c' + + # maybe its forwarded? # + + # get sender information from chat info # + if not is_user and not obj.message.post: + if obj.message.from_id not in self._groups_users: + + chat_info = self.invoke(GetFullChatRequest(cid)) if is_group else self.invoke(GetParticipantRequest(InputPeerChannel(cid, access_hash), InputPeerUser(self.me.id, self.me.access_hash))) + for usr in chat_info.users: + self._groups_users[usr.id] = usr + + nickname = display_tg_name(self._groups_users[obj.message.from_id].first_name, self._groups_users[obj.message.from_id].last_name) + msg = '[User: {}] {}'.format(nickname, msg) + + + # message media # + if obj.message.media: + msg = '{} {}'.format( msg, self._process_media_msg(obj.message.media) ) + + # edited # + if obj.message.edit_date: + msg = '[Edited] {}'.format(msg) + + # send message # + self.gate_send_message(prefix + str(cid), mbody = '[MSG {}] {}{}'.format(mid, fwd_from, msg) ) + + # delivery report + if is_user and usr.access_hash: # make as read + self.invoke(ReadHistoryRequest( InputPeerUser(usr.id, usr.access_hash), mid )) + if is_group: + self.invoke(ReadHistoryRequest(InputPeerChat(cid), mid)) + if is_supergroup and access_hash: + self.invoke(ReadHistoryChannel(InputPeerChannel(cid, access_hash), mid)) + + + # Status Updates # + if type(obj) is UpdateUserStatus: + + print(self._status_last) + print(time.time()) + + # save last update time # + if (obj.user_id in self._status_last) and ( (time.time() - self._status_last[obj.user_id]['time'] < self.user_options['status_update_interval']) or self._status_last[obj.user_id]['status'] == obj.status ): + return + + self._status_last[obj.user_id] = {'status': obj.status, 'time': time.time()} + + # process status update # + if type(obj.status) is UserStatusOnline: + self.xmpp_gate.send_presence( pto=self.jid, pfrom='u'+str(obj.user_id)+'@'+self.xmpp_gate.config['jid']) + elif type(obj.status) is UserStatusOffline: + self.xmpp_gate.send_presence( pto=self.jid, pfrom='u'+str(obj.user_id)+'@'+self.xmpp_gate.config['jid'], ptype='xa', pstatus=obj.status.was_online.strftime('Last seen at %H:%M %d/%m/%Y') ) + elif type(obj.status) is UserStatusRecently: + self.xmpp_gate.send_presence( pto=self.jid, pfrom='u' + str(obj.user_id) + '@' + self.xmpp_gate.config['jid'], pstatus='Last seen recently' ) + else: + print(type(obj.status)) + print(obj.update.status.__dict__) + + + except Exception: + print('Exception occurs!') + print(traceback.format_exc()) + + print(' ') + + def gate_send_message(self, mfrom, mbody): + tg_from = int(mfrom[1:]) + if not tg_from in self.xmpp_gate.tg_dialogs[self.jid]['users'] and not tg_from in self.xmpp_gate.tg_dialogs[self.jid]['groups'] and not tg_from in self.xmpp_gate.tg_dialogs[self.jid]['supergroups']: + print('Re-init dialog list...') + self.xmpp_gate.tg_process_dialogs( self.jid ) + + self.xmpp_gate.send_message( mto=self.jid, mfrom=mfrom + '@' + self.xmpp_gate.config['jid'], mtype='chat', mbody=mbody) + + def generate_media_link(self, media): + """ + Генерирует будующее имя и ссылку на скачиваемое медиа-вложения из сообщения + :param media: + :return: + """ + if type(media) is MessageMediaPhoto: + media_id = media.photo.id + elif type(media) is MessageMediaDocument: + media_id = media.document.id + else: + return None + + ext = get_extension(media) + if ext == '.oga': + ext = '.ogg' + + file_name = hashlib.new('sha256') + file_name.update(str(media_id).encode('ascii')) + file_name.update(str(os.urandom(2)).encode('ascii')) + file_name = file_name.hexdigest() + ext + + link = self.xmpp_gate.config['media_web_link_prefix'] + file_name + + return {'name': file_name, 'link': link} + + @staticmethod + def get_document_attribute(attributes, match): + """ + Находит заданных аттрибут в списке. Используется при разборе медиа-вложений типа Документ. + :param attributes: + :param match: + :return: + """ + for attrib in attributes: + if type(attrib) == match: + return attrib + return None + + def _process_forward_msg(self, message): + """ + Обрабатывает информацию в пересланном сообщении (от кого оно и/или из какого канала). Требует дополнительно + предоставление информации об пользователях/каналах. + :param message: + :param users: + :param channels: + :return: + """ + if message.fwd_from.from_id: # От пользователя + + usr = self.get_entity(message.fwd_from.from_id) if not message.fwd_from.from_id in self.xmpp_gate.tg_dialogs[self.jid]['users'] else self.xmpp_gate.tg_dialogs[self.jid]['users'][message.fwd_from.from_id] + print(usr) + if usr.access_hash: + self.xmpp_gate.tg_dialogs[self.jid]['users'][message.fwd_from.from_id] = usr + fwd_from = display_tg_name(usr.first_name, usr.last_name) + else: + fwd_from = '' + + if message.fwd_from.channel_id: # От канала + fwd_from = ''.format(message.fwd_from.channel_id) + + # let's construct + fwd_reply = '|Forwarded from [{}]|'.format(fwd_from) + return fwd_reply + + def _process_media_msg(self, media): + """ + Обрабатывает медиа-вложения в сообщениях. Добавляет их в очередь на загрузку. Производит разбор с генерацию + готового для вывода сообщения с информацией о медиа и сгенерированной ссылкой на него. + :param media: + :return: + """ + msg = '' + # print(var_dump(media)) + + if type(media) is MessageMediaDocument: # Документ или замаскированная сущность + attributes = media.document.attributes + attributes_types = [type(a) for a in attributes] # Документами могут быть разные вещи и иметь аттрибуты + + size_text = '|Size: {:.2f} Mb'.format(media.document.size / 1024 / 1024) + + if media.document.size > self.xmpp_gate.config['media_max_download_size']: # Не загружаем большие файлы + g_link = {'link': 'File is too big to be downloaded via Telegram <---> XMPP Gateway. Sorry.'} + else: + g_link = self.generate_media_link(media) # Добавляем файл в очередь на загрузку в отдельном потоке + self._media_queue.put({'media': media, 'file': g_link['name']}) + + attr_fn = self.get_document_attribute(attributes, DocumentAttributeFilename) + if attr_fn: # Если есть оригинальное имя файла, то выводим + msg = '[FileName:{}{}] {}'.format(attr_fn.file_name, size_text, g_link['link']) + else: + msg = g_link['link'] + + if DocumentAttributeSticker in attributes_types: # Стикер + smile = self.get_document_attribute(attributes, DocumentAttributeSticker).alt + msg = '[Sticker {}] {}'.format(smile, g_link['link']) # У стикеров свой формат вывода + elif DocumentAttributeAudio in attributes_types: # Аудио файл / Голосовое сообщение + attr_a = self.get_document_attribute(attributes, DocumentAttributeAudio) + + if attr_a.voice: # Голосовое сообщение + msg = '[VoiceMessage|{} sec] {}'.format(attr_a.duration, g_link['link']) # Тоже свой формат + else: # Приложенный аудиофайл, добавляем возможную информацию из его тегов + attr_f = self.get_document_attribute(attributes, DocumentAttributeFilename) + msg = '[Audio|File:{}{}|Performer:{}|Title:{}|Duration:{} sec] {}' \ + .format(attr_f.file_name, size_text, attr_a.performer, attr_a.title, + attr_a.duration, g_link['link']) + elif DocumentAttributeVideo in attributes_types: # Видео + video_type = 'Video' + video_file = '' + caption = '' + + if DocumentAttributeAnimated in attributes_types: # Проверка на "gif" + video_type = 'AnimatedVideo' + + if DocumentAttributeFilename in attributes_types: # Если есть оригинальное имя файла - указываем + attr_v = self.get_document_attribute(attributes, DocumentAttributeFilename) + video_file = '|File:{}'.format(attr_v.file_name) + + if media.caption: + caption = media.caption + ' ' + + # Тоже свой формат + msg = '[{}{}{}] {}{}'.format(video_type, video_file, size_text, caption, g_link['link']) + elif type(media) is MessageMediaPhoto: # Фотография (сжатая, jpeg) + g_link = self.generate_media_link(media) + msg = g_link['link'] + + self._media_queue.put({'media': media, 'file': g_link['name']}) + + if media.caption: # Если есть описание - указываем + msg = '{} {}'.format(media.caption, msg) + + elif type(media) is MessageMediaContact: # Контакт (с номером) + msg = 'First name: {} / Last name: {} / Phone: {}'\ + .format(media.first_name, media.last_name, media.phone_number) + elif type(media) in [MessageMediaGeo, MessageMediaVenue]: # Адрес на карте + map_link_template = 'https://maps.google.com/maps?q={0:.4f},{1:.4f}&ll={0:.4f},{1:.4f}&z=16' + map_link = map_link_template.format(media.geo.lat, media.geo.long) + msg = map_link + + if type(media) is MessageMediaVenue: + msg = '[Title: {}|Address: {}|Provider: {}] {}'.format(media.title, media.address, media.provider, msg) + + return msg + + @staticmethod + def _process_info_msg(message, users): + """ + Обрабатывает информационные сообщения в групповых чатах. Возвращает готовое для вывода сообщение. + :param message: + :param users: + :return: + """ + alt_msg = None + nickname = display_tg_name(users[0].first_name, users[0].last_name) + uid = users[0].id + + # MessageActionChatEditPhoto + + # Создана супергруппа + if type(message.action) is MessageActionChannelCreate: + # Пока нет смысла - поддержка каналов не реализована + pass + # Создана группа + elif type(message.action) is MessageActionChatCreate: + pass + # Добавлен пользователь в чат + elif type(message.action) is MessageActionChatAddUser: + if len(users) == 2: # Кто-то добавил другого пользователя + j_name = display_tg_name(users[1].first_name, users[1].last_name) + j_uid = users[1].id + alt_msg = 'User [{}] (UID:{}) added [{}] (UID:{})'.format(nickname, uid, + j_name, j_uid) + else: # Пользователь вошел сам + alt_msg = 'User [{}] (UID:{}) joined'.format(nickname, uid) + # Пользователь удален/вышел/забанен + elif type(message.action) is MessageActionChatDeleteUser: + pass + # Пользователь вошел по инвайт ссылке + elif type(message.action) is MessageActionChatJoinedByLink: + alt_msg = 'User [{}] (UID:{}) joined via invite link'.format(nickname, uid) + # Изменено название чата + elif type(message.action) is MessageActionChatEditTitle: + g_title = message.action.title + alt_msg = 'User [{}] (UID:{}) changed title to [{}]'.format(nickname, uid, g_title) + # Прикреплено сообщение в чате + elif type(message.action) is MessageActionPinMessage: + # Notify all members реализовано путем указания, что пользователя упомянули, + # то есть флаг mentioned=True. Но для транспорта он не имеет смысла. + p_mid = message.reply_to_msg_id # Наркоманы + alt_msg = 'User [{}] (UID:{}) pinned message with MID:{}'.format(nickname, uid, p_mid) + # Группа была преобразована в супергруппу + elif type(message.action) is MessageActionChatMigrateTo: + # Это сложный ивент, который ломает текущую реализацию хендлинга + # (ибо в доках, которых нет, не сказано, что так можно было) + # Пусть полежит до рефакторинга + pass + # Супергруппа была технически создана из группы + elif type(message.action) is MessageActionChannelMigrateFrom: + # ---...---...--- + # ---...---...--- + # ---...---...--- + pass + + return alt_msg + + def get_cached_message(self, dlg_id, msg_id, user=False, group=False, supergroup=False): + """ + Получает из кэша сообщение диалога указанной группы (для работы цитат в последних сообщениях) + :param dlg_id: + :param msg_id: + :param user: + :param group: + :param supergroup: + :return: + """ + if user: + obj = self._message_cache_users + elif group: + obj = self._message_cache_groups + elif supergroup: + obj = self._message_cache_supergroups + else: + return None + + if dlg_id in obj: + if msg_id in obj[dlg_id]: + return obj[dlg_id][msg_id] + + return None + + def set_cached_message(self, dlg_id, msg_id, msg, user=False, group=False, supergroup=False): + """ + Кэширует сообщение из диалога указанной группы (для работы цитат в последних сообщениях) + :param dlg_id: + :param msg_id: + :param msg: + :param user: + :param group: + :param supergroup: + :return: + """ + if user: + obj = self._message_cache_users + elif group: + obj = self._message_cache_groups + elif supergroup: + obj = self._message_cache_supergroups + else: + return + + if dlg_id not in obj: + obj[dlg_id] = dict() + + obj[dlg_id][msg_id] = msg + + # Удаляем старые сообщения из кэша + if len(obj[dlg_id]) > self.xmpp_gate.config['messages_max_max_cache_size']: + del obj[dlg_id][sorted(obj[dlg_id].keys())[0]] + + def media_thread_downloader(self): + """ + Этот метод запускается в отдельном потоке и скачивает по очереди все медиа вложения из сообщений + :return: + """ + while True: + try: + if self._media_queue.empty(): # Нет медиа в очереди - спим + time.sleep(0.1) + else: # Иначе скачиваем медиа + print('MTD ::: Queue is not empty. Downloading...') + media = self._media_queue.get() + file_path = self.xmpp_gate.config['media_store_path'] + media['file'] + if os.path.isfile(file_path): + print('MTD ::: File already exists') + else: + self.download_media(media['media'], file_path, False) + print('MTD ::: Media downloaded') + except Exception: + print(traceback.format_exc()) diff --git a/xmpp_tg/utils.py b/xmpp_tg/utils.py new file mode 100644 index 0000000..04f41f4 --- /dev/null +++ b/xmpp_tg/utils.py @@ -0,0 +1,63 @@ +""" +Различные полезные функции +""" + +import types +from datetime import datetime + + +def display_tg_name(first_name, last_name): + if first_name and last_name: + return '{} {}'.format(first_name, last_name) + elif first_name: + return first_name + elif last_name: + return last_name + else: + return '[No name]' + + +def make_gate_jid(): + pass + + +def parse_gate_jid(): + pass + + +def var_dump(obj, depth=7, l=""): + # fall back to repr + if depth < 0 or type(obj) is datetime: + return repr(obj) + + # expand/recurse dict + if isinstance(obj, dict): + name = "" + objdict = obj + else: + # if basic type, or list thereof, just print + canprint = lambda o: isinstance(o, (int, float, str, bool, type(None), types.LambdaType)) + + try: + if canprint(obj) or sum(not canprint(o) for o in obj) == 0: + return repr(obj) + except TypeError: + pass + + # try to iterate as if obj were a list + try: + return "[\n" + "\n".join(l + var_dump(k, depth=depth - 1, l=l + " ") + "," for k in obj) + "\n" + l + "]" + except TypeError as e: + # else, expand/recurse object attribs + name = (hasattr(obj, '__class__') and obj.__class__.__name__ or type(obj).__name__) + objdict = {} + + for a in dir(obj): + if a[:2] != "__" and (not hasattr(obj, a) or not hasattr(getattr(obj, a), '__call__')): + try: + objdict[a] = getattr(obj, a) + except Exception as e: + objdict[a] = str(e) + + return name + "{\n" + "\n".join(l + repr(k) + ": " + var_dump(v, depth=depth - 1, l=l + " ") + "," for k, v in + objdict.items()) + "\n" + l + "}" \ No newline at end of file diff --git a/xmpp_tg/xmpp.py b/xmpp_tg/xmpp.py new file mode 100644 index 0000000..2e77e2e --- /dev/null +++ b/xmpp_tg/xmpp.py @@ -0,0 +1,605 @@ +import sqlite3 +import re +import sys + +import sleekxmpp +from sleekxmpp.componentxmpp import ComponentXMPP +import xml.etree.ElementTree as ET + +from telethon.tl.functions.messages import GetDialogsRequest, SendMessageRequest +from telethon.tl.functions.account import UpdateStatusRequest, GetAuthorizationsRequest +from telethon.tl.types import InputPeerEmpty, InputPeerUser, InputPeerChat, InputPeerChannel +from telethon.tl.types import PeerChannel, PeerChat, PeerUser, Chat, ChatForbidden, Channel, ChannelForbidden +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 +import xmpp_tg.monkey # Патчим баги в библиотеках + +class XMPPTelegram(ComponentXMPP): + """ + Класс XMPP компонента транспорта между Telegram и Jabber + """ + + def __init__(self, config_dict): + """ + Инициализация, подключение плагинов и регистрация событий + :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.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', self.event_presence) + self.add_event_handler('presence_unsubscribe', self.event_presence_unsub) + self.add_event_handler('presence_unsubscribed', self.event_presence_unsub) + 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): + """ + Деструктор. Теоретически. + :return: + """ + self.db_connection.close() + + def handle_start(self, arg): + """ + Обработчик события успешного подключения компонента к Jabber серверу + :param arg: + :return: + """ + users = self.db_connection.execute("SELECT * FROM accounts").fetchall() + for usr in users: + print('Sending presence...') + self.send_presence(pto=usr['jid'], pfrom=self.boundjid.bare, ptype='probe') + + def message(self, iq): + """ + Обработчик входящих сообщений из XMPP + :param iq: + :return: + """ + jid = iq['from'].bare + + if iq['to'] == self.config['jid'] and iq['type'] == 'chat': # Пишут транспорту + if iq['body'].startswith('!'): + self.process_command(iq) + else: + self.gate_reply_message(iq, 'Only commands accepted. Try !help for more info.') + else: # Пишут в Telegram + if jid in self.tg_connections and self.tg_connections[jid].is_user_authorized(): + if iq['body'].startswith('!'): # Команда из чата + print('command received') + if iq['to'].bare.startswith('u'): + self.process_chat_user_command(iq) + elif iq['to'].bare.startswith('g') or iq['to'].bare.startswith('s'): + self.process_chat_group_command(iq) + else: + self.gate_reply_message(iq, 'Error.') + else: # Обычное сообщение + print('sent message') + tg_id = int(iq['to'].node[1:]) + tg_peer = None + msg = iq['body'] + reply_mid = None + + if msg.startswith('>'): # Проверка на цитирование + msg_lines = msg.split('\n') + matched = re.match(r'>[ ]*(?P[\d]+)[ ]*', msg_lines[0]).groupdict() + + if 'mid' in matched: # Если нашли ID сообщения, то указываем ответ + reply_mid = int(matched['mid']) + msg = '\n'.join(msg_lines[1:]) + + if iq['to'].bare.startswith('u'): # Обычный пользователь + tg_peer = InputPeerUser(tg_id, self.tg_dialogs[jid]['users'][tg_id].access_hash) + elif iq['to'].bare.startswith('g'): # Обычная группа + tg_peer = InputPeerChat(tg_id) + elif iq['to'].bare.startswith('s') or iq['to'].bare.startswith('c'): # Супергруппа + tg_peer = InputPeerChannel(tg_id, self.tg_dialogs[jid]['supergroups'][tg_id].access_hash) + + print(tg_peer) + + if tg_peer: + # Отправляем сообщение и получаем новый апдейт + result = self.tg_connections[jid].invoke( + SendMessageRequest(tg_peer, msg, generate_random_long(), reply_to_msg_id=reply_mid) + ) + msg_id = None + + # Ищем ID отправленного сообщения + if type(result) is Updates: # Супегруппа / канал + for upd in result.updates: + if type(upd) is UpdateMessageID: + msg_id = upd.id + elif type(result) is UpdateShortSentMessage: # ЛС / Группа + msg_id = result.id + + # if msg_id: + # # Отправляем ответ с ID отправленного сообщения + # self.send_message(mto=iq['from'], mfrom=iq['to'], mtype='chat', + # mbody='[Your MID:{}]'.format(msg_id)) + + def event_presence_unsub(self, presence): + print('defense') + return + + def event_presence(self, presence): + """ + Обработчик события subscribe + :param presence: + :return: + """ + ptype = presence['type'] + + if ptype == 'subscribe': + print('Send SUBSCRIBED') + self.send_presence(pto=presence['from'].bare, pfrom=presence['to'].bare, ptype='subscribed') + elif ptype == 'subscribed': + self.send_presence(pto=presence['from'].bare, pfrom=presence['to'].bare, ptype='subscribee') + pass + elif ptype == 'unsubscribe': + pass + elif ptype == 'unsubscribed': + pass + elif ptype == 'probe': + self.send_presence(pto=presence['from'], pfrom=presence['to'], ptype='available') + pass + elif ptype == 'unavailable': + pass + else: + # self.send_presence(pto=presence['from'], pfrom=presence['to']) + pass + + def handle_online(self, event): + """ + Обработчик события online. Подключается к Telegram при наличии авторизации. + :param event: + :return: + """ + jid = event['from'].bare + + 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) + + + def handle_offline(self, event): + """ + Обработчик события offline. Отключается от Telegram, если было создано подключение. + :param event: + :return: + """ + jid = event['from'].bare + + 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): + + 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, iq): + """ + Обработчик общих команд транспорта + :param iq: + :return: + """ + parced = iq['body'].split(' ') + jid = iq['from'].bare + + if parced[0] == '!help': + self.gate_reply_message(iq, 'Available command:\n\n' + '!help - Displays this text\n' + '!login +123456789 - Initiates Telegram session\n' + '!code 12345 - Entering one-time code during auth\n' + '!password abc123 - Entering password during two-factor auth\n' + '!list_sessions - List all created sessions at Telegram servers\n' + '!delete_session 123 - Delete session\n' + '!logout - Deletes current Telegram session at gate\n' + '!reload_dialogs - Reloads dialogs list from Telegram\n\n' + '!create_group - Initiates group creation\n' + '!create_channel - Initiates channel creation\n\n' + '!change_name first last - Changes your name in Telegram\n' + '!change_username username - Changes your @username in Telegram\n' + # '!blocked_users_list\n' + # '!blocked_users_add\n' + # '!blocked_users_remove\n' + # '!last_seen_privacy_status\n' + # '!last_seen_privacy_set\n' + # '!last_seen_privacy_never_add\n' + # '!last_seen_privacy_never_remove\n' + # '!last_seen_privacy_always_add\n' + # '!last_seen_privacy_always_remove\n' + # '!group_invite_settings_status\n' + # '!group_invite_settings_set\n' + # '!group_invite_settings_add\n' + # '!group_invite_settings_remove\n' + # '!group_invite_settings_add\n' + # '!group_invite_settings_remove\n' + # '!account_selfdestruct_setting_status\n' + # '!account_selfdestruct_setting_set\n' + ) + elif parced[0] == '!login': # -------------------------------------------------- + self.gate_reply_message(iq, 'Please wait...') + self.spawn_tg_client(jid, parced[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: + self.tg_connections[jid].send_code_request(parced[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 parced[0] in ['!code', '!password']: # -------------------------------------------------- + if not self.tg_connections[jid].is_user_authorized(): + if parced[0] == '!code': + try: + self.gate_reply_message(iq, 'Trying authenticate...') + self.tg_connections[jid].sign_in(self.tg_phones[jid], parced[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 parced[0] == '!password': + self.gate_reply_message(iq, 'Checking password...') + self.tg_connections[jid].sign_in(password=parced[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.init_tg(jid) + self.db_connection.execute("INSERT INTO accounts VALUES(?, ?)", (jid, self.tg_phones[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 parced[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()) + print(sessions.__dict__) + elif parced[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 parced[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') + else: # -------------------------------------------------- + self.gate_reply_message(iq, 'Unknown command. Try !help for list all commands.') + + def process_chat_user_command(self, iq): + parced = [] + + if parced[0] == '!search': + pass + elif parced[0] == '!get_history': + pass + elif parced[0] == '!forward_messages': + pass + elif parced[0] == '!delete_messages': + pass + elif parced[0] == '!block_status': + pass + elif parced[0] == '!block_set': + pass + elif parced[0] == '!block_unser': + pass + elif parced[0] == '!clear_history': + pass + elif parced[0] == '!delete_conversation': + pass + elif parced[0] == '!help': + pass + + def process_chat_group_command(self, iq): + parced = [] + + if parced[0] == '!search': + pass + elif parced[0] == '!get_history': + pass + elif parced[0] == '!forward_messages': + pass + elif parced[0] == '!delete_messages': + pass + elif parced[0] == '!pin_message': + pass + elif parced[0] == '!unpin_message': + pass + elif parced[0] == '!leave_group': + pass + elif parced[0] == '!add_members': + pass + elif parced[0] == '!bans_list': + pass + elif parced[0] == '!ban_user': + pass + elif parced[0] == '!unban_user': + pass + elif parced[0] == '!restrict_user': + pass + elif parced[0] == '!unrestrict_user': + pass + elif parced[0] == '!get_recent_actions': + pass + elif parced[0] == '!get_recent_actions': + pass + + def spawn_tg_client(self, jid, phone): + """ + Создает и инициализирует подключение к Telegram + :param jid: + :param phone: + :return: + """ + client = TelegramGateClient('a_'+phone, int(self.config['tg_api_id']), self.config['tg_api_hash'], + self, jid, phone) + 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): + """ + Инициализация транспорта для конкретного пользователя после подключения к Telegram + :param jid: + :return: + """ + # Устанавливаем, что пользователь онлайн + self.tg_connections[jid].invoke(UpdateStatusRequest(offline=False)) + + # Получаем и обрабатываем список диалогов + self.tg_process_dialogs(jid) + + # Регистрируем обработчик обновлений в Telegram + self.tg_connections[jid].add_update_handler(self.tg_connections[jid].xmpp_update_handler) + + def roster_exchange(self, tojid, contacts): + + message = sleekxmpp.Message() + message['from'] = self.boundjid.bare + message['to'] = tojid + rawxml = "" + for jid, nick in contacts.items(): + c = "Telegram" % (jid, nick) + rawxml = rawxml + c + + rawxml = rawxml + "" + message.appendxml(ET.fromstring(rawxml)) + + return message + + def tg_process_dialogs(self, jid): + + print('! -- Process Dialogs -- !') + # Инициализируем словари для диалогов + 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() + + # Оффсеты для получения диалогов + 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]: # Старая группа + self.tg_dialogs[jid]['groups'][cht.id] = cht + elif type(cht) in [Channel, ChannelForbidden]: # Супергруппа + 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 = 'u' + str(usr.id) + '@' + self.boundjid.bare + + 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.first_name, usr.last_name) + vcard['FN'] = display_tg_name(usr.first_name, usr.last_name) + 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.bot: + vcard['DESC'] += ' [Bot]' + + vcard['NICKNAME'] = vcard['FN'] + + vcard['JABBERID'] = u_jid + self.plugin['xep_0054'].publish_vcard(jid=u_jid, vcard=vcard) + self.plugin['xep_0172'].publish_nick(nick=vcard['FN'], ifrom=u_jid) + + # self.send_presence(pto=jid, pfrom=u_jid, ptype='subscribe') + self.contact_list[jid][u_jid] = rostername + + if usr.bot: + self.send_presence(pto=jid, pfrom=u_jid, pstatus='Bot') + else: + if type(usr.status) is UserStatusOnline: + self.send_presence(pto=jid, pfrom=u_jid) + elif type(usr.status) is UserStatusRecently: + self.send_presence(pto=jid, pfrom=u_jid, pshow='away', pstatus='Last seen recently') + elif type(usr.status) is UserStatusOffline: + self.send_presence( + pto=jid, + pfrom=u_jid, + ptype='xa', + pstatus=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]: + g_type = '' + cht = None + + if type(dlg.peer) is PeerChat: # Старая группа + cht = self.tg_dialogs[jid]['groups'][dlg.peer.chat_id] + c_jid = 'g' + str(cht.id) + '@' + self.boundjid.bare + g_type = 'G' + elif type(dlg.peer) is PeerChannel: # Супергруппа + cht = self.tg_dialogs[jid]['supergroups'][dlg.peer.channel_id] + + if cht.broadcast: + g_type = 'C' + c_jid = 'c' + str(cht.id) + '@' + self.boundjid.bare + else: + g_type = 'SG' + c_jid = 's' + str(cht.id) + '@' + self.boundjid.bare + + rostername = '[{}] {}'.format(g_type, cht.title) + + vcard = self.plugin['xep_0054'].make_vcard() + vcard['FN'] = '[{}] {}'.format(g_type, cht.title) + vcard['NICKNAME'] = vcard['FN'] + vcard['JABBERID'] = c_jid + self.plugin['xep_0054'].publish_vcard(jid=c_jid, vcard=vcard) + self.plugin['xep_0172'].publish_nick(nick=vcard['FN'], ifrom=c_jid) + + self.contact_list[jid][c_jid] = rostername + #self.send_presence(pto=jid, pfrom=c_jid, ptype='subscribe') + self.send_presence(pto=jid, pfrom=c_jid) + + if len(dlgs.dialogs) == 0: # Если все диалоги получены - прерываем цикл + rosterxchange = self.roster_exchange(jid, self.contact_list[jid]) + self.send(rosterxchange) + break + else: # Иначе строим оффсеты + last_msg_id = dlgs.dialogs[-1].top_message # Нужен последний id сообщения. Наркоманы. + last_peer = dlgs.dialogs[-1].peer + + last_date = next(msg for msg in dlgs.messages # Ищем дату среди сообщений + if type(msg.to_id) is type(last_peer) and msg.id == last_msg_id).date + + if type(last_peer) is PeerUser: # Пользователь + 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]: # Группа + last_peer = InputPeerChat(last_peer.chat_id) + elif type(last_peer) in [Channel, ChannelForbidden]: # Супергруппа + 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): + """ + Отправляет ответное сообщение от имени транспорта + :param iq: + :param msg: + :return: + """ + self.send_message(mto=iq['from'], mfrom=self.config['jid'], mtype='chat', mbody=msg) + + def init_database(self): + """ + Инициализация БД + :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)" + ")") + + # conn.execute("CREATE TABLE IF NOT EXISTS roster(" + # "") + + return conn