[SVN] initial commit after SVN rebirth;

[SVN] bump to version 2.0
[UPD] now transport working with telethon 0.15.5 and sleekxmpp 1.3.2
[FIX] fixed everlasting authorization requests. if you got deauth message — ignore it, FROM subscription is enough.
[ADD] implemented roster exchange via XEP-0144
[ADD] we will send authorization request when unknown contact sent us a message
[ADD] correct presence handling for transport and users
[ADD] fixed presence spam (by default, we updating presence once for 60 seconds -- look at `status_update_interval` in mtproto.py)
[ADD] we will automatically connect to all actual sessions after transport start
This commit is contained in:
annelin
2018-06-19 05:09:38 +00:00
commit bca6860159
11 changed files with 1320 additions and 0 deletions

3
xmpp_tg/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from xmpp_tg.xmpp import XMPPTelegram
__version__ = 15

22
xmpp_tg/monkey.py Normal file
View File

@@ -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

499
xmpp_tg/mtproto.py Normal file
View File

@@ -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 = '<Unknown User>'
if message.fwd_from.channel_id: # От канала
fwd_from = '<Channel {}>'.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())

63
xmpp_tg/utils.py Normal file
View File

@@ -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 + "}"

605
xmpp_tg/xmpp.py Normal file
View File

@@ -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<mid>[\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 = "<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))
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