[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:
3
xmpp_tg/__init__.py
Normal file
3
xmpp_tg/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from xmpp_tg.xmpp import XMPPTelegram
|
||||
|
||||
__version__ = 15
|
||||
22
xmpp_tg/monkey.py
Normal file
22
xmpp_tg/monkey.py
Normal 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
499
xmpp_tg/mtproto.py
Normal 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
63
xmpp_tg/utils.py
Normal 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
605
xmpp_tg/xmpp.py
Normal 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
|
||||
Reference in New Issue
Block a user