diff --git a/pyrogram/client/dispatcher/__init__.py b/pyrogram/client/dispatcher/__init__.py new file mode 100644 index 00000000..c0cb368a --- /dev/null +++ b/pyrogram/client/dispatcher/__init__.py @@ -0,0 +1,19 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from .dispatcher import Dispatcher diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py new file mode 100644 index 00000000..035e2aff --- /dev/null +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -0,0 +1,129 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import logging +import threading +from collections import OrderedDict +from queue import Queue +from threading import Thread + +import pyrogram +from pyrogram.api import types +from . import message_parser +from ..handler import ( + Handler, MessageHandler, EditedMessageHandler, + ChannelPostHandler, EditedChannelPostHandler +) + +log = logging.getLogger(__name__) + + +class Dispatcher: + def __init__(self, client, workers): + self.client = client + self.workers = workers + self.updates = Queue() + self.handlers = OrderedDict() + + def start(self): + for i in range(self.workers): + Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start() + + def stop(self): + for _ in range(self.workers): + self.updates.put(None) + + def add_handler(self, handler: Handler, group: int): + if group not in self.handlers: + self.handlers[group] = {} + self.handlers = OrderedDict(sorted(self.handlers.items())) + + if type(handler) not in self.handlers[group]: + self.handlers[group][type(handler)] = handler + else: + raise ValueError( + "'{0}' is already registered in Group #{1}. " + "You can register a different handler in this group or another '{0}' in a different group".format( + type(handler).__name__, + group + ) + ) + + def dispatch(self, update): + if update.message: + key = MessageHandler + elif update.edited_message: + key = EditedMessageHandler + elif update.channel_post: + key = ChannelPostHandler + elif update.edited_channel_post: + key = EditedChannelPostHandler + else: + return + + for group in self.handlers.values(): + handler = group.get(key, None) + + if handler is not None: + handler.callback(self.client, update) + + def update_worker(self): + name = threading.current_thread().name + log.debug("{} started".format(name)) + + while True: + update = self.updates.get() + + if update is None: + break + + try: + users = {i.id: i for i in update[1]} + chats = {i.id: i for i in update[2]} + update = update[0] + + valid_updates = (types.UpdateNewMessage, types.UpdateNewChannelMessage, + types.UpdateEditMessage, types.UpdateEditChannelMessage) + + if isinstance(update, valid_updates): + message = update.message + + if isinstance(message, types.Message): + m = message_parser.parse_message(self.client, message, users, chats) + elif isinstance(message, types.MessageService): + m = message_parser.parse_message_service(self.client, message, users, chats) + else: + continue + else: + continue + + edit = isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)) + + self.dispatch( + pyrogram.Update( + update_id=0, + message=(m if m.chat.type is not "channel" else None) if not edit else None, + edited_message=(m if m.chat.type is not "channel" else None) if edit else None, + channel_post=(m if m.chat.type is "channel" else None) if not edit else None, + edited_channel_post=(m if m.chat.type is "channel" else None) if edit else None + ) + ) + except Exception as e: + log.error(e, exc_info=True) + + log.debug("{} stopped".format(name)) diff --git a/pyrogram/client/dispatcher/message_parser.py b/pyrogram/client/dispatcher/message_parser.py new file mode 100644 index 00000000..a101b78e --- /dev/null +++ b/pyrogram/client/dispatcher/message_parser.py @@ -0,0 +1,572 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from base64 import b64encode, b64decode +from struct import pack + +import pyrogram +from pyrogram.api import types + +# TODO: Organize the code better? + +ENTITIES = { + types.MessageEntityMention.ID: "mention", + types.MessageEntityHashtag.ID: "hashtag", + types.MessageEntityBotCommand.ID: "bot_command", + types.MessageEntityUrl.ID: "url", + types.MessageEntityEmail.ID: "email", + types.MessageEntityBold.ID: "bold", + types.MessageEntityItalic.ID: "italic", + types.MessageEntityCode.ID: "code", + types.MessageEntityPre.ID: "pre", + types.MessageEntityTextUrl.ID: "text_link", + types.MessageEntityMentionName.ID: "text_mention" +} + + +def parse_entities(entities: list, users: dict) -> list: + output_entities = [] + + for entity in entities: + entity_type = ENTITIES.get(entity.ID, None) + + if entity_type: + output_entities.append(pyrogram.MessageEntity( + type=entity_type, + offset=entity.offset, + length=entity.length, + url=getattr(entity, "url", None), + user=parse_user( + users.get( + getattr(entity, "user_id", None), + None + ) + ) + )) + + return output_entities + + +def parse_user(user: types.User) -> pyrogram.User or None: + return pyrogram.User( + id=user.id, + is_bot=user.bot, + first_name=user.first_name, + last_name=user.last_name, + username=user.username, + language_code=user.lang_code + ) if user else None + + +def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat: + if isinstance(message.to_id, types.PeerUser): + return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id]) + elif isinstance(message.to_id, types.PeerChat): + return parse_chat_chat(chats[message.to_id.chat_id]) + else: + return parse_channel_chat(chats[message.to_id.channel_id]) + + +def parse_user_chat(user: types.User) -> pyrogram.Chat: + return pyrogram.Chat( + id=user.id, + type="private", + username=user.username, + first_name=user.first_name, + last_name=user.last_name + ) + + +def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: + return pyrogram.Chat( + id=-chat.id, + type="group", + title=chat.title, + all_members_are_administrators=not chat.admins_enabled + ) + + +def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat: + return pyrogram.Chat( + id=int("-100" + str(channel.id)), + type="supergroup" if channel.megagroup else "channel", + title=channel.title, + username=channel.username + ) + + +def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None: + if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)): + loc = thumb.location + + if isinstance(thumb, types.PhotoSize): + file_size = thumb.size + else: + file_size = len(thumb.bytes) + + if isinstance(loc, types.FileLocation): + return pyrogram.PhotoSize( + file_id=encode( + pack( + " pyrogram.Message: + entities = parse_entities(message.entities, users) + + forward_from = None + forward_from_chat = None + forward_from_message_id = None + forward_signature = None + forward_date = None + + forward_header = message.fwd_from # type: types.MessageFwdHeader + + if forward_header: + forward_date = forward_header.date + + if forward_header.from_id: + forward_from = parse_user(users[forward_header.from_id]) + else: + forward_from_chat = parse_channel_chat(chats[forward_header.channel_id]) + forward_from_message_id = forward_header.channel_post + forward_signature = forward_header.post_author + + photo = None + location = None + contact = None + venue = None + audio = None + voice = None + video = None + video_note = None + sticker = None + document = None + + media = message.media + + if media: + if isinstance(media, types.MessageMediaPhoto): + photo = media.photo + + if isinstance(photo, types.Photo): + sizes = photo.sizes + photo_sizes = [] + + for size in sizes: + if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): + loc = size.location + + if isinstance(size, types.PhotoSize): + file_size = size.size + else: + file_size = len(size.bytes) + + if isinstance(loc, types.FileLocation): + photo_size = pyrogram.PhotoSize( + file_id=encode( + pack( + " pyrogram.Message: + action = message.action + + new_chat_members = None + left_chat_member = None + new_chat_title = None + delete_chat_photo = None + migrate_to_chat_id = None + migrate_from_chat_id = None + group_chat_created = None + channel_chat_created = None + new_chat_photo = None + + if isinstance(action, types.MessageActionChatAddUser): + new_chat_members = [parse_user(users[i]) for i in action.users] + elif isinstance(action, types.MessageActionChatJoinedByLink): + new_chat_members = [parse_user(users[action.inviter_id])] + elif isinstance(action, types.MessageActionChatDeleteUser): + left_chat_member = parse_user(users[action.user_id]) + elif isinstance(action, types.MessageActionChatEditTitle): + new_chat_title = action.title + elif isinstance(action, types.MessageActionChatDeletePhoto): + delete_chat_photo = True + elif isinstance(action, types.MessageActionChatMigrateTo): + migrate_to_chat_id = action.channel_id + elif isinstance(action, types.MessageActionChannelMigrateFrom): + migrate_from_chat_id = action.chat_id + elif isinstance(action, types.MessageActionChatCreate): + group_chat_created = True + elif isinstance(action, types.MessageActionChannelCreate): + channel_chat_created = True + elif isinstance(action, types.MessageActionChatEditPhoto): + photo = action.photo + + if isinstance(photo, types.Photo): + sizes = photo.sizes + photo_sizes = [] + + for size in sizes: + if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): + loc = size.location + + if isinstance(size, types.PhotoSize): + file_size = size.size + else: + file_size = len(size.bytes) + + if isinstance(loc, types.FileLocation): + photo_size = pyrogram.PhotoSize( + file_id=encode( + pack( + " bytes: + s = b64decode(s + "=" * (-len(s) % 4), "-_") + r = b"" + + assert s[-1] == 2 + + i = 0 + while i < len(s) - 1: + if s[i] != 0: + r += bytes([s[i]]) + else: + r += b"\x00" * s[i + 1] + i += 1 + + i += 1 + + return r + + +def encode(s: bytes) -> str: + r = b"" + n = 0 + + for i in s + bytes([2]): + if i == 0: + n += 1 + else: + if n: + r += b"\x00" + bytes([n]) + n = 0 + + r += bytes([i]) + + return b64encode(r, b"-_").decode().rstrip("=") diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py deleted file mode 100644 index 39512414..00000000 --- a/pyrogram/client/message_parser.py +++ /dev/null @@ -1 +0,0 @@ -from base64 import b64encode, b64decode from struct import pack import pyrogram from pyrogram.api import types # TODO: Organize the code better? ENTITIES = { types.MessageEntityMention.ID: "mention", types.MessageEntityHashtag.ID: "hashtag", types.MessageEntityBotCommand.ID: "bot_command", types.MessageEntityUrl.ID: "url", types.MessageEntityEmail.ID: "email", types.MessageEntityBold.ID: "bold", types.MessageEntityItalic.ID: "italic", types.MessageEntityCode.ID: "code", types.MessageEntityPre.ID: "pre", types.MessageEntityTextUrl.ID: "text_link", types.MessageEntityMentionName.ID: "text_mention" } def parse_entities(entities: list, users: dict) -> list: output_entities = [] for entity in entities: entity_type = ENTITIES.get(entity.ID, None) if entity_type: output_entities.append(pyrogram.MessageEntity( type=entity_type, offset=entity.offset, length=entity.length, url=getattr(entity, "url", None), user=parse_user( users.get( getattr(entity, "user_id", None), None ) ) )) return output_entities def parse_user(user: types.User) -> pyrogram.User or None: return pyrogram.User( id=user.id, is_bot=user.bot, first_name=user.first_name, last_name=user.last_name, username=user.username, language_code=user.lang_code ) if user else None def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat: if isinstance(message.to_id, types.PeerUser): return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id]) elif isinstance(message.to_id, types.PeerChat): return parse_chat_chat(chats[message.to_id.chat_id]) else: return parse_channel_chat(chats[message.to_id.channel_id]) def parse_user_chat(user: types.User) -> pyrogram.Chat: return pyrogram.Chat( id=user.id, type="private", username=user.username, first_name=user.first_name, last_name=user.last_name ) def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: return pyrogram.Chat( id=-chat.id, type="group", title=chat.title, all_members_are_administrators=not chat.admins_enabled ) def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat: return pyrogram.Chat( id=int("-100" + str(channel.id)), type="supergroup" if channel.megagroup else "channel", title=channel.title, username=channel.username ) def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None: if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)): loc = thumb.location if isinstance(thumb, types.PhotoSize): file_size = thumb.size else: file_size = len(thumb.bytes) if isinstance(loc, types.FileLocation): return pyrogram.PhotoSize( file_id=encode( pack( " pyrogram.Message: entities = parse_entities(message.entities, users) forward_from = None forward_from_chat = None forward_from_message_id = None forward_signature = None forward_date = None forward_header = message.fwd_from # type: types.MessageFwdHeader if forward_header: forward_date = forward_header.date if forward_header.from_id: forward_from = parse_user(users[forward_header.from_id]) else: forward_from_chat = parse_channel_chat(chats[forward_header.channel_id]) forward_from_message_id = forward_header.channel_post forward_signature = forward_header.post_author photo = None location = None contact = None venue = None audio = None voice = None video = None video_note = None sticker = None document = None media = message.media if media: if isinstance(media, types.MessageMediaPhoto): photo = media.photo if isinstance(photo, types.Photo): sizes = photo.sizes photo_sizes = [] for size in sizes: if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): loc = size.location if isinstance(size, types.PhotoSize): file_size = size.size else: file_size = len(size.bytes) if isinstance(loc, types.FileLocation): photo_size = pyrogram.PhotoSize( file_id=encode( pack( " pyrogram.Message: action = message.action new_chat_members = None left_chat_member = None new_chat_title = None delete_chat_photo = None migrate_to_chat_id = None migrate_from_chat_id = None group_chat_created = None channel_chat_created = None new_chat_photo = None if isinstance(action, types.MessageActionChatAddUser): new_chat_members = [parse_user(users[i]) for i in action.users] elif isinstance(action, types.MessageActionChatJoinedByLink): new_chat_members = [parse_user(users[action.inviter_id])] elif isinstance(action, types.MessageActionChatDeleteUser): left_chat_member = parse_user(users[action.user_id]) elif isinstance(action, types.MessageActionChatEditTitle): new_chat_title = action.title elif isinstance(action, types.MessageActionChatDeletePhoto): delete_chat_photo = True elif isinstance(action, types.MessageActionChatMigrateTo): migrate_to_chat_id = action.channel_id elif isinstance(action, types.MessageActionChannelMigrateFrom): migrate_from_chat_id = action.chat_id elif isinstance(action, types.MessageActionChatCreate): group_chat_created = True elif isinstance(action, types.MessageActionChannelCreate): channel_chat_created = True elif isinstance(action, types.MessageActionChatEditPhoto): photo = action.photo if isinstance(photo, types.Photo): sizes = photo.sizes photo_sizes = [] for size in sizes: if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): loc = size.location if isinstance(size, types.PhotoSize): file_size = size.size else: file_size = len(size.bytes) if isinstance(loc, types.FileLocation): photo_size = pyrogram.PhotoSize( file_id=encode( pack( " bytes: s = b64decode(s + "=" * (-len(s) % 4), "-_") r = b"" assert s[-1] == 2 i = 0 while i < len(s) - 1: if s[i] != 0: r += bytes([s[i]]) else: r += b"\x00" * s[i + 1] i += 1 i += 1 return r def encode(s: bytes) -> str: r = b"" n = 0 for i in s + bytes([2]): if i == 0: n += 1 else: if n: r += b"\x00" + bytes([n]) n = 0 r += bytes([i]) return b64encode(r, b"-_").decode().rstrip("=") \ No newline at end of file