2017-08-23 01:55:49 +03:00
|
|
|
import os
|
2016-11-26 14:04:02 +03:00
|
|
|
from getpass import getpass
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-09-04 12:24:10 +03:00
|
|
|
from telethon import TelegramClient, ConnectionMode
|
2017-06-10 12:47:51 +03:00
|
|
|
from telethon.errors import SessionPasswordNeededError
|
2017-10-01 21:12:27 +03:00
|
|
|
from telethon.tl.types import (
|
|
|
|
UpdateShortChatMessage, UpdateShortMessage, PeerChat
|
|
|
|
)
|
2017-06-08 15:00:27 +03:00
|
|
|
from telethon.utils import get_display_name
|
2016-11-30 00:29:42 +03:00
|
|
|
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-05-29 18:07:09 +03:00
|
|
|
def sprint(string, *args, **kwargs):
|
|
|
|
"""Safe Print (handle UnicodeEncodeErrors on some terminals)"""
|
|
|
|
try:
|
|
|
|
print(string, *args, **kwargs)
|
|
|
|
except UnicodeEncodeError:
|
|
|
|
string = string.encode('utf-8', errors='ignore')\
|
|
|
|
.decode('ascii', errors='ignore')
|
|
|
|
print(string, *args, **kwargs)
|
|
|
|
|
|
|
|
|
2016-09-12 15:07:45 +03:00
|
|
|
def print_title(title):
|
2017-10-21 17:21:58 +03:00
|
|
|
"""Helper function to print titles to the console more nicely"""
|
|
|
|
sprint('\n')
|
|
|
|
sprint('=={}=='.format('=' * len(title)))
|
2017-05-29 18:07:09 +03:00
|
|
|
sprint('= {} ='.format(title))
|
2017-10-21 17:21:58 +03:00
|
|
|
sprint('=={}=='.format('=' * len(title)))
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
|
2016-09-17 18:04:30 +03:00
|
|
|
def bytes_to_string(byte_count):
|
|
|
|
"""Converts a byte count to a string (in KB, MB...)"""
|
|
|
|
suffix_index = 0
|
|
|
|
while byte_count >= 1024:
|
|
|
|
byte_count /= 1024
|
|
|
|
suffix_index += 1
|
|
|
|
|
2017-10-21 17:21:58 +03:00
|
|
|
return '{:.2f}{}'.format(
|
|
|
|
byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]
|
|
|
|
)
|
2016-09-17 18:04:30 +03:00
|
|
|
|
|
|
|
|
2016-09-12 15:07:45 +03:00
|
|
|
class InteractiveTelegramClient(TelegramClient):
|
2017-06-08 15:00:27 +03:00
|
|
|
"""Full featured Telegram client, meant to be used on an interactive
|
|
|
|
session to see what Telethon is capable off -
|
|
|
|
|
|
|
|
This client allows the user to perform some basic interaction with
|
|
|
|
Telegram through Telethon, such as listing dialogs (open chats),
|
|
|
|
talking to people, downloading media, and receiving updates.
|
|
|
|
"""
|
2017-06-16 10:34:09 +03:00
|
|
|
def __init__(self, session_user_id, user_phone, api_id, api_hash,
|
|
|
|
proxy=None):
|
2017-10-21 17:21:58 +03:00
|
|
|
"""
|
|
|
|
Initializes the InteractiveTelegramClient.
|
|
|
|
:param session_user_id: Name of the *.session file.
|
|
|
|
:param user_phone: The phone of the user that will login.
|
|
|
|
:param api_id: Telegram's api_id acquired through my.telegram.org.
|
|
|
|
:param api_hash: Telegram's api_hash.
|
|
|
|
:param proxy: Optional proxy tuple/dictionary.
|
|
|
|
"""
|
2016-09-12 15:07:45 +03:00
|
|
|
print_title('Initialization')
|
|
|
|
|
|
|
|
print('Initializing interactive example...')
|
2017-10-21 17:21:58 +03:00
|
|
|
|
|
|
|
# The first step is to initialize the TelegramClient, as we are
|
|
|
|
# subclassing it, we need to call super().__init__(). On a more
|
|
|
|
# normal case you would want 'client = TelegramClient(...)'
|
2017-09-04 12:24:10 +03:00
|
|
|
super().__init__(
|
2017-10-21 17:21:58 +03:00
|
|
|
# These parameters should be passed always, session name and API
|
2017-09-04 12:24:10 +03:00
|
|
|
session_user_id, api_id, api_hash,
|
2017-10-21 17:21:58 +03:00
|
|
|
|
|
|
|
# You can optionally change the connection mode by using this enum.
|
|
|
|
# This changes how much data will be sent over the network with
|
|
|
|
# every request, and how it will be formatted. Default is
|
|
|
|
# ConnectionMode.TCP_FULL, and smallest is TCP_TCP_ABRIDGED.
|
2017-09-07 21:17:40 +03:00
|
|
|
connection_mode=ConnectionMode.TCP_ABRIDGED,
|
2017-10-21 17:21:58 +03:00
|
|
|
|
|
|
|
# If you're using a proxy, set it here.
|
2017-10-01 20:56:47 +03:00
|
|
|
proxy=proxy,
|
2017-10-21 17:21:58 +03:00
|
|
|
|
|
|
|
# If you want to receive updates, you need to start one or more
|
|
|
|
# "update workers" which are background threads that will allow
|
|
|
|
# you to run things when your update handlers (callbacks) are
|
|
|
|
# called with an Update object.
|
2017-10-01 20:56:47 +03:00
|
|
|
update_workers=1
|
2017-09-04 12:24:10 +03:00
|
|
|
)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2018-01-16 20:36:50 +03:00
|
|
|
# Store {message.id: message} map here so that we can download
|
|
|
|
# media known the message ID, for every message having media.
|
|
|
|
self.found_media = {}
|
2016-09-12 16:39:23 +03:00
|
|
|
|
2017-10-21 17:21:58 +03:00
|
|
|
# Calling .connect() may return False, so you need to assert it's
|
|
|
|
# True before continuing. Otherwise you may want to retry as done here.
|
2016-09-12 15:07:45 +03:00
|
|
|
print('Connecting to Telegram servers...')
|
2017-05-30 14:27:23 +03:00
|
|
|
if not self.connect():
|
|
|
|
print('Initial connection failed. Retrying...')
|
|
|
|
if not self.connect():
|
|
|
|
print('Could not connect to Telegram servers.')
|
|
|
|
return
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-10-21 17:21:58 +03:00
|
|
|
# If the user hasn't called .sign_in() or .sign_up() yet, they won't
|
|
|
|
# be authorized. The first thing you must do is authorize. Calling
|
|
|
|
# .sign_in() should only be done once as the information is saved on
|
|
|
|
# the *.session file so you don't need to enter the code every time.
|
2016-09-12 15:07:45 +03:00
|
|
|
if not self.is_user_authorized():
|
|
|
|
print('First run. Sending code request...')
|
2017-10-21 17:21:58 +03:00
|
|
|
self.sign_in(user_phone)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-06-08 14:12:57 +03:00
|
|
|
self_user = None
|
|
|
|
while self_user is None:
|
2016-09-12 20:32:16 +03:00
|
|
|
code = input('Enter the code you just received: ')
|
2016-11-26 14:04:02 +03:00
|
|
|
try:
|
2017-10-21 17:21:58 +03:00
|
|
|
self_user = self.sign_in(code=code)
|
2016-11-26 14:04:02 +03:00
|
|
|
|
2017-10-21 17:21:58 +03:00
|
|
|
# Two-step verification may be enabled, and .sign_in will
|
|
|
|
# raise this error. If that's the case ask for the password.
|
|
|
|
# Note that getpass() may not work on PyCharm due to a bug,
|
|
|
|
# if that's the case simply change it for input().
|
2017-06-16 10:34:09 +03:00
|
|
|
except SessionPasswordNeededError:
|
2017-06-10 12:47:51 +03:00
|
|
|
pw = getpass('Two step verification is enabled. '
|
|
|
|
'Please enter your password: ')
|
|
|
|
|
|
|
|
self_user = self.sign_in(password=pw)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
def run(self):
|
2017-10-21 17:21:58 +03:00
|
|
|
"""Main loop of the TelegramClient, will wait for user action"""
|
|
|
|
|
|
|
|
# Once everything is ready, we can add an update handler. Every
|
|
|
|
# update object will be passed to the self.update_handler method,
|
|
|
|
# where we can process it as we need.
|
2016-09-12 15:07:45 +03:00
|
|
|
self.add_update_handler(self.update_handler)
|
|
|
|
|
|
|
|
# Enter a while loop to chat as long as the user wants
|
|
|
|
while True:
|
2017-10-21 17:21:58 +03:00
|
|
|
# Retrieve the top dialogs. You can set the limit to None to
|
|
|
|
# retrieve all of them if you wish, but beware that may take
|
|
|
|
# a long time if you have hundreds of them.
|
2017-10-02 19:59:29 +03:00
|
|
|
dialog_count = 15
|
2016-10-03 20:44:01 +03:00
|
|
|
|
|
|
|
# Entities represent the user, chat or channel
|
2017-10-21 17:21:58 +03:00
|
|
|
# corresponding to the dialog on the same index.
|
2018-01-02 11:56:37 +03:00
|
|
|
dialogs = self.get_dialogs(limit=dialog_count)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
i = None
|
|
|
|
while i is None:
|
2017-05-07 14:33:35 +03:00
|
|
|
print_title('Dialogs window')
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-05-07 14:33:35 +03:00
|
|
|
# Display them so the user can choose
|
2018-01-02 11:56:37 +03:00
|
|
|
for i, dialog in enumerate(dialogs, start=1):
|
|
|
|
sprint('{}. {}'.format(i, get_display_name(dialog.entity)))
|
2017-05-07 14:33:35 +03:00
|
|
|
|
|
|
|
# Let the user decide who they want to talk to
|
|
|
|
print()
|
|
|
|
print('> Who do you want to send messages to?')
|
|
|
|
print('> Available commands:')
|
|
|
|
print(' !q: Quits the dialogs window and exits.')
|
|
|
|
print(' !l: Logs out, terminating this session.')
|
|
|
|
print()
|
|
|
|
i = input('Enter dialog ID or a command: ')
|
|
|
|
if i == '!q':
|
|
|
|
return
|
|
|
|
if i == '!l':
|
2017-10-21 17:21:58 +03:00
|
|
|
# Logging out will cause the user to need to reenter the
|
|
|
|
# code next time they want to use the library, and will
|
|
|
|
# also delete the *.session file off the filesystem.
|
|
|
|
#
|
|
|
|
# This is not the same as simply calling .disconnect(),
|
|
|
|
# which simply shuts down everything gracefully.
|
2017-05-07 14:33:35 +03:00
|
|
|
self.log_out()
|
|
|
|
return
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-05-07 14:33:35 +03:00
|
|
|
try:
|
2016-09-12 15:07:45 +03:00
|
|
|
i = int(i if i else 0) - 1
|
2017-06-16 10:34:09 +03:00
|
|
|
# Ensure it is inside the bounds, otherwise retry
|
2016-09-12 15:07:45 +03:00
|
|
|
if not 0 <= i < dialog_count:
|
|
|
|
i = None
|
|
|
|
except ValueError:
|
2017-01-17 22:22:47 +03:00
|
|
|
i = None
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2016-10-03 20:44:01 +03:00
|
|
|
# Retrieve the selected user (or chat, or channel)
|
2018-01-02 11:56:37 +03:00
|
|
|
entity = dialogs[i].entity
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
# Show some information
|
2016-10-09 13:57:38 +03:00
|
|
|
print_title('Chat with "{}"'.format(get_display_name(entity)))
|
2016-09-16 14:35:14 +03:00
|
|
|
print('Available commands:')
|
2016-09-12 15:07:45 +03:00
|
|
|
print(' !q: Quits the current chat.')
|
2016-09-12 20:32:16 +03:00
|
|
|
print(' !Q: Quits the current chat and exits.')
|
2017-06-08 15:00:27 +03:00
|
|
|
print(' !h: prints the latest messages (message History).')
|
|
|
|
print(' !up <path>: Uploads and sends the Photo from path.')
|
|
|
|
print(' !uf <path>: Uploads and sends the File from path.')
|
2017-10-02 19:59:29 +03:00
|
|
|
print(' !d <msg-id>: Deletes a message by its id')
|
2017-06-08 15:00:27 +03:00
|
|
|
print(' !dm <msg-id>: Downloads the given message Media (if any).')
|
2016-10-03 20:44:01 +03:00
|
|
|
print(' !dp: Downloads the current dialog Profile picture.')
|
2016-09-16 14:35:14 +03:00
|
|
|
print()
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
# And start a while loop to chat
|
|
|
|
while True:
|
|
|
|
msg = input('Enter a message: ')
|
|
|
|
# Quit
|
|
|
|
if msg == '!q':
|
|
|
|
break
|
2016-09-12 20:32:16 +03:00
|
|
|
elif msg == '!Q':
|
|
|
|
return
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
# History
|
|
|
|
elif msg == '!h':
|
|
|
|
# First retrieve the messages and some information
|
2018-01-16 20:36:50 +03:00
|
|
|
messages = self.get_message_history(entity, limit=10)
|
2017-06-08 15:00:27 +03:00
|
|
|
|
|
|
|
# Iterate over all (in reverse order so the latest appear
|
|
|
|
# the last in the console) and print them with format:
|
|
|
|
# "[hh:mm] Sender: Message"
|
2018-01-16 20:36:50 +03:00
|
|
|
for msg in reversed(messages):
|
|
|
|
# Note that the .sender attribute is only there for
|
|
|
|
# convenience, the API returns it differently. But
|
|
|
|
# this shouldn't concern us. See the documentation
|
|
|
|
# for .get_message_history() for more information.
|
|
|
|
name = get_display_name(msg.sender)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
# Format the message content
|
2017-04-17 10:36:24 +03:00
|
|
|
if getattr(msg, 'media', None):
|
2018-01-16 20:36:50 +03:00
|
|
|
self.found_media[msg.id] = msg
|
2017-06-08 15:00:27 +03:00
|
|
|
# The media may or may not have a caption
|
|
|
|
caption = getattr(msg.media, 'caption', '')
|
|
|
|
content = '<{}> {}'.format(
|
|
|
|
type(msg.media).__name__, caption)
|
|
|
|
|
2017-04-17 10:36:24 +03:00
|
|
|
elif hasattr(msg, 'message'):
|
2016-09-12 15:07:45 +03:00
|
|
|
content = msg.message
|
2017-04-17 10:36:24 +03:00
|
|
|
elif hasattr(msg, 'action'):
|
|
|
|
content = str(msg.action)
|
|
|
|
else:
|
|
|
|
# Unknown message, simply print its class name
|
2017-06-08 15:00:27 +03:00
|
|
|
content = type(msg).__name__
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
# And print it to the user
|
2017-05-29 18:07:09 +03:00
|
|
|
sprint('[{}:{}] (ID={}) {}: {}'.format(
|
|
|
|
msg.date.hour, msg.date.minute, msg.id, name,
|
|
|
|
content))
|
2016-09-12 15:07:45 +03:00
|
|
|
|
|
|
|
# Send photo
|
2016-10-03 20:44:01 +03:00
|
|
|
elif msg.startswith('!up '):
|
2016-09-12 20:32:16 +03:00
|
|
|
# Slice the message to get the path
|
2017-01-17 22:22:47 +03:00
|
|
|
self.send_photo(path=msg[len('!up '):], entity=entity)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2016-09-12 20:32:16 +03:00
|
|
|
# Send file (document)
|
2016-10-03 20:44:01 +03:00
|
|
|
elif msg.startswith('!uf '):
|
2016-09-12 20:32:16 +03:00
|
|
|
# Slice the message to get the path
|
2017-01-17 22:22:47 +03:00
|
|
|
self.send_document(path=msg[len('!uf '):], entity=entity)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-10-02 19:59:29 +03:00
|
|
|
# Delete messages
|
|
|
|
elif msg.startswith('!d '):
|
|
|
|
# Slice the message to get message ID
|
|
|
|
deleted_msg = self.delete_messages(entity, msg[len('!d '):])
|
2018-01-16 20:36:50 +03:00
|
|
|
print('Deleted {}'.format(deleted_msg))
|
2017-10-02 19:59:29 +03:00
|
|
|
|
2016-09-12 16:39:23 +03:00
|
|
|
# Download media
|
2016-10-03 20:44:01 +03:00
|
|
|
elif msg.startswith('!dm '):
|
2016-09-12 20:32:16 +03:00
|
|
|
# Slice the message to get message ID
|
2017-08-23 01:55:49 +03:00
|
|
|
self.download_media_by_id(msg[len('!dm '):])
|
2016-09-12 16:39:23 +03:00
|
|
|
|
2016-10-03 20:44:01 +03:00
|
|
|
# Download profile photo
|
|
|
|
elif msg == '!dp':
|
2017-08-23 02:35:12 +03:00
|
|
|
print('Downloading profile picture to usermedia/...')
|
|
|
|
os.makedirs('usermedia', exist_ok=True)
|
|
|
|
output = self.download_profile_photo(entity, 'usermedia')
|
|
|
|
if output:
|
|
|
|
print(
|
|
|
|
'Profile picture downloaded to {}'.format(output)
|
|
|
|
)
|
2016-10-03 20:44:01 +03:00
|
|
|
else:
|
2018-01-16 20:36:50 +03:00
|
|
|
print('No profile picture found for this user!')
|
2016-10-03 20:44:01 +03:00
|
|
|
|
2016-09-12 15:07:45 +03:00
|
|
|
# Send chat message (if any)
|
|
|
|
elif msg:
|
2018-01-16 20:36:50 +03:00
|
|
|
self.send_message(entity, msg, link_preview=False)
|
2016-09-12 15:07:45 +03:00
|
|
|
|
2017-01-17 22:22:47 +03:00
|
|
|
def send_photo(self, path, entity):
|
2017-10-21 17:21:58 +03:00
|
|
|
"""Sends the file located at path to the desired entity as a photo"""
|
2017-08-23 01:55:49 +03:00
|
|
|
self.send_file(
|
|
|
|
entity, path,
|
|
|
|
progress_callback=self.upload_progress_callback
|
|
|
|
)
|
2016-09-12 20:32:16 +03:00
|
|
|
print('Photo sent!')
|
|
|
|
|
2017-01-17 22:22:47 +03:00
|
|
|
def send_document(self, path, entity):
|
2017-10-21 17:21:58 +03:00
|
|
|
"""Sends the file located at path to the desired entity as a document"""
|
2017-08-23 01:55:49 +03:00
|
|
|
self.send_file(
|
|
|
|
entity, path,
|
|
|
|
force_document=True,
|
|
|
|
progress_callback=self.upload_progress_callback
|
|
|
|
)
|
2016-09-12 20:32:16 +03:00
|
|
|
print('Document sent!')
|
|
|
|
|
2017-08-23 01:55:49 +03:00
|
|
|
def download_media_by_id(self, media_id):
|
2017-10-21 17:21:58 +03:00
|
|
|
"""Given a message ID, finds the media this message contained and
|
|
|
|
downloads it.
|
|
|
|
"""
|
2016-09-12 20:32:16 +03:00
|
|
|
try:
|
2018-01-16 20:36:50 +03:00
|
|
|
msg = self.found_media[int(media_id)]
|
|
|
|
except (ValueError, KeyError):
|
|
|
|
# ValueError when parsing, KeyError when accessing dictionary
|
|
|
|
print('Invalid media ID given or message not found!')
|
|
|
|
return
|
|
|
|
|
|
|
|
print('Downloading media to usermedia/...')
|
|
|
|
os.makedirs('usermedia', exist_ok=True)
|
|
|
|
output = self.download_media(
|
|
|
|
msg.media,
|
|
|
|
file='usermedia/',
|
|
|
|
progress_callback=self.download_progress_callback
|
|
|
|
)
|
|
|
|
print('Media downloaded to {}!'.format(output))
|
2016-09-12 20:32:16 +03:00
|
|
|
|
2016-09-17 18:04:30 +03:00
|
|
|
@staticmethod
|
|
|
|
def download_progress_callback(downloaded_bytes, total_bytes):
|
2017-10-01 21:12:27 +03:00
|
|
|
InteractiveTelegramClient.print_progress(
|
|
|
|
'Downloaded', downloaded_bytes, total_bytes
|
|
|
|
)
|
2016-09-17 18:04:30 +03:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def upload_progress_callback(uploaded_bytes, total_bytes):
|
2017-10-01 21:12:27 +03:00
|
|
|
InteractiveTelegramClient.print_progress(
|
|
|
|
'Uploaded', uploaded_bytes, total_bytes
|
|
|
|
)
|
2016-09-17 18:04:30 +03:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def print_progress(progress_type, downloaded_bytes, total_bytes):
|
2017-10-01 21:12:27 +03:00
|
|
|
print('{} {} out of {} ({:.2%})'.format(
|
|
|
|
progress_type, bytes_to_string(downloaded_bytes),
|
|
|
|
bytes_to_string(total_bytes), downloaded_bytes / total_bytes)
|
|
|
|
)
|
2016-09-17 18:04:30 +03:00
|
|
|
|
2017-10-01 21:12:27 +03:00
|
|
|
def update_handler(self, update):
|
2017-10-21 17:21:58 +03:00
|
|
|
"""Callback method for received Updates"""
|
|
|
|
|
|
|
|
# We have full control over what we want to do with the updates.
|
|
|
|
# In our case we only want to react to chat messages, so we use
|
|
|
|
# isinstance() to behave accordingly on these cases.
|
2017-10-01 21:12:27 +03:00
|
|
|
if isinstance(update, UpdateShortMessage):
|
|
|
|
who = self.get_entity(update.user_id)
|
|
|
|
if update.out:
|
|
|
|
sprint('>> "{}" to user {}'.format(
|
|
|
|
update.message, get_display_name(who)
|
|
|
|
))
|
2017-05-22 00:57:13 +03:00
|
|
|
else:
|
2017-11-14 11:48:40 +03:00
|
|
|
sprint('<< {} sent "{}"'.format(
|
2017-10-01 21:12:27 +03:00
|
|
|
get_display_name(who), update.message
|
|
|
|
))
|
|
|
|
|
|
|
|
elif isinstance(update, UpdateShortChatMessage):
|
|
|
|
which = self.get_entity(PeerChat(update.chat_id))
|
|
|
|
if update.out:
|
|
|
|
sprint('>> sent "{}" to chat {}'.format(
|
|
|
|
update.message, get_display_name(which)
|
|
|
|
))
|
2017-05-22 00:57:13 +03:00
|
|
|
else:
|
2017-10-01 21:12:27 +03:00
|
|
|
who = self.get_entity(update.from_id)
|
|
|
|
sprint('<< {} @ {} sent "{}"'.format(
|
|
|
|
get_display_name(which), get_display_name(who),
|
|
|
|
update.message
|
|
|
|
))
|