[SVN] release 0.4.0
[SVN] all comments in code now in English; Sofia ♥ Fuck you with this barbarian language in code comments! (no, no fuck please) [FIX] monkey-patched telethon library to fix updates receiving in some sessions (fix github issue #686; commit b20aa0ccc91b3d767c26702f3611c44772d87f5a) [FIX] fixed "normal" quotation, such as `> text`, in previous releases it was intended to be message id to reply [FIX] now processing bots in contact list, in roster with `b` prefix [FIX] fixed message editing and deleting in supergroups [FIX] now removing old telegram session from database when initiating a new one for current JID [FIX] fixed group creating; group, supergroup, channels creating is now OK and tested [FIX] fixed processing bot in roster; now it does not crashes gateway [UPD] default status update interval is now 30 [UPD] slighly (and finally) changed behaviour of presences: `Last seen recently` = `dnd (do not disturb)`, because user that enabled this privacy settings does not want you to disturb `Last seen a long time ago` = `xa (extended away)`, because it is 'long' away 'Last seen at %date%` = `away`, because user just got away [ADD] basic interaction with bots (now only with text commands)
This commit is contained in:
140
xmpp_tg/xmpp.py
140
xmpp_tg/xmpp.py
@@ -29,16 +29,16 @@ from telethon.errors import SessionPasswordNeededError
|
||||
|
||||
from xmpp_tg.mtproto import TelegramGateClient
|
||||
from xmpp_tg.utils import var_dump, display_tg_name, get_contact_jid, localtime
|
||||
import xmpp_tg.monkey # Патчим баги в библиотеках
|
||||
import xmpp_tg.monkey # monkeypatch
|
||||
|
||||
class XMPPTelegram(ComponentXMPP):
|
||||
"""
|
||||
Класс XMPP компонента транспорта между Telegram и Jabber
|
||||
Main XMPPTelegram class.
|
||||
"""
|
||||
|
||||
def __init__(self, config_dict):
|
||||
"""
|
||||
Инициализация, подключение плагинов и регистрация событий
|
||||
Transport initialization
|
||||
:param config_dict:
|
||||
"""
|
||||
|
||||
@@ -86,14 +86,14 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Деструктор. Теоретически.
|
||||
Destructor
|
||||
:return:
|
||||
"""
|
||||
self.db_connection.close()
|
||||
|
||||
def handle_start(self, arg):
|
||||
"""
|
||||
Обработчик события успешного подключения компонента к Jabber серверу
|
||||
Successful connection to Jabber server
|
||||
:param arg:
|
||||
:return:
|
||||
"""
|
||||
@@ -104,47 +104,49 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def message(self, iq):
|
||||
"""
|
||||
Обработчик входящих сообщений из XMPP
|
||||
Message from XMPP
|
||||
:param iq:
|
||||
:return:
|
||||
"""
|
||||
jid = iq['from'].bare
|
||||
|
||||
if iq['to'] == self.config['jid'] and iq['type'] == 'chat': # Пишут транспорту
|
||||
if iq['to'] == self.config['jid'] and iq['type'] == 'chat': # message to gateway
|
||||
if iq['body'].startswith('!'):
|
||||
self.process_command(iq)
|
||||
else:
|
||||
self.gate_reply_message(iq, 'Only commands accepted. Try !help for more info.')
|
||||
else: # Пишут в Telegram
|
||||
else: # --- outgoing message ---
|
||||
if jid in self.tg_connections and self.tg_connections[jid].is_user_authorized():
|
||||
if iq['body'].startswith('!'): # Команда из чата
|
||||
if iq['to'].bare.startswith('u'):
|
||||
if iq['body'].startswith('!'): # it is command!
|
||||
if iq['to'].bare.startswith( ('u', 'b') ):
|
||||
self.process_chat_user_command(iq)
|
||||
elif iq['to'].bare.startswith('g') or iq['to'].bare.startswith('s') or iq['to'].bare.startswith('c'):
|
||||
self.process_chat_group_command(iq)
|
||||
else:
|
||||
self.gate_reply_message(iq, 'Error.')
|
||||
else: # Обычное сообщение
|
||||
else: # -- normal message --
|
||||
tg_id = int(iq['to'].node[1:])
|
||||
tg_peer = None
|
||||
msg = iq['body']
|
||||
reply_mid = None
|
||||
|
||||
if msg.startswith('>'): # Проверка на цитирование
|
||||
if msg.startswith('>'): # quoting check
|
||||
msg_lines = msg.split('\n')
|
||||
matched = re.match(r'>[ ]*(?P<mid>[\d]+)[ ]*', msg_lines[0]).groupdict()
|
||||
matched = re.match(r'>[ ]*(?P<mid>[\d]+)[ ]*', msg_lines[0])
|
||||
matched = matched.groupdict() if matched else {}
|
||||
|
||||
if 'mid' in matched: # Если нашли ID сообщения, то указываем ответ
|
||||
if 'mid' in matched: # citation
|
||||
reply_mid = int(matched['mid'])
|
||||
msg = '\n'.join(msg_lines[1:])
|
||||
|
||||
if iq['to'].bare.startswith('u'): # Обычный пользователь
|
||||
if iq['to'].bare.startswith( ('u', 'b') ): # normal user
|
||||
tg_peer = InputPeerUser(tg_id, self.tg_dialogs[jid]['users'][tg_id].access_hash)
|
||||
elif iq['to'].bare.startswith('g'): # Обычная группа
|
||||
elif iq['to'].bare.startswith('g'): # generic group
|
||||
tg_peer = InputPeerChat(tg_id)
|
||||
elif iq['to'].bare.startswith('s') or iq['to'].bare.startswith('c'): # Супергруппа
|
||||
elif iq['to'].bare.startswith( ('s', 'c') ): # supergroup
|
||||
tg_peer = InputPeerChannel(tg_id, self.tg_dialogs[jid]['supergroups'][tg_id].access_hash)
|
||||
|
||||
# peer OK.
|
||||
if tg_peer:
|
||||
result = None
|
||||
|
||||
@@ -162,30 +164,19 @@ class XMPPTelegram(ComponentXMPP):
|
||||
if not result:
|
||||
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: # ЛС / Группа
|
||||
# find sent message id and save it
|
||||
if result and hasattr(result, 'id'): # update id
|
||||
msg_id = result.id
|
||||
|
||||
# last message id for current peer
|
||||
self.tg_dialogs[jid]['messages'][tg_id] = {'id': msg_id, 'body': msg}
|
||||
|
||||
# if msg_id:
|
||||
# # Отправляем ответ с ID отправленного сообщения
|
||||
# self.send_message(mto=iq['from'], mfrom=iq['to'], mtype='chat',
|
||||
# mbody='[Your MID:{}]'.format(msg_id))
|
||||
self.tg_dialogs[jid]['messages'][tg_id] = {'id': msg_id, 'body': msg}
|
||||
#self.send_message(mto=iq['from'], mfrom=iq['to'], mtype='chat', mbody='[Your MID:{}]'.format(msg_id))
|
||||
|
||||
|
||||
def event_presence_unsub(self, presence):
|
||||
return
|
||||
|
||||
def event_presence(self, presence):
|
||||
"""
|
||||
Обработчик события subscribe
|
||||
Presence handler
|
||||
:param presence:
|
||||
:return:
|
||||
"""
|
||||
@@ -213,7 +204,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def handle_online(self, event, sync_roster = True):
|
||||
"""
|
||||
Обработчик события online. Подключается к Telegram при наличии авторизации.
|
||||
Gateway's subscriber comes online
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
@@ -240,7 +231,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def handle_offline(self, event):
|
||||
"""
|
||||
Обработчик события offline. Отключается от Telegram, если было создано подключение.
|
||||
Gateway's subscriber comes offline.
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
@@ -255,6 +246,11 @@ class XMPPTelegram(ComponentXMPP):
|
||||
self.tg_connections[jid].disconnect()
|
||||
|
||||
def handle_interrupt(self, signal, frame):
|
||||
"""
|
||||
Interrupted (Ctrl+C).
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
|
||||
for jid in self.tg_connections:
|
||||
print('Disconnecting: %s' % jid)
|
||||
@@ -267,7 +263,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def process_command(self, iq):
|
||||
"""
|
||||
Обработчик общих команд транспорта
|
||||
Commands to gateway, users or chats (starts with !)
|
||||
:param iq:
|
||||
:return:
|
||||
"""
|
||||
@@ -311,11 +307,13 @@ class XMPPTelegram(ComponentXMPP):
|
||||
elif parsed[0] == '!login': # --------------------------------------------------
|
||||
self.gate_reply_message(iq, 'Please wait...')
|
||||
self.spawn_tg_client(jid, parsed[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:
|
||||
# remove old sessions for this JID #
|
||||
self.db_connection.execute("DELETE from accounts where jid = ?", (jid, ) )
|
||||
self.tg_connections[jid].send_code_request(parsed[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.')
|
||||
@@ -389,7 +387,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
groupuser = self.tg_connections[jid].get_entity(parsed[2])
|
||||
|
||||
# we re ready to make group
|
||||
self.tg_connections[jid].invoke(CreateChatRequest([groupusers], groupname))
|
||||
self.tg_connections[jid].invoke(CreateChatRequest([groupuser], groupname))
|
||||
self.tg_process_dialogs(jid)
|
||||
|
||||
elif parsed[0] == '!channel' and len(parsed) >= 2: # create new channel
|
||||
@@ -523,11 +521,11 @@ class XMPPTelegram(ComponentXMPP):
|
||||
tg_id = int(iq['to'].node[1:])
|
||||
if tg_id in self.tg_dialogs[jid]['supergroups']:
|
||||
kicked_user = self.tg_connections[jid].get_entity(parsed[1])
|
||||
if type(invited_user) == User:
|
||||
if type(kicked_user) == User:
|
||||
self.tg_connections[jid].invoke(EditBannedRequest( InputPeerChannel(tg_id, self.tg_dialogs[jid]['supergroups'][tg_id].access_hash), kicked_user, ChannelBannedRights(until_date=None,view_messages=True) ) )
|
||||
if tg_id in self.tg_dialogs[jid]['groups']:
|
||||
kicked_user = self.tg_connections[jid].get_entity(parsed[1])
|
||||
if type(invited_user) == User:
|
||||
if type(kicked_user) == User:
|
||||
self.tg_connections[jid].invoke( DeleteChatUserRequest(tg_id, kicked_user) )
|
||||
|
||||
elif iq['body'].startswith('!s/'):
|
||||
@@ -553,7 +551,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def spawn_tg_client(self, jid, phone):
|
||||
"""
|
||||
Создает и инициализирует подключение к Telegram
|
||||
Spawns Telegram client
|
||||
:param jid:
|
||||
:param phone:
|
||||
:return:
|
||||
@@ -572,17 +570,17 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def init_tg(self, jid):
|
||||
"""
|
||||
Инициализация транспорта для конкретного пользователя после подключения к Telegram
|
||||
Initialize
|
||||
:param jid:
|
||||
:return:
|
||||
"""
|
||||
# Устанавливаем, что пользователь онлайн
|
||||
# Set status = Online
|
||||
self.tg_connections[jid].invoke(UpdateStatusRequest(offline=False))
|
||||
|
||||
# Получаем и обрабатываем список диалогов
|
||||
# Process Telegram contact list
|
||||
self.tg_process_dialogs(jid, sync_roster = False)
|
||||
|
||||
# Регистрируем обработчик обновлений в Telegram
|
||||
# Register Telegrap updates handler
|
||||
self.tg_connections[jid].add_update_handler(self.tg_connections[jid].xmpp_update_handler)
|
||||
|
||||
def roster_exchange(self, tojid, contacts):
|
||||
@@ -615,7 +613,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
print('Processing dialogs...')
|
||||
|
||||
# Инициализируем словари для диалогов
|
||||
# dialogs dictonaries
|
||||
self.tg_dialogs[jid] = dict()
|
||||
self.tg_dialogs[jid]['raw'] = list()
|
||||
self.tg_dialogs[jid]['users'] = dict()
|
||||
@@ -623,7 +621,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
self.tg_dialogs[jid]['supergroups'] = dict()
|
||||
self.tg_dialogs[jid]['messages'] = dict()
|
||||
|
||||
# Оффсеты для получения диалогов
|
||||
# offsets
|
||||
last_peer = InputPeerEmpty()
|
||||
last_msg_id = 0
|
||||
last_date = None
|
||||
@@ -631,7 +629,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
# roster exchange #
|
||||
self.contact_list[jid] = dict()
|
||||
|
||||
while True: # В цикле по кускам получаем все диалоги
|
||||
while True:
|
||||
dlgs = self.tg_connections[jid].invoke(GetDialogsRequest(offset_date=last_date, offset_id=last_msg_id,
|
||||
offset_peer=last_peer, limit=100))
|
||||
|
||||
@@ -640,9 +638,9 @@ class XMPPTelegram(ComponentXMPP):
|
||||
for usr in dlgs.users:
|
||||
self.tg_dialogs[jid]['users'][usr.id] = usr
|
||||
for cht in dlgs.chats:
|
||||
if type(cht) in [Chat, ChatForbidden]: # Старая группа
|
||||
if type(cht) in [Chat, ChatForbidden]: # normal group
|
||||
self.tg_dialogs[jid]['groups'][cht.id] = cht
|
||||
elif type(cht) in [Channel, ChannelForbidden]: # Супергруппа
|
||||
elif type(cht) in [Channel, ChannelForbidden]: # supergroup
|
||||
self.tg_dialogs[jid]['supergroups'][cht.id] = cht
|
||||
|
||||
for dlg in dlgs.dialogs:
|
||||
@@ -660,18 +658,20 @@ class XMPPTelegram(ComponentXMPP):
|
||||
vcard['DESC'] = 'This user no longer exists in Telegram'
|
||||
else:
|
||||
rostername = display_tg_name(usr)
|
||||
rostername = '[B] ' + rostername if usr.bot else rostername
|
||||
|
||||
vcard['FN'] = display_tg_name(usr)
|
||||
vcard['DESC'] = ''
|
||||
if usr.first_name:
|
||||
vcard['N']['GIVEN'] = usr.first_name
|
||||
if usr.last_name:
|
||||
vcard['N']['FAMILY'] = usr.last_name
|
||||
if usr.username:
|
||||
vcard['DESC'] = 'Telegram Username: @' + usr.username
|
||||
if usr.phone:
|
||||
vcard['DESC'] += "\n" + 'Phone number: ' + usr.phone
|
||||
|
||||
if usr.bot:
|
||||
vcard['DESC'] += ' [Bot]'
|
||||
|
||||
vcard['NICKNAME'] = vcard['FN']
|
||||
vcard['NICKNAME'] = vcard['FN']
|
||||
|
||||
# add photo to VCard #
|
||||
photo, photosha1hash = self.get_peer_photo(jid, usr) if sync_roster else (None, None)
|
||||
@@ -692,21 +692,21 @@ class XMPPTelegram(ComponentXMPP):
|
||||
if type(usr.status) is UserStatusOnline:
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pstatus = 'Online' )
|
||||
elif type(usr.status) is UserStatusRecently:
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pshow='away', pstatus='Last seen recently')
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pshow='dnd', pstatus='Last seen recently')
|
||||
elif type(usr.status) is UserStatusOffline:
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pshow='xa', pstatus=localtime(usr.status.was_online).strftime('Last seen at %H:%M %d/%m/%Y') )
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pshow='away', pstatus=localtime(usr.status.was_online).strftime('Last seen at %H:%M %d/%m/%Y') )
|
||||
else:
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pshow='dnd', pstatus='Last seen a long time ago')
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pshow='xa', pstatus='Last seen a long time ago')
|
||||
|
||||
if type(dlg.peer) in [PeerChat, PeerChannel]:
|
||||
cht = None
|
||||
|
||||
if type(dlg.peer) is PeerChat: # Старая группа
|
||||
if type(dlg.peer) is PeerChat: # old group
|
||||
cht = self.tg_connections[jid].invoke(GetFullChatRequest(dlg.peer.chat_id))
|
||||
cht = cht.chats[0]
|
||||
if cht.deactivated or cht.left:
|
||||
cht = None
|
||||
elif type(dlg.peer) is PeerChannel: # Супергруппа
|
||||
elif type(dlg.peer) is PeerChannel: # supergroup
|
||||
cht = self.tg_dialogs[jid]['supergroups'][dlg.peer.channel_id]
|
||||
|
||||
|
||||
@@ -732,25 +732,25 @@ class XMPPTelegram(ComponentXMPP):
|
||||
self.send_presence(pto=jid, pfrom=u_jid, pshow = 'chat', pstatus = cht.title)
|
||||
|
||||
|
||||
if len(dlgs.dialogs) == 0: # Если все диалоги получены - прерываем цикл
|
||||
if len(dlgs.dialogs) == 0: # all dialogs was received.
|
||||
if sync_roster and 'use_roster_exchange' in self.accounts[jid] and self.accounts[jid]['use_roster_exchange'] == 'true':
|
||||
self.roster_exchange(jid, self.contact_list[jid])
|
||||
elif sync_roster:
|
||||
self.roster_fill(jid, self.contact_list[jid])
|
||||
break
|
||||
else: # Иначе строим оффсеты
|
||||
last_msg_id = dlgs.dialogs[-1].top_message # Нужен последний id сообщения. Наркоманы.
|
||||
else: # get next part of dialogs.
|
||||
last_msg_id = dlgs.dialogs[-1].top_message # we fucking need last msg id!
|
||||
last_peer = dlgs.dialogs[-1].peer
|
||||
|
||||
last_date = next(msg for msg in dlgs.messages # Ищем дату среди сообщений
|
||||
last_date = next(msg for msg in dlgs.messages # find date
|
||||
if type(msg.to_id) is type(last_peer) and msg.id == last_msg_id).date
|
||||
|
||||
if type(last_peer) is PeerUser: # Пользователь
|
||||
if type(last_peer) is PeerUser: # user/bot
|
||||
access_hash = self.tg_dialogs[jid]['users'][last_peer.user_id].access_hash
|
||||
last_peer = InputPeerUser(last_peer.user_id, access_hash)
|
||||
elif type(last_peer) in [Chat, ChatForbidden]: # Группа
|
||||
elif type(last_peer) in [Chat, ChatForbidden]: # normal group
|
||||
last_peer = InputPeerChat(last_peer.chat_id)
|
||||
elif type(last_peer) in [Channel, ChannelForbidden]: # Супергруппа
|
||||
elif type(last_peer) in [Channel, ChannelForbidden]: # supergroup/channel
|
||||
access_hash = self.tg_dialogs[jid]['supergroups'][last_peer.channel_id].access_hash
|
||||
last_peer = InputPeerChannel(last_peer.channel_id, access_hash)
|
||||
|
||||
@@ -759,7 +759,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def gate_reply_message(self, iq, msg):
|
||||
"""
|
||||
Отправляет ответное сообщение от имени транспорта
|
||||
Reply to message to gate.
|
||||
:param iq:
|
||||
:param msg:
|
||||
:return:
|
||||
@@ -808,7 +808,7 @@ class XMPPTelegram(ComponentXMPP):
|
||||
|
||||
def init_database(self):
|
||||
"""
|
||||
Инициализация БД
|
||||
Database initialization
|
||||
:return:
|
||||
"""
|
||||
def dict_factory(cursor, row):
|
||||
@@ -820,6 +820,6 @@ class XMPPTelegram(ComponentXMPP):
|
||||
conn = sqlite3.connect(self.config['db_connect'], isolation_level=None, check_same_thread=False)
|
||||
conn.row_factory = dict_factory
|
||||
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS accounts(jid VARCHAR(255), tg_phone VARCHAR(25), use_roster_exchange BOOLEAN default false, keep_online BOOLEAN default false, status_update_interval INTEGER default 60, enable_avatars BOOLEAN default false)")
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS accounts(jid VARCHAR(255), tg_phone VARCHAR(25), use_roster_exchange BOOLEAN default false, keep_online BOOLEAN default false, status_update_interval INTEGER default 30, enable_avatars BOOLEAN default false)")
|
||||
|
||||
return conn
|
||||
|
||||
Reference in New Issue
Block a user