diff --git a/LICENSE b/LICENSE index 39baeff7..7a430dd1 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 LonamiWebs +Copyright (c) 2016-2019 LonamiWebs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/telethon_examples/LICENSE b/telethon_examples/LICENSE new file mode 100644 index 00000000..670154e3 --- /dev/null +++ b/telethon_examples/LICENSE @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + diff --git a/telethon_examples/README.md b/telethon_examples/README.md new file mode 100644 index 00000000..acab382d --- /dev/null +++ b/telethon_examples/README.md @@ -0,0 +1,120 @@ +# Examples + +This folder contains several single-file examples using [Telethon]. + +## Requisites + +You should have the `telethon` library installed with `pip`. +Run `python3 -m pip install --upgrade telethon --user` if you don't +have it installed yet (this is the most portable way to install it). + +Examples assume you have the following environment variables defined: + +* `TG_API_ID`, this is your API ID from https://my.telegram.org. +* `TG_API_HASH`, this is your API hash from https://my.telegram.org. +* `TG_TOKEN`, this is your bot token from [@BotFather] for bot examples. +* `TG_SESSION`, this is the name of the `*.session` file to use. + +You can ignore any of these, and the scripts will prompt you to enter them. + +## Downloading Examples + +You may download all and run any example with by typing in a terminal: +```sh +git clone https://github.com/LonamiWebs/Telethon.git +cd Telethon +cd telethon_examples +python3 gui.py +``` + +You can also right-click the title of any example and use "Save Link As…" to +download only a particular example. + +All examples are licensed under the [CC0 License], so you can use +them as the base for your own code without worrying about copyright. + +## Available Examples + +### [`print_updates.py`] + +* Usable as a: **user and bot**. +* Difficulty: **easy**. + +Trivial example that just prints all the updates Telegram originally +sends. Your terminal should support UTF-8, or Python may fail to print +some characters on screen. + +### [`print_messages.py`] + +* Usable as a: **user and bot**. +* Difficulty: **easy**. + +This example uses the different `@client.on` syntax to register event +handlers, and uses the `pattern=` variable to filter only some messages. + +There are a lot other things you can do, but you should refer to the +documentation of [`events.NewMessage`] since this is only a simple example. + +### [`replier.py`] + +* Usable as a: **user and bot**. +* Difficulty: **easy**. + +This example showcases a third way to add event handlers (using decorators +but without the client; you should use the one you prefer) and will also +reply to some messages with different reactions, or to your commands. + +It also shows how to enable `logging`, which you should always do, but was +not really needed for the previous two trivial examples. + +### [`assistant.py`] + +* Usable as a: **bot**. +* Difficulty: **medium**. + +This example is the actual bot account [@TelethonianBot] running in the +[official Telethon's chat] to help people out. The file is a bit big and +assumes some [`asyncio`] knowledge, but otherwise is easy to follow. + +### [`interactive_telegram_client.py`] + +* Usable as a: **user and bot**. +* Difficulty: **medium**. + +Interactive terminal client that you can use to list your dialogs, +send messages, delete them, and download media. The code is a bit +long which may make it harder to follow, and requires saving some +state in order for downloads to work later. + +### [`gui.py`] + +* Usable as a: **user and bot**. +* Difficulty: **high**. + +This is a simple GUI written with [`tkinter`] which becomes more complicated +when there's a need to use [`asyncio`] (although it's only a bit of additional +setup). The code to deal with the interface and the commands the GUI supports +also complicate the code further and require knowledge and careful reading. + +This example is the actual bot account [@TelethonianBot] running in the +[official Telethon's chat] to help people out. The file is a bit big and +assumes some [`asyncio`] knowledge, but otherwise is easy to follow. + +![Screenshot of the tkinter GUI][tkinter GUI] + + +[Telethon]: https://github.com/LonamiWebs/Telethon +[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/LICENSE +[@BotFather]: https://t.me/BotFather +[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/assistant.py +[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/gui.py +[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/interactive_telegram_client.py +[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_messages.py +[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_updates.py +[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/replier.py +[@TelethonianBot]: https://t.me/TelethonianBot +[official Telethon's chat]: https://t.me/TelethonChat +[`asyncio`]: https://docs.python.org/3/library/asyncio.html +[`tkinter`]: https://docs.python.org/3/library/tkinter.html +[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/screenshot-gui.jpg +[`events.NewMessage`]: https://telethon.readthedocs.io/en/stable/telethon.events.html#telethon.events.newmessage.NewMessage diff --git a/telethon_examples/README.txt b/telethon_examples/README.txt deleted file mode 100644 index 73af9702..00000000 --- a/telethon_examples/README.txt +++ /dev/null @@ -1,15 +0,0 @@ -You should be able to run these files with python3 filename.py after -installing Telethon (`pip3 install .` on the root of the project if you -haven't installed it yet and you downloaded the repository). - -Most of these examples assume you have the following variables defined -in your environment: - - TG_API_ID, this is the api ID of your Telegram application. - TG_API_HASH, similarly, this is the api hash. - TG_TOKEN, for bot examples, this should be the bot token. - TG_SESSION, this is the session file name to be (re)used. - -See https://superuser.com/q/284342 to learn how to define them. -It's more convenient to define them, but if you forget to do so, -the scripts will ask you to enter the variables when ran. diff --git a/telethon_examples/print_messages.py b/telethon_examples/print_messages.py new file mode 100644 index 00000000..21aafc59 --- /dev/null +++ b/telethon_examples/print_messages.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# A simple script to print some messages. +import os +import sys +import time + +from telethon import TelegramClient, events, utils + + +def get_env(name, message, cast=str): + if name in os.environ: + return os.environ[name] + while True: + value = input(message) + try: + return cast(value) + except ValueError as e: + print(e, file=sys.stderr) + time.sleep(1) + + +session = os.environ.get('TG_SESSION', 'printer') +api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) +api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') +proxy = None # https://github.com/Anorov/PySocks + +# Create and start the client so we can make requests (we don't here) +client = TelegramClient(session, api_id, api_hash, proxy=proxy).start() + + +# `pattern` is a regex, see https://docs.python.org/3/library/re.html +# Use https://regexone.com/ if you want a more interactive way of learning. +# +# "(?i)" makes it case-insensitive, and | separates "options". +@client.on(events.NewMessage(pattern=r'(?i).*\b(hello|hi)\b')) +async def handler(event): + sender = await event.get_sender() + name = utils.get_display_name(sender) + print(name, 'said', event.text, '!') + +try: + print('(Press Ctrl+C to stop this)') + client.run_until_disconnected() +finally: + client.disconnect() + +# Note: We used try/finally to show it can be done this way, but using: +# +# with client: +# client.run_until_disconnected() +# +# is almost always a better idea. diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index a93992ca..48ade9d4 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 -# A simple script to print all updates received +# A simple script to print all updates received. +# Import modules to access environment, sleep, write to stderr import os import sys import time +# Import the client from telethon import TelegramClient +# This is a helper method to access environment variables or +# prompt the user to type them in the terminal if missing. def get_env(name, message, cast=str): if name in os.environ: return os.environ[name] @@ -19,28 +23,23 @@ def get_env(name, message, cast=str): time.sleep(1) -client = TelegramClient( - os.environ.get('TG_SESSION', 'printer'), - get_env('TG_API_ID', 'Enter your API ID: ', int), - get_env('TG_API_HASH', 'Enter your API hash: '), - proxy=None -) +# Define some variables so the code reads easier +session = os.environ.get('TG_SESSION', 'printer') +api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) +api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') +proxy = None # https://github.com/Anorov/PySocks -async def update_handler(update): +# This is our update handler. It is called when a new update arrives. +async def handler(update): print(update) -client.add_event_handler(update_handler) +# Use the client in a `with` block. It calls `start/disconnect` automatically. +with TelegramClient(session, api_id, api_hash, proxy=proxy) as client: + # Register the update handler so that it gets called + client.add_event_handler(handler) -'''You could also have used the @client.on(...) syntax: -from telethon import events - -@client.on(events.Raw) -async def update_handler(update): - print(update) -''' - -with client.start(): + # Run the client until Ctrl+C is pressed, or the client disconnects print('(Press Ctrl+C to stop this)') client.run_until_disconnected() diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index 3f109abd..f11c39b4 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -6,26 +6,17 @@ This script assumes that you have certain files on the working directory, such as "xfiles.m4a" or "anytime.png" for some of the automated replies. """ import os -import re import sys import time from collections import defaultdict -from datetime import datetime, timedelta from telethon import TelegramClient, events -"""Uncomment this for debugging import logging -logging.basicConfig(level=logging.DEBUG) -logging.debug('dbg') -logging.info('info') -""" +logging.basicConfig(level=logging.WARNING) -REACTS = {'emacs': 'Needs more vim', - 'chrome': 'Needs more Firefox'} - -# A list of dates of reactions we've sent, so we can keep track of floods -recent_reacts = defaultdict(list) +# "When did we last react?" dictionary, 0.0 by default +recent_reacts = defaultdict(float) def get_env(name, message, cast=str): @@ -40,6 +31,60 @@ def get_env(name, message, cast=str): time.sleep(1) +def can_react(chat_id): + # Get the time when we last sent a reaction (or 0) + last = recent_reacts[chat_id] + + # Get the current time + now = time.time() + + # If 10 minutes as seconds have passed, we can react + if now - last < 10 * 60: + # Make sure we updated the last reaction time + recent_reacts[chat_id] = now + return True + else: + return False + + +# Register `events.NewMessage` before defining the client. +# Once you have a client, `add_event_handler` will use this event. +@events.register(events.NewMessage) +async def handler(event): + # There are better ways to do this, but this is simple. + # If the message is not outgoing (i.e. someone else sent it) + if not event.out: + if 'emacs' in event.raw_text: + if can_react(event.chat_id): + await event.reply('> emacs\nneeds more vim') + + elif 'vim' in event.raw_text: + if can_react(event.chat_id): + await event.reply('> vim\nneeds more emacs') + + elif 'chrome' in event.raw_text: + if can_react(event.chat_id): + await event.reply('> chrome\nneeds more firefox') + + # Reply always responds as a reply. We can respond without replying too + if 'shrug' in event.raw_text: + if can_react(event.chat_id): + await event.respond(r'¯\_(ツ)_/¯') + + # We can also use client methods from here + client = event.client + + # If we sent the message, we are replying to someone, + # and we said "save pic" in the message + if event.out and event.reply_to_msg_id and 'save pic' in event.raw_text: + reply_msg = await event.get_reply_message() + replied_to_user = await reply_msg.get_input_sender() + + message = await event.reply('Downloading your profile photo...') + file = await client.download_profile_photo(replied_to_user) + await message.edit('I saved your photo in {}'.format(file)) + + client = TelegramClient( os.environ.get('TG_SESSION', 'replier'), get_env('TG_API_ID', 'Enter your API ID: ', int), @@ -47,45 +92,9 @@ client = TelegramClient( proxy=None ) +with client: + # This remembers the events.NewMessage we registered before + client.add_event_handler(handler) -@client.on(events.NewMessage) -async def my_handler(event: events.NewMessage.Event): - global recent_reacts - - # Through event.raw_text we access the text of messages without format - words = re.split('\W+', event.raw_text) - - # Try to match some reaction - for trigger, response in REACTS.items(): - if len(recent_reacts[event.chat_id]) > 3: - # Silently ignore triggers if we've recently sent 3 reactions - break - - if trigger in words: - # Remove recent replies older than 10 minutes - recent_reacts[event.chat_id] = [ - a for a in recent_reacts[event.chat_id] if - datetime.now() - a < timedelta(minutes=10) - ] - # Send a reaction as a reply (otherwise, event.respond()) - await event.reply(response) - # Add this reaction to the list of recent actions - recent_reacts[event.chat_id].append(datetime.now()) - - # Automatically send relevant media when we say certain things - # When invoking requests, get_input_entity needs to be called manually - if event.out: - chat = await event.get_input_chat() - if event.raw_text.lower() == 'x files theme': - await client.send_file(chat, 'xfiles.m4a', - reply_to=event.message.id, voice_note=True) - if event.raw_text.lower() == 'anytime': - await client.send_file(chat, 'anytime.png', - reply_to=event.message.id) - if '.shrug' in event.text: - await event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯')) - - -with client.start(): print('(Press Ctrl+C to stop this)') client.run_until_disconnected() diff --git a/telethon_examples/screenshot-gui.jpg b/telethon_examples/screenshot-gui.jpg new file mode 100644 index 00000000..4da34b0a Binary files /dev/null and b/telethon_examples/screenshot-gui.jpg differ