├── runtime.txt ├── Procfile ├── heroku.png ├── requirements.txt ├── plugins ├── data │ ├── images │ │ ├── hoe.jpg │ │ ├── moc.jpg │ │ ├── nou.jpg │ │ ├── retarded.jpg │ │ ├── get_some_help.gif │ │ └── played_yourself.gif │ ├── memes │ ├── commands │ └── insults ├── __init__.py ├── help.py ├── tag.py ├── die.py ├── quote.py ├── messages.py ├── myname.py ├── cleanup.py ├── groupinfo.py ├── waiting.py ├── server.py ├── user.py ├── omni.py ├── meme.py ├── reminder.py ├── anim.py ├── wall.py ├── generator.py ├── google.py └── weather.py ├── heroku-exec.sh ├── heroku.yml ├── .gitignore ├── sample.env ├── Pipfile ├── stringsession.py ├── userbot ├── __main__.py └── __init__.py ├── Dockerfile ├── LICENSE ├── app.json ├── README.md ├── config.py ├── Plugins.md └── Pipfile.lock /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.1 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python3 -m userbot -------------------------------------------------------------------------------- /heroku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/heroku.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/requirements.txt -------------------------------------------------------------------------------- /plugins/data/images/hoe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/plugins/data/images/hoe.jpg -------------------------------------------------------------------------------- /plugins/data/images/moc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/plugins/data/images/moc.jpg -------------------------------------------------------------------------------- /plugins/data/images/nou.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/plugins/data/images/nou.jpg -------------------------------------------------------------------------------- /plugins/data/images/retarded.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/plugins/data/images/retarded.jpg -------------------------------------------------------------------------------- /heroku-exec.sh: -------------------------------------------------------------------------------- 1 | echo "heroku exec =====" 2 | [ -z "$SSH_CLIENT" ] && source <(curl --fail --retry 3 -sSL "$HEROKU_EXEC_URL") -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | run: 5 | worker: bash heroku-exec.sh && python3 -m userbot 6 | -------------------------------------------------------------------------------- /plugins/data/images/get_some_help.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/plugins/data/images/get_some_help.gif -------------------------------------------------------------------------------- /plugins/data/images/played_yourself.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fosslife/grambot/HEAD/plugins/data/images/played_yourself.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.session 2 | .vscode/ 3 | .env 4 | __pycache__ 5 | tguserbot.session-journal 6 | logs/ 7 | mockup.py 8 | pyrightconfig.json -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile 2 | import glob 3 | modules = glob.glob(dirname(__file__)+"/*.py") 4 | All_PLUGINS = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] 5 | -------------------------------------------------------------------------------- /plugins/data/memes: -------------------------------------------------------------------------------- 1 | pys - Congratulations, You played yourself! 2 | nou - No, U! 3 | ret - Oh no! it's retarded 4 | moc - You are a man of culture as well 5 | hoe - You won't get anything done hoeing like that 6 | sgh - Stop it, get some help -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | apihash="telegram_api_hashg" 2 | apiid="telegram_api_id" 3 | openweather_api_key="ow_apikey" 4 | allowed_chats=1234 3214 5123 5 | my_name_aliases=Sam Samuel Sammy S4m 6 | wolfram_appid=1234-1243 7 | string_session_key=yourstringsessionkey get this by running stringsession.py file 8 | env=dev -------------------------------------------------------------------------------- /plugins/help.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import help 4 | 5 | @bot.on(events.NewMessage(**help)) 6 | async def helpfn(event): 7 | logger.info("help plugin called") 8 | commands = open("./plugins/data/commands").read() 9 | await event.respond(commands) -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | autopep8 = "*" 8 | pylint = "*" 9 | black = "*" 10 | 11 | [packages] 12 | telethon = "*" 13 | requests = "*" 14 | python-dotenv = "*" 15 | beautifulsoup4 = "*" 16 | 17 | [pipenv] 18 | allow_prereleases = true 19 | -------------------------------------------------------------------------------- /plugins/tag.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import tag 4 | 5 | @bot.on(events.NewMessage(**tag)) 6 | async def fn(event): 7 | logger.info("tag plugin is called") 8 | await event.delete() 9 | await event.respond(event.raw_text, reply_to=event.reply_to_msg_id) 10 | -------------------------------------------------------------------------------- /plugins/data/commands: -------------------------------------------------------------------------------- 1 | `.anim(f, s)` - animates message between `f` and `s` 2 | `.die()` - sends a random insult to the replied message 3 | `.help()` - this message 4 | `.meme(x)` - send x meme, check more on .meme(help) 5 | `.quote()` - sends a random quote 6 | `.user(name/id/link)` - information about user 7 | `.weather(City)` - gives weather information about `City` -------------------------------------------------------------------------------- /stringsession.py: -------------------------------------------------------------------------------- 1 | from telethon import TelegramClient 2 | from telethon.sessions import StringSession 3 | from dotenv import load_dotenv 4 | import os 5 | 6 | load_dotenv(".env") 7 | 8 | api_id = os.environ["apiid"] 9 | api_hash = os.environ["apihash"] 10 | 11 | with TelegramClient(StringSession(), api_id, api_hash) as client: 12 | print("your session string is:") 13 | print(client.session.save()) 14 | -------------------------------------------------------------------------------- /userbot/__main__.py: -------------------------------------------------------------------------------- 1 | from userbot import bot 2 | from plugins import All_PLUGINS 3 | import importlib 4 | from userbot import logger 5 | 6 | 7 | for plugin in All_PLUGINS: 8 | logger.info("importing " + plugin) 9 | importlib.import_module("plugins." + plugin) 10 | 11 | logger.info("All plugins loaded, starting bot") 12 | 13 | bot.start() 14 | logger.info("bot started") 15 | 16 | bot.run_until_disconnected() -------------------------------------------------------------------------------- /plugins/die.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | from userbot import bot, logger 3 | from telethon import TelegramClient, events 4 | from config import die 5 | 6 | @bot.on(events.NewMessage(**die)) 7 | async def insult(event): 8 | logger.info("insults plugin called") 9 | file = open("./plugins/data/insults").readlines() 10 | insult = choice(file) 11 | logger.info(f"chosen insult - {insult}") 12 | await event.respond(insult, reply_to=event.reply_to_msg_id) -------------------------------------------------------------------------------- /plugins/quote.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from userbot import bot, logger 3 | from telethon import TelegramClient, events 4 | from config import quote 5 | 6 | @bot.on(events.NewMessage(**quote)) 7 | async def sendQute(event): 8 | logger.info("quotes plugin called") 9 | quotes_api_url = "https://opinionated-quotes-api.gigalixirapp.com/v1/quotes" 10 | data = requests.get(quotes_api_url).json() 11 | quote = data['quotes'][0]['quote'] 12 | author = data['quotes'][0]['author'] 13 | await event.reply(f'`{quote}`\n - __{author}__') 14 | -------------------------------------------------------------------------------- /plugins/messages.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import messages 4 | 5 | messages_mapper = { 6 | 'todo': '✅ Added to TODO!', 7 | 'riir': 'RIIR' 8 | } 9 | 10 | @bot.on(events.NewMessage(**messages)) 11 | async def messagesfns(event): 12 | logger.info("called messages plugin") 13 | pattern_string = event.pattern_match.string 14 | message = pattern_string[pattern_string.find("(")+1:pattern_string.find(")")] 15 | logger.info(f"message to send {message}") 16 | await event.respond(messages_mapper[message]) 17 | -------------------------------------------------------------------------------- /plugins/myname.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import myname 4 | from os import environ 5 | from datetime import datetime 6 | 7 | 8 | @bot.on(events.NewMessage(**myname)) 9 | async def mynamefn(event): 10 | pattern = myname.get("pattern") 11 | if pattern and event.is_group: 12 | logger.info("myname plugin is called") 13 | logger.info(f"incoming message is {event.raw_text.lower()}") 14 | group_id = event.message.to_id.channel_id 15 | await bot.forward_messages("me", event.message.id, group_id) 16 | -------------------------------------------------------------------------------- /plugins/cleanup.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import cleanup 4 | 5 | @bot.on(events.NewMessage(**cleanup)) 6 | async def clean(event): 7 | logger.info("cleanup plugin called") 8 | pattern_string = event.pattern_match.string 9 | limit = pattern_string[pattern_string.find("(")+1:pattern_string.find(")")] 10 | id = await event.get_input_chat() 11 | logger.info(f"deleting {limit} messages from {id}") 12 | m = await bot.get_messages(id, limit=int(limit)) 13 | await bot.delete_messages(id, m) 14 | await event.respond("cleanup done") 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #Create a ubuntu base image with python 3 installed. 2 | FROM pypy:slim-bullseye 3 | 4 | WORKDIR /app 5 | 6 | # Copy the requirements.txt file to the container 7 | COPY requirements.txt . 8 | 9 | # Install project dependencies 10 | RUN pip install --no-cache-dir -r requirements.txt 11 | 12 | # Copy the entire project to the container 13 | COPY . . 14 | 15 | # Copy the .env file to the container 16 | COPY .env . 17 | 18 | # Set environment variables if necessary 19 | # ENV VARIABLE_NAME value 20 | 21 | # Expose any necessary ports 22 | # EXPOSE port_number 23 | 24 | # Run the command to start the application 25 | CMD [ "python", "-m", "userbot" ] -------------------------------------------------------------------------------- /plugins/groupinfo.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import groupinfo 4 | 5 | 6 | @bot.on(events.NewMessage(**groupinfo)) 7 | async def fn(event): 8 | logger.info("group info plugin called") 9 | try: 10 | id = event.message.to_id.channel_id 11 | logger.info(f"sending group id - {id}") 12 | await event.respond(f"groupid - {id}") 13 | except AttributeError: 14 | id = event.message.to_id.user_id 15 | logger.info(f"sending user id - {id}") 16 | await event.respond(f"userid - {id}") 17 | except Exception as e: 18 | logger.exception(f"Error while fetching records {e}") 19 | return 20 | 21 | -------------------------------------------------------------------------------- /plugins/waiting.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import waiting 4 | from asyncio import sleep 5 | 6 | async def animated_response(event): 7 | await event.delete() 8 | sent_message = await event.respond("waiting", parse_mode="html") 9 | try: 10 | reply_to_user = event.message.to_id.user_id 11 | except AttributeError: 12 | reply_to_user = event.message.to_id.channel_id 13 | for i in range(0, 2): 14 | for j in range(0, 6): 15 | await sleep(0.4) 16 | await bot.edit_message(reply_to_user, sent_message.id, f"```waiting{'.'*j}```") 17 | 18 | @bot.on(events.NewMessage(**waiting)) 19 | async def myname(event): 20 | logger.info("waiting plugin called") 21 | await animated_response(event) -------------------------------------------------------------------------------- /plugins/server.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import server 4 | import subprocess 5 | 6 | 7 | allowed_commands = [ 8 | 'ls', 9 | 'ls -l', 10 | 'cd', 11 | 'mkdir', 12 | ] 13 | 14 | @bot.on(events.NewMessage(**server)) 15 | async def serverfn(event): 16 | logger.info("server plugin called") 17 | pattern_string = event.pattern_match.string 18 | command = pattern_string[pattern_string.find("(")+1:pattern_string.find(")")] 19 | logger.info(f"command to execute - {command}") 20 | parametrize = command.split(" ") 21 | pipe = subprocess.check_output(parametrize, stderr=subprocess.PIPE) 22 | message = pipe.decode('utf-8') 23 | if command in allowed_commands: 24 | logger.info("command is in allowed_commands") 25 | await event.respond(f"```{message}```") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Prabhanjan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugins/user.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import user 4 | from telethon.tl.functions.users import GetFullUserRequest 5 | 6 | @bot.on(events.NewMessage(**user)) 7 | async def getUser(event): 8 | logger.info("user plugin is called") 9 | pattern_string = event.pattern_match.string 10 | entity = pattern_string[pattern_string.find("(")+1:pattern_string.find(")")] 11 | logger.info(f"entity to search - {entity}") 12 | try: 13 | info = await bot(GetFullUserRequest(entity)) 14 | await event.respond(f""" 15 | Username - `{info.user.username}` 16 | {"User is a bot" if info.user.bot else "user is not a bot"} 17 | {"User is restricted for " + info.user.restriction_reason if info.user.restricted else "User is not restricted"} 18 | Name - {info.user.first_name} {info.user.last_name if info.user.last_name else ""} 19 | Status - `{info.about}` 20 | id - {info.user.id} 21 | {info.common_chats_count} groups common with me 22 | {"I have blocked this user" if info.blocked else "I have not blocked this user"} 23 | 24 | """) 25 | except Exception: 26 | await event.respond(f"Cannot find entity with `{entity}`") -------------------------------------------------------------------------------- /plugins/omni.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import omni 4 | import requests 5 | from os import environ 6 | 7 | @bot.on(events.NewMessage(**omni)) 8 | async def omnifn(event): 9 | logger.info("omni plugin called") 10 | appid = environ.get("wolfram_appid", None) 11 | if not appid: 12 | await event.respond("no appid provided") 13 | return 14 | pattern_string = event.pattern_match.string 15 | query_string = pattern_string[pattern_string.find("(")+1:pattern_string.find(")")] 16 | if not query_string: 17 | await event.respond("please provide (a query to search)") 18 | query_params = query_string.replace(" ", "+") 19 | url = f"https://api.wolframalpha.com/v2/query?input={query_params}" 20 | res = requests.get(url, { 21 | "format": "plaintext", 22 | "output": "JSON", 23 | "appid": appid 24 | }) 25 | res_json = res.json() 26 | pods = res_json['queryresult']['pods'] 27 | result_pod = '' 28 | for o in pods: 29 | if o['title'] == "Result": 30 | result_pod = o 31 | # print(result_pod) 32 | await event.respond(result_pod['subpods'][0]['plaintext']) -------------------------------------------------------------------------------- /plugins/meme.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import meme 4 | 5 | images_dir = "./plugins/data/images/" 6 | 7 | file_path_mapping = { 8 | "help": "./plugins/data/memes", 9 | "pys": images_dir + "played_yourself.gif", 10 | "nou": images_dir + "nou.jpg", 11 | "ret": images_dir + "retarded.jpg", 12 | "moc": images_dir + "moc.jpg", 13 | "hoe": images_dir + "hoe.jpg", 14 | "sgh": images_dir + "get_some_help.gif", 15 | } 16 | 17 | @bot.on(events.NewMessage(**meme)) 18 | async def memes(event): 19 | logger.info("meme plugin is called") 20 | await event.delete() 21 | pattern_string = event.pattern_match.string 22 | meme_name = pattern_string[pattern_string.find("(")+1:pattern_string.find(")")] 23 | logger.info(f"file to send {file_path_mapping.get(meme_name)}") 24 | file_to_send = file_path_mapping.get(meme_name) 25 | if meme_name=="help": 26 | await event.respond(open(file_to_send).read(), reply_to=event.reply_to_msg_id) 27 | else: 28 | try: 29 | await event.respond("", reply_to=event.reply_to_msg_id, file=file_to_send) 30 | except Exception: 31 | await event.respond("No U") 32 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grambot", 3 | "description": "Telgram userbot", 4 | "repository": "https://github.com/fosslife/grambot", 5 | "keywords": ["telethon", "telegram", "python", "userbot"], 6 | "website": "https://github.com/fosslife/grambot", 7 | "stack": "container", 8 | "env": { 9 | "apihash": { 10 | "description": "you app API hash. get it from my.telegram.org", 11 | "required": true 12 | }, 13 | "apiid": { 14 | "description": "you app API ID. get it from my.telegram.org", 15 | "required": true 16 | }, 17 | "string_session_key": { 18 | "description": "your string session key. get this by running stringsession.py from repo", 19 | "required":true 20 | }, 21 | "openweather_api_key": { 22 | "description": "OpenWeather key for weather plugin", 23 | "required":false 24 | }, 25 | "allowed_chats": { 26 | "description": "List of chats your bot is allowed to run. this is helpful to prevent spam. space seprated list of ids. you can get these ids by .id plugin of bot", 27 | "required":true 28 | }, 29 | "my_name_aliases": { 30 | "description": "your hidden eye on who is speaking your name RTFM", 31 | "required":false 32 | }, 33 | "wolfram_appid": { 34 | "description": "Wolfram api id", 35 | "required":true 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugins/reminder.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import reminder 4 | from datetime import timedelta 5 | 6 | def get_expiry(timestr: str): 7 | timeFactor = timestr.split(" ")[1] 8 | time = int(timestr.split(" ")[0]) 9 | 10 | multipliers = { 11 | "sec": lambda t: t, 12 | "seconds": lambda t: t, 13 | "min": lambda t: t * 60, 14 | "minutes": lambda t: t * 60, 15 | "hour": lambda t: t * 3600, 16 | "hours": lambda t: t * 3600, 17 | "day": lambda t: t * 86400, 18 | "days": lambda t: t * 86400, 19 | "week": lambda t: t * 604800, 20 | "weeks": lambda t: t * 604800, 21 | "month": lambda t: t * 2.628e6, 22 | "months": lambda t: t * 2.628e6, 23 | } 24 | return multipliers.get(timeFactor)(time) 25 | 26 | 27 | @bot.on(events.NewMessage(**reminder)) 28 | async def reminder(event): 29 | logger.info("reminder plugin called") 30 | groups = event.pattern_match 31 | task = groups.group(3) 32 | time = groups.group(5) 33 | duration = groups.group(4) 34 | unit = groups.group(6) 35 | fromid = await event.get_sender() 36 | timestr = f"{time} {unit}" 37 | expiry_second = get_expiry(timestr) 38 | logger.info("task=%s time=%s duration=%s unit=%s", task, time, duration, unit) 39 | await event.respond(f"Ok! will remind you to {task} {duration} {time} {unit}") 40 | await event.respond( 41 | f"@{fromid.username} reminding you to {task}", 42 | schedule=timedelta(seconds=expiry_second), 43 | ) -------------------------------------------------------------------------------- /plugins/anim.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from asyncio import sleep 4 | import re 5 | from config import chats 6 | 7 | # both regex and parse() Courtesy of @ceda_ei 8 | reg = r"^\.anim\((?P\"|')?(?(q)((?:(?!(?P=q)).)+)(?P=q)|([^,)]+)),\s*(?P\"|')?(?(r)((?:(?!(?P=r)).)+)(?P=r)|([^,)]+))\)$" 9 | compiled = re.compile(reg) 10 | 11 | 12 | def parse(text): 13 | x = compiled 14 | match = x.match(text) 15 | if match: 16 | if match.group(2): 17 | first_param = match.group(2) 18 | else: 19 | first_param = match.group(3) 20 | if match.group(5): 21 | second_param = match.group(5) 22 | else: 23 | second_param = match.group(6) 24 | return (first_param, second_param) 25 | return None 26 | 27 | 28 | @bot.on(events.NewMessage(pattern=compiled, chats=chats, incoming=False, outgoing=True)) 29 | async def anim(event): 30 | logger.info("anim plugin called") 31 | matched = parse(event.pattern_match.string) 32 | before = matched[0] 33 | after = matched[1] 34 | logger.info(f"before string - {before}") 35 | logger.info(f"after string - {after}") 36 | if before and after: 37 | try: 38 | reply_to_user = event.message.to_id.user_id 39 | except AttributeError: 40 | reply_to_user = event.message.to_id.channel_id 41 | sent = await event.respond(after, reply_to=event.reply_to_msg_id) 42 | logger.info(f"replying to {sent.id}") 43 | for i in range(0, 5): 44 | await bot.edit_message(reply_to_user, sent.id, before) 45 | await sleep(0.5) 46 | await bot.edit_message(reply_to_user, sent.id, after) 47 | await sleep(0.5) 48 | -------------------------------------------------------------------------------- /userbot/__init__.py: -------------------------------------------------------------------------------- 1 | from telethon import TelegramClient, events 2 | from telethon.sessions import StringSession 3 | import os 4 | import sys 5 | 6 | import logging 7 | from logging import StreamHandler, log 8 | from logging.handlers import RotatingFileHandler 9 | import time 10 | from dotenv import load_dotenv 11 | 12 | load_dotenv(".env") 13 | 14 | 15 | logger = logging.getLogger(__name__) 16 | logger.setLevel(logging.DEBUG) 17 | 18 | STRING_SESSION = os.environ.get("string_session_key", None) 19 | 20 | if STRING_SESSION: 21 | handler = StreamHandler(sys.stdout) 22 | formatter = logging.Formatter( 23 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 24 | ) 25 | handler.setLevel(logging.INFO) 26 | handler.setFormatter(formatter) 27 | logger.addHandler(handler) 28 | 29 | else: 30 | if (os.environ.get("env") == "dev"): 31 | handler = StreamHandler(sys.stdout) 32 | formatter = logging.Formatter( 33 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 34 | ) 35 | handler.setLevel(logging.INFO) 36 | handler.setFormatter(formatter) 37 | logger.addHandler(handler) 38 | else: 39 | LOG_FILENAME = "logs/grambot.log" 40 | if not os.path.exists("logs"): 41 | os.mkdir("logs") 42 | 43 | needRoll = os.path.isfile(LOG_FILENAME) 44 | handler = RotatingFileHandler(LOG_FILENAME, backupCount=10) 45 | formatter = logging.Formatter( 46 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 47 | ) 48 | handler.setFormatter(formatter) 49 | logger.addHandler(handler) 50 | 51 | if needRoll: 52 | logger.handlers[0].doRollover() 53 | 54 | 55 | api_id = os.environ["apiid"] 56 | api_hash = os.environ["apihash"] 57 | 58 | 59 | if STRING_SESSION: 60 | logger.info("String session exists") 61 | bot = TelegramClient(StringSession(STRING_SESSION), api_id, api_hash) 62 | else: 63 | logger.info("String session does not exists") 64 | bot = TelegramClient("tguserbot", api_id, api_hash) 65 | -------------------------------------------------------------------------------- /plugins/wall.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from telethon.tl import types 4 | from bs4 import BeautifulSoup 5 | import requests 6 | from config import wall 7 | from random import randint, choice 8 | 9 | USER_AGENT = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'} 10 | 11 | 12 | @bot.on(events.NewMessage(**wall)) 13 | async def wall(event): 14 | logger.info("Wallpaper plugin called") 15 | try: 16 | category = event.pattern_match.string.split(" ")[1] 17 | except IndexError: 18 | return await event.respond(f"select a category:```\ntoplist\nrandom\nhot\nlatest```") 19 | url = f"https://wallhaven.cc/{category}" 20 | try: 21 | tonsfw = '010' if event.pattern_match.string.split(" ")[2] == "nsfw" else "000" 22 | except IndexError: 23 | tonsfw = '000' 24 | try: 25 | # 120 is the limit of max pages 26 | res = requests.get(url, {"page": randint(2, 40), "purity": tonsfw }, headers=USER_AGENT) 27 | logger.info(f"url generated {res.url}") 28 | soup = BeautifulSoup(res.text, 'html.parser') 29 | try: 30 | page = soup.find("section", {"class": "thumb-listing-page"}) 31 | ls = page.findChildren("li") 32 | selectedls = choice(ls) 33 | thumbinfo = selectedls.findChild("div", {"class": "thumb-info"}) 34 | imageid = selectedls.findChild().get("data-wallpaper-id") 35 | url = f"https://w.wallhaven.cc/full/{imageid[:2]}/wallhaven-{imageid}." 36 | isPng = thumbinfo.findChild("span", {"class": "png"}, recursive=True) 37 | if isPng is None: 38 | url = url + "jpg" 39 | else: 40 | url = url + "png" 41 | logger.info(f"url is {url}") 42 | await event.respond(file=types.InputMediaPhotoExternal(url)) 43 | except Exception as e: 44 | await event.respond(f"Error occurred while sending, please try once again if file is too large\n here's full url to download manually{url}") 45 | logger.exception(e) 46 | pass 47 | except Exception as e: 48 | logger.exception(e) -------------------------------------------------------------------------------- /plugins/generator.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from telethon.tl import types 4 | from config import generator 5 | from bs4 import BeautifulSoup 6 | import requests 7 | from random import randint 8 | import urllib 9 | from random import random 10 | 11 | urls = { 12 | "word": "https://www.thisworddoesnotexist.com/", 13 | "waifu": "https://www.thiswaifudoesnotexist.net/", 14 | "art": "https://thisartworkdoesnotexist.com/", 15 | "cat": "https://thiscatdoesnotexist.com/", 16 | "person": "https://thispersondoesnotexist.com/image", 17 | "lyrics": "https://theselyricsdonotexist.com/generate.php", 18 | } 19 | 20 | 21 | @bot.on(events.NewMessage(**generator)) 22 | async def generate(event): 23 | logger.info("generator plugin called") 24 | category = event.pattern_match.string.split(" ")[1] 25 | if not category: 26 | logger.info("No category found to generate") 27 | return 28 | url = urls[category] 29 | if category == "word": 30 | res = requests.get(url) 31 | soup = BeautifulSoup(res.text, 'html.parser') 32 | word = soup.find(id="definition-word").text 33 | definition = soup.find(id="definition-definition").text 34 | example = soup.find(id="definition-example").text 35 | await event.respond(f"""**{word}**\n{definition}\n\n__{example}__""", parse_mode="md") 36 | elif category == "waifu": 37 | fullurl = f"""{url}example-{randint(1, 99999)}.jpg""" 38 | await event.respond(file=fullurl) 39 | elif category == "art": 40 | fullurl = f"""{url}?random={random()}""" 41 | try: 42 | await event.respond(file=types.InputMediaPhotoExternal(fullurl)) 43 | except Exception as ex: 44 | print("Error Occurred", ex) 45 | await event.respond("Error") 46 | elif category == "cat": 47 | fullurl = f"{url}?random={random()}" 48 | await event.respond(file=types.InputMediaPhotoExternal(fullurl)) 49 | elif category == "person": 50 | fullurl = f"{url}?random={random()}" 51 | await event.respond(file=types.InputMediaPhotoExternal(fullurl)) 52 | # Doesn't work 53 | # elif category == "lyrics": 54 | # # print(event.pattern_match.string) 55 | # try: 56 | # mood = event.pattern_match.string.split(" ")[2] 57 | # genre = event.pattern_match.string.split(" ")[3] 58 | # message = event.pattern_match.string.split(" ")[4] 59 | # res = requests.post(url, data={"lyricMood": mood, "lyricsGenre": genre, "message": message}, headers={ 60 | # "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36", 61 | # "Origin": "https://theselyricsdonotexist.com"}) 62 | # print(res) 63 | # await event.respond("Done") 64 | # except Exception as ex: 65 | # print("Error Occurred", ex) 66 | # await event.respond("Error") 67 | else: 68 | await event.respond("Cannot generate selected entity") 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grambot 2 | 3 | ## What is it 4 | 5 | It's a telegram userbot. if you don't know what a userbot is, it's a normal user account but backed by the power of programming. Think it like, a programming language is handling your account and that's all. It knows when someone sends you a message, what is the message, if you want to send something to someone etc etc. Thus, automation! 6 | 7 | ## How does it work? 8 | 9 | This bot is written in Python, with a famous telegram MTProto framework known as [Telethon](https://github.com/LonamiWebs/Telethon). With the help of Telethon, we can keep the server in `listening` mode for events such as New Incoming message etc etc and then for a specific type of message, take a specific action. 10 | 11 | ## Deploy 12 | 13 |

Deploy to Heroku

14 | 15 | > Note that heroku deploys the bot successfully, but somehow doesn't start the bot automatically. I am not good 16 | > with heroku. if someone knows the issue feel free to open PR. otherwise, for now, once bot is deployed with above 17 | > button, go to following section from heroku dashboard, and toggle it to turn dyno on. 18 | 19 | ![heroku](./heroku.png) 20 | 21 | to get the `string_session` key, just clone the repo, get apiid and apikey from telegram (see steps below), paste them in .env file. 22 | then run `stringsession.py` it will output the key for you, paste it in heroku deploy dashboard you see after you click above button. 23 | 24 | to check logs: 25 | 26 | ``` 27 | heroku logs -a heroku-app-id --tail 28 | ``` 29 | 30 | it's always a good idea to connect heroku with repository, so just fork the repo and connect it with github, for manual deploys 31 | 32 | ## Example 33 | 34 | [See full list [Here](./Plugins.md)] 35 | 36 | for this bot I have enabled `commands` such as `.help` and `.weather`. 37 | When the bot encounters a new message on telegram that matches exact `.weather` i.e start from `(dot)(weather)`, it executes a special coroutine that fetches weather from open weather API of given city. thus 38 | 39 | ``` 40 | .weather(Berlin) => weather details of berlin 41 | .help => help message on how to use other commands 42 | ``` 43 | 44 | These output are sent to the same channel the command was sent on, by YOUR account. 45 | 46 | [![Demo](https://img.youtube.com/vi/wybDkn1q3mA/0.jpg)](https://www.youtube.com/watch?v=wybDkn1q3mA) 47 | 48 | ## Installation: 49 | 50 | - Pull the latest source. Clone the repo or download zip and extract it, whatever works for you 51 | - get apiid and apihash from telegram (free). [Instructions](https://core.telegram.org/api/obtaining_api_id) 52 | - get openweather api key (free). [Instructions](https://openweathermap.org/appid) 53 | - rename `sample.env` to `.env` at the root of folder. and edit it accordingly 54 | - if you have pipenv installed 55 | - run `pipenv shell` 56 | - run `pipenv install` 57 | - run `python3 -m userbot` 58 | - it should start the bot. 59 | - if you don't have pipenv 60 | - use virtualenv if possible 61 | - run `pip install -r requirements.txt` 62 | - run `python3 -m userbot` 63 | 64 | if you face any issues contact me on Telegram or feel free to open issue :D 65 | 66 | ## License 67 | 68 | grambot is licensed under MIT. Please see LICENSE file for more information 69 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | import re 3 | 4 | allowed_chats = environ.get("allowed_chats") 5 | blacklist_chats_env = environ.get("blacklist_chats") 6 | 7 | if allowed_chats: 8 | chats = list(map(int, allowed_chats.split(" "))) 9 | else: 10 | chats = [] 11 | 12 | # aliases = environ.get("my_name_aliases").replace(" ", "|") 13 | aliases_env = environ.get("my_name_aliases") 14 | if aliases_env: 15 | aliases = aliases_env.replace(" ", "|") 16 | else: 17 | aliases = None 18 | 19 | if blacklist_chats_env: 20 | blacklist_chats = list(map(int, blacklist_chats_env.split(" "))) 21 | else: 22 | blacklist_chats = [] 23 | 24 | 25 | async def namefilter(x): 26 | return not await x.client.is_bot() 27 | 28 | 29 | cleanup = { 30 | "pattern": r"\.clean", 31 | "incoming": False, 32 | "outgoing": True, 33 | # "chats": chats 34 | } 35 | 36 | die = { 37 | "pattern": r"\.die", 38 | "incoming": False, 39 | "outgoing": True, 40 | # "chats": chats 41 | } 42 | 43 | 44 | generator = { 45 | "pattern": r"\.generate", 46 | "incoming": False, 47 | "outgoing": True, 48 | # "chats": chats 49 | } 50 | 51 | 52 | google = { 53 | "pattern": r"\.google", 54 | "incoming": False, 55 | "outgoing": True, 56 | # "chats": chats 57 | } 58 | 59 | groupinfo = { 60 | "pattern": r"\.id", 61 | "incoming": False, 62 | "outgoing": True, 63 | } 64 | 65 | help = { 66 | "pattern": r"\.help", 67 | "incoming": False, 68 | "outgoing": True, 69 | # "chats": chats 70 | } 71 | 72 | 73 | meme = { 74 | "pattern": r"\.meme", 75 | "incoming": False, 76 | "outgoing": True, 77 | # "chats": chats 78 | } 79 | 80 | messages = { 81 | "pattern": r"\.say", 82 | "incoming": False, 83 | "outgoing": True, 84 | # "chats": chats 85 | } 86 | 87 | myname = { 88 | # "pattern": if aliases re.compile(r".*(" + aliases + ")", re.IGNORECASE) else "", 89 | "incoming": True, 90 | "outgoing": False, 91 | "func": namefilter, 92 | "blacklist_chats": blacklist_chats 93 | # "chats": chats 94 | } 95 | if aliases and len(aliases) > 0: 96 | myname["pattern"] = re.compile(r".*(" + aliases + ")", re.IGNORECASE) 97 | 98 | omni = { 99 | "pattern": r"\.omni", 100 | "incoming": False, 101 | "outgoing": True, 102 | # "chats": chats 103 | } 104 | 105 | quote = { 106 | "pattern": r"\.quote", 107 | "incoming": False, 108 | "outgoing": True, 109 | # "chats": chats 110 | } 111 | 112 | reminder = { 113 | "pattern": r"(.remindme)\s(to)\s(.*)\s(in|after|every)\s(\d+){1,2}\s(sec|seconds|min|minutes|hour|hours|day|days|week|weeks|month|months)", 114 | "incoming": True, 115 | "outgoing": True, 116 | "chats": chats, 117 | } 118 | 119 | server = {"pattern": r"\.exec", "incoming": False, "outgoing": True, "chats": "me"} 120 | 121 | tag = { 122 | "pattern": r"(.*)\[([^\]]+)\]\(([^)]+)\)(.*)", 123 | "incoming": False, 124 | "outgoing": True, 125 | # "chats": chats 126 | } 127 | 128 | user = { 129 | "pattern": r"\.user", 130 | "incoming": False, 131 | "outgoing": True, 132 | # "chats": chats 133 | } 134 | waiting = { 135 | "pattern": r"\.wait", 136 | "incoming": False, 137 | "outgoing": True, 138 | # "chats": chats 139 | } 140 | 141 | wall = { 142 | "pattern": r"\.wall", 143 | "incoming": False, 144 | "outgoing": True, 145 | } 146 | 147 | weather = { 148 | "pattern": r"\.weather", 149 | "incoming": True, 150 | "outgoing": True, 151 | # "chats": chats 152 | } 153 | -------------------------------------------------------------------------------- /plugins/google.py: -------------------------------------------------------------------------------- 1 | from userbot import bot, logger 2 | from telethon import TelegramClient, events 3 | from config import google 4 | from bs4 import BeautifulSoup 5 | import requests 6 | import re 7 | 8 | USER_AGENT = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'} 9 | 10 | @bot.on(events.NewMessage(**google)) 11 | async def googlefn(event): 12 | logger.info("google plugin called") 13 | # pattern_string = event.pattern_match.string 14 | try: 15 | query_string = event.pattern_match.string.split(" ", 1)[1] or "" 16 | logger.info(f"query string to search {query_string}") 17 | query_params = query_string.replace(" ", "+") 18 | logger.info(f"Query params {query_params}") 19 | res = requests.get('https://www.google.com/search', {"q": query_params, "hl": "en"}, headers=USER_AGENT) 20 | soup = BeautifulSoup(res.text, 'html.parser') 21 | except Exception as e: 22 | logger.exception(f"Error while fetching records {e}") 23 | return 24 | # first try to find div tag with attribute data-tts and data-tts-text 25 | try: 26 | tts_text = soup.findAll("div", {"data-tts" : True, "data-tts-text" : True}) 27 | logger.info(f"it's a tts_text match {tts_text}") 28 | # there should be only one element present: 29 | try: 30 | msg = tts_text[0].text 31 | logger.info(f"found record with attribute type tts_text {msg}") 32 | await event.respond(msg) 33 | return # don't execute this method further 34 | except IndexError as e: 35 | logger.error(f"index error occurred {e}") 36 | pass 37 | logger.info("tts_text match FAILED") 38 | # that means try another method: 39 | attr_kc_text = soup.findAll("div", {"data-attrid" : re.compile(r"^kc:/\w+/\w+:\w+")}) 40 | # if there's any such tag 41 | if attr_kc_text: 42 | logger.info("it's attr_kc_text match") 43 | # it's probably in the second child of div tag with this attribute: 44 | try: 45 | msg = attr_kc_text[0].findChild().findChild("div", {"role": "heading"}).text 46 | await event.respond(msg) 47 | logger.info("found record with attribute type kc:/x/x in second div") 48 | return 49 | except AttributeError: 50 | msg = attr_kc_text[0].findChild("div", {"role": "heading"}).findChild().text 51 | await event.respond(msg) 52 | logger.info("found record with attribute type kc:/x/x in first div") 53 | return # don't execute this method further 54 | # else search for another attribute type 55 | attr_hc_text = soup.findAll("div", {"data-attrid" : re.compile(r"^hw:/\w+/\w+:\w+")}) 56 | # if it's present 57 | if attr_hc_text: 58 | # same logic 59 | msg = attr_hc_text[0].findChild().findChild().text 60 | await event.respond(msg) 61 | logger.info("found record with attribute type hw:/x/x in second div") 62 | return # don't execute this method further 63 | # Well, everything up above failed, try another methods: 64 | # Let's see if it's a time-related card 65 | rso = soup.find(id="rso") 66 | card = rso.findChildren("div", {"class": "card-section"}, recursive=True) 67 | try: 68 | # print(rso) 69 | attribute = card[0].get('aria-label') 70 | if attribute is None: 71 | # it's a time realted card 72 | time = card[0].text 73 | logger.info(f"found record as time card {time}") 74 | await event.respond(time) 75 | return 76 | elif attribute == "Currency exchange rate converter": 77 | logger.info("it's a currency card") 78 | parent = soup.find(id="knowledge-currency__updatable-data-column") # Hopefully they won't change ID 79 | currency = parent.findChild().text 80 | _ = currency.split('equals') 81 | # print(currency, _) 82 | hacked_text = f"{_[0]} equals {_[1]}" # ugly hack because some smartass in google did not put space after 'equals' word 83 | logger.info(f"currency conversion should be {currency}") 84 | await event.respond(hacked_text) 85 | except Exception as e: 86 | # print(e.with_traceback()) 87 | logger.exception(e) 88 | pass 89 | # it's not a time card either 90 | except Exception as e: 91 | logger.info(f"Final exception {e}") 92 | await event.respond("can't find anything on that, please report this query to Spark") 93 | -------------------------------------------------------------------------------- /plugins/data/insults: -------------------------------------------------------------------------------- 1 | If laughter is the best medicine, your face must be curing the world. 2 | You're so ugly, you scared the crap out of the toilet. 3 | Your family tree must be a cactus because everybody on it is a prick. 4 | No I'm not insulting you, I'm describing you. 5 | It's better to let someone think you are an Idiot than to open your mouth and prove it. 6 | If I had a face like yours, I'd sue my parents. 7 | Your birth certificate is an apology letter from the condom factory. 8 | I guess you prove that even god makes mistakes sometimes. 9 | The only way you'll ever get laid is if you crawl up a chicken's ass and wait. 10 | You're so fake, Barbie is jealous. 11 | I’m jealous of people that don’t know you! 12 | My psychiatrist told me I was crazy and I said I want a second opinion. He said okay, you're ugly too. 13 | You're so ugly, when your mom dropped you off at school she got a fine for littering. 14 | If I wanted to kill myself I'd climb your ego and jump to your IQ. 15 | You must have been born on a highway because that's where most accidents happen. 16 | Brains aren't everything. In your case they're nothing. 17 | I don't know what makes you so stupid, but it really works. 18 | I can explain it to you, but I can’t understand it for you. 19 | Roses are red violets are blue, God made me pretty, what happened to you? 20 | Behind every fat woman there is a beautiful woman. No seriously, you're in the way. 21 | Calling you an idiot would be an insult to all the stupid people. 22 | You, sir, are an oxygen thief! 23 | Some babies were dropped on their heads but you were clearly thrown at a wall. 24 | Don't like my sarcasm, well I don't like your stupid. 25 | Why don't you go play in traffic. 26 | Please shut your mouth when you’re talking to me. 27 | I'd slap you, but that would be animal abuse. 28 | They say opposites attract. I hope you meet someone who is good-looking, intelligent, and cultured. 29 | Stop trying to be a smart ass, you're just an ass. 30 | The last time I saw something like you, I flushed it. 31 | 'm busy now. Can I ignore you some other time? 32 | You have Diarrhea of the mouth; constipation of the ideas. 33 | If ugly were a crime, you'd get a life sentence. 34 | Your mind is on vacation but your mouth is working overtime. 35 | I can lose weight, but you’ll always be ugly. 36 | Why don't you slip into something more comfortable... like a coma. 37 | Shock me, say something intelligent. 38 | If your gonna be two faced, honey at least make one of them pretty. 39 | Keep rolling your eyes, perhaps you'll find a brain back there. 40 | You are not as bad as people say, you are much, much worse. 41 | I don't know what your problem is, but I'll bet it's hard to pronounce. 42 | You get ten times more girls than me? ten times zero is zero... 43 | There is no vaccine against stupidity. 44 | You're the reason the gene pool needs a lifeguard. 45 | Sure, I've seen people like you before - but I had to pay an admission. 46 | How old are you? - Wait I shouldn't ask, you can't count that high. 47 | Have you been shopping lately? They're selling lives, you should go get one. 48 | You're like Monday mornings, nobody likes you. 49 | Of course I talk like an idiot, how else would you understand me? 50 | All day I thought of you... I was at the zoo. 51 | To make you laugh on Saturday, I need to you joke on Wednesday. 52 | You're so fat, you could sell shade. 53 | I'd like to see things from your point of view but I can't seem to get my head that far up my ass. 54 | Don't you need a license to be that ugly? 55 | My friend thinks he is smart. He told me an onion is the only food that makes you cry, so I threw a coconut at his face. 56 | Your house is so dirty you have to wipe your feet before you go outside. 57 | If you really spoke your mind, you'd be speechless. 58 | Stupidity is not a crime so you are free to go. 59 | You are so old, when you were a kid rainbows were black and white. 60 | If I told you that I have a piece of dirt in my eye, would you move? 61 | You so dumb, you think Cheerios are doughnut seeds. 62 | So, a thought crossed your mind? Must have been a long and lonely journey. 63 | You are so old, your birth-certificate expired. 64 | Every time I'm next to you, I get a fierce desire to be alone. 65 | You're so dumb that you got hit by a parked car. 66 | Keep talking, someday you'll say something intelligent! 67 | You're so fat, you leave footprints in concrete. 68 | How did you get here? Did someone leave your cage open? 69 | Pardon me, but you've obviously mistaken me for someone who gives a damn. 70 | Wipe your mouth, there's still a tiny bit of bullshit around your lips. 71 | Don't you have a terribly empty feeling - in your skull? 72 | As an outsider, what do you think of the human race? 73 | Just because you have one doesn't mean you have to act like one. 74 | We can always tell when you are lying. Your lips move. 75 | Are you always this stupid or is today a special occasion? 76 | -------------------------------------------------------------------------------- /Plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | The plugin system has a nice syntax. to execute any kind of plugin, you need to use as if it was a function. like doc said, to get weather just type `.weather(city)` and so on. 4 | 5 | ## anim 6 | Takes two parameters, and toggles between two in the message as animated text. 7 | 8 | example: `.anim(lOl, LoL)` 9 | first sends message as `lOl` then after 0.5s delay edits it to `LoL` and after 0.5s to again `lOl` and so on for few seconds. it gives a feel that message text is animating :P 10 | 11 | ## die 12 | Sends a random insult to the replied message. just send `.die()` or reply to some message with it 13 | 14 | ## google 15 | Scrapes google seach result for google cards only. Please note that it doesn't just scrape all of the google search results/images or anything else. the query MUST have only 1 specific answer, it won't work on questions like "How to iterate in JS" or "top 10 sports in 2016" 16 | 17 | example: `.google(height of mount everest)` and it should give you correct answer. if it doesn't, google prolly changed HTML structure (plugin scrapes, doesn't use API). in that case just PM me or Open an Issue 18 | 19 | ## groupinfo 20 | Gets `id` of tg group for whitelisting the userbot. you are going to definitly need this plugin as the `config.py` file takes an argument in each of it's config called `chats`. you need to set this option in your `.env` file to enable this userbot only on those selected groups. the commands won't work on other places 21 | 22 | example: `.id()` in any chat you want to get id of. 23 | 24 | ## help 25 | Basic syntax and small description of each plugin command 26 | 27 | example: `.help()` duh! 28 | 29 | ## meme 30 | Noice. Finally some good content! it has a very small collection of memes that I need mostly but it's configurable totally according to your needs. It replies to the message with a meme of your choice. I have included 5-6 of 'em as per my need. 31 | - pys - Congratulations, You played yourself! 32 | - nou - No, U! 33 | - ret - Oh no! it's retarded 34 | - moc - You are a man of culture as well 35 | - hoe - You won't get anything done hoeing like that 36 | - sgh - Stop it, get some help 37 | 38 | To add your own meme, paste the file in `images` dir under `plugins/data` dir. then go to `meme.py` and update `file_path_mapping` variable accordingly. 39 | 40 | example: `.meme(hoe)` will send the image "You won't get anything done hoeing like that" 41 | 42 | ## messages 43 | Simple mapping plugin that sends an equivalent text message according to configuration 44 | 45 | example: `.say(todo)` will send text "✅ Added to TODO!" 46 | 47 | ## myname 48 | It's a very important plugin, especially when you are not online. it reads for your name in all incoming message and if someone says your name without tagging you i.e. they are talking about you, bot will forward that message to your `Saved Messages`. very useful! 49 | To set it up just edit `my_name_aliases` env variable in `.env` file. Note that the bot doesn't have anything to do with your name so you can `listen` for other words too i.e if you are interested in JavaScript, just add it to the variable! 50 | 51 | 52 | ## quote 53 | Sends a famous quote randomly. 54 | 55 | example: `.quote()` PS: I know it's not very useful, but it's my first plugin I even integrated so kept it :P 56 | 57 | ## server 58 | DANGER ZONE! 59 | Again, this plugin is Very Very Dangerous, please Disable it. I made it just becase a friend of mine requested. it can actually run any command that you send, on the sever, where the bot is running. I have taken some precautionary measures but still don't use it unless you know what you are doing. 60 | 61 | example: send `.exec(ls)` only in `Saved Messages`, it will exec `ls` on server shell, and if there's any output, it will forward it to you. I haven't done any formatting to output as they vary according to command. 62 | Note: 1) it will work only in `Saved Message` 2) only the commands mentioned in `server.py` file under `allowed_commands` section will work. 3) Don't use it it's risky 63 | 64 | ## tag 65 | Fun part again. tag anyone with any label. It just creates a `link` to the given username with given text with `markdown` format. 66 | 67 | example: `[Smart Guy](@Sparkenstein)` will send message as `Smart Guy` but it will actually tag whoever @Sparkenstein is in the group. 68 | 69 | ## user 70 | Get as much details as possible of any given user. bot also tries to get common traits shared between the bot owner and given user. 71 | 72 | example: `.user(Sparkenstein)` will get as much information as possible and safe to collect and share it with you in the form of message. 73 | 74 | ## waiting 75 | Animated `waiting` text. I use it extensively. just use go ahead and use it 76 | 77 | example: `.wait()` 78 | 79 | ## weather 80 | You already know what it does. takes a city name, returns weather condition of that city. Note: if city name is ambiguous, mention state/province name with a comma like London,uk. If you still can't find any details, search your city name on OpenWeatherMap first. the bot uses it's API directlys 81 | 82 | example: `.weather(London)` -------------------------------------------------------------------------------- /plugins/weather.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from userbot import bot, logger 3 | from telethon import TelegramClient, events 4 | from os import environ 5 | import re 6 | from config import weather 7 | 8 | 9 | @bot.on(events.NewMessage(**weather)) 10 | async def get_weather(event): 11 | logger.info("weather plugin called") 12 | query = event.pattern_match.string.split(" ") 13 | logger.info(f"weather query - {query}") 14 | openweather_api_key = environ.get("openweather_api_key", None) 15 | 16 | if len(query) == 1: 17 | logger.info(f"no city provided") 18 | await event.respond("Please provide a city name") 19 | return 20 | elif len(query) == 2: 21 | city_to_find = query[1] 22 | logger.info(f"city to find - {city_to_find}") 23 | 24 | latinfo = get_lat_lon_from_city(city_to_find) 25 | if latinfo is None: 26 | await event.respond("City not found") 27 | return 28 | lat, lon = latinfo 29 | 30 | logger.info(f"Sending current weather info") 31 | openweather_url = "https://api.openweathermap.org/data/2.5/weather" 32 | params = { 33 | "lat": lat, 34 | "lon": lon, 35 | "appid": openweather_api_key, 36 | "units": "metric", 37 | } 38 | weather_res = get_response(openweather_url, params) 39 | tg_response = format_weather_response(weather_res) 40 | await event.respond(tg_response) 41 | return 42 | elif len(query) == 3 and query[2] == "forecast": 43 | logger.info(f"forecast") 44 | await event.respond("Forecast not implemented yet") 45 | return 46 | elif len(query) == 3 and query[2] == "air": 47 | city_to_find = query[1] 48 | logger.info(f"city to find - {city_to_find}") 49 | 50 | latinfo = get_lat_lon_from_city(city_to_find) 51 | if latinfo is None: 52 | await event.respond("City not found") 53 | return 54 | lat, lon = latinfo 55 | 56 | logger.info(f"Sending current air pollution info") 57 | openweather_url = "https://api.openweathermap.org/data/2.5/air_pollution" 58 | params = { 59 | "lat": lat, 60 | "lon": lon, 61 | "appid": openweather_api_key, 62 | "units": "metric", 63 | } 64 | weather_res = get_response(openweather_url, params) 65 | tg_response = format_air_pollution_response(weather_res, city_to_find) 66 | await event.respond(tg_response) 67 | 68 | 69 | def get_lat_lon_from_city(city_to_find): 70 | # Prepare request 71 | openweather_api_key = environ.get("openweather_api_key", None) 72 | params = {"q": city_to_find, "appid": openweather_api_key} 73 | 74 | # get geocoding lat log first 75 | geocoding_url = "http://api.openweathermap.org/geo/1.0/direct" 76 | geocoding_res = get_response(geocoding_url, params) 77 | if len(geocoding_res) == 0: 78 | # logger.info(f"geocoding response - {geocoding_res}") 79 | return 80 | # logger.info(f"geocoding response - {geocoding_res}") 81 | 82 | lat, lon = geocoding_res[0]["lat"], geocoding_res[0]["lon"] 83 | return lat, lon 84 | 85 | 86 | def get_response(url, params): 87 | res = requests.get(url, params=params).json() 88 | return res 89 | 90 | 91 | def format_weather_response(weather_res): 92 | try: 93 | weather_icons = { 94 | "01d": "☀️", 95 | "01n": "☀️", 96 | "02d": "🌤️", 97 | "02n": "🌤️", 98 | "03d": "⛅", 99 | "03n": "⛅", 100 | "04d": "🌥️", 101 | "04n": "🌥️", 102 | "09d": "🌦️", 103 | "09n": "🌦️", 104 | "10d": "🌧️", 105 | "10n": "🌧️", 106 | "11d": "⛈️", 107 | "11n": "⛈️", 108 | "13d": "❄️", 109 | "13n": "❄️", 110 | "50d": "🌫️", 111 | "50n": "🌫️" 112 | } 113 | # logger.info(f"request to format {weather_res}") 114 | temp_in_celcius = weather_res["main"]["temp"] 115 | feels_like = weather_res["main"]["feels_like"] 116 | temp_min = weather_res["main"]["temp_min"] 117 | temp_max = weather_res["main"]["temp_max"] 118 | humidity = weather_res["main"]["humidity"] 119 | pressure = weather_res["main"]["pressure"] 120 | sky = weather_res["weather"][0]["description"] 121 | icon = weather_res["weather"][0]["icon"] 122 | city = weather_res["name"] 123 | country = weather_res["sys"]["country"] 124 | wind = weather_res["wind"]["speed"] 125 | if "rain" in weather_res: 126 | rain = weather_res["rain"]["1h"] 127 | else: 128 | rain = 0 129 | if "snow" in weather_res: 130 | snow = weather_res["snow"]["1h"] 131 | else: 132 | snow = 0 133 | cloud_coverage = weather_res["clouds"]["all"] 134 | # print("\n\n", re.findall("", pattern_string)) 135 | return f""" 136 | ``` 137 | Weather: {city},{country} - {weather_icons[icon]} {sky} 138 | 139 | Temperature: {temp_in_celcius:.2f}°C, 140 | Feels like: {feels_like:.2f}°C 141 | Min: {temp_min:.2f}°C 142 | Max: {temp_max:.2f}°C 143 | 144 | Humidity: {humidity}% 145 | Pressure: {pressure} Pa 146 | Wind: {wind} meter/sec 147 | Rain: {rain} cm (last 1 hour) 148 | Snow: {snow} mm (last 1 hour) 149 | 150 | Clouds: {cloud_coverage}% Coverage 151 | ``` 152 | """ 153 | except KeyError as e: 154 | logger.exception(f"Error in formatting {e}") 155 | pass 156 | except Exception as e: 157 | print("Error", weather_res) 158 | logger.exception(f"Error in formatting {e}") 159 | 160 | 161 | def format_air_pollution_response(air_response, location): 162 | print(air_response) 163 | 164 | aqi_emojis_dict = { 165 | "1": "🟢", 166 | "2": "🟡", 167 | "3": "🟠", 168 | "4": "🔴", 169 | "5": "🟤", 170 | } 171 | 172 | aqi = air_response["list"][0]["main"]["aqi"] 173 | co2 = air_response["list"][0]["components"]["co"] 174 | no = air_response["list"][0]["components"]["no"] 175 | no2 = air_response["list"][0]["components"]["no2"] 176 | o3 = air_response["list"][0]["components"]["o3"] 177 | so2 = air_response["list"][0]["components"]["so2"] 178 | pm2_5 = air_response["list"][0]["components"]["pm2_5"] 179 | pm10 = air_response["list"][0]["components"]["pm10"] 180 | nh3 = air_response["list"][0]["components"]["nh3"] 181 | 182 | return f""" 183 | Air Pollution: __{location}__ 184 | 185 | Air Quality Index: {aqi_emojis_dict[str(aqi)]} __{aqi}__ 186 | 187 | CO: __{co2} μg/m³__ (Carbon Monoxide) 188 | NO: __{no} μg/m³__ (Nitrogen Monoxide) 189 | NO2: __{no2} μg/m³__ (Nitrogen Dioxide) 190 | O3: __{o3} μg/m³__ (Ozone) 191 | SO2: __{so2} μg/m³__ (Sulfur Dioxide) 192 | PM2.5: __{pm2_5} μg/m³__ (Fine Particulate Matter) 193 | PM10: __{pm10} μg/m³__ (Coarse Particulate Matter) 194 | NH3: __{nh3} μg/m³__ (Ammonia) 195 | 196 | """ 197 | 198 | 199 | 200 | """ 201 | note: 202 | ``` 203 | CO - Carbon Monoxide 204 | NO - Nitrogen Monoxide 205 | NO2 - Nitrogen Dioxide 206 | O3 - Ozone 207 | SO2 - Sulfur Dioxide 208 | PM2.5 - Fine Particulate Matter 209 | PM10 - Coarse Particulate Matter 210 | NH3 - Ammonia 211 | ``` 212 | """ -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "bc88c11ae5e1d14f03b39aabfe693c2131954cf06572abc179287ebee98a5bc9" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "beautifulsoup4": { 18 | "hashes": [ 19 | "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", 20 | "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a" 21 | ], 22 | "index": "pypi", 23 | "version": "==4.12.2" 24 | }, 25 | "certifi": { 26 | "hashes": [ 27 | "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", 28 | "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" 29 | ], 30 | "markers": "python_version >= '3.6'", 31 | "version": "==2023.5.7" 32 | }, 33 | "charset-normalizer": { 34 | "hashes": [ 35 | "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", 36 | "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", 37 | "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", 38 | "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", 39 | "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", 40 | "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", 41 | "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", 42 | "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", 43 | "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", 44 | "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", 45 | "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", 46 | "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", 47 | "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", 48 | "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", 49 | "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", 50 | "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", 51 | "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", 52 | "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", 53 | "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", 54 | "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", 55 | "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", 56 | "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", 57 | "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", 58 | "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", 59 | "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", 60 | "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", 61 | "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", 62 | "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", 63 | "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", 64 | "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", 65 | "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", 66 | "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", 67 | "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", 68 | "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", 69 | "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", 70 | "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", 71 | "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", 72 | "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", 73 | "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", 74 | "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", 75 | "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", 76 | "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", 77 | "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", 78 | "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", 79 | "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", 80 | "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", 81 | "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", 82 | "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", 83 | "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", 84 | "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", 85 | "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", 86 | "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", 87 | "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", 88 | "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", 89 | "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", 90 | "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", 91 | "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", 92 | "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", 93 | "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", 94 | "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", 95 | "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", 96 | "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", 97 | "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", 98 | "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", 99 | "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", 100 | "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", 101 | "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", 102 | "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", 103 | "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", 104 | "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", 105 | "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", 106 | "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", 107 | "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", 108 | "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", 109 | "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" 110 | ], 111 | "markers": "python_full_version >= '3.7.0'", 112 | "version": "==3.1.0" 113 | }, 114 | "idna": { 115 | "hashes": [ 116 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 117 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 118 | ], 119 | "markers": "python_version >= '3.5'", 120 | "version": "==3.4" 121 | }, 122 | "pyaes": { 123 | "hashes": [ 124 | "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f" 125 | ], 126 | "version": "==1.6.1" 127 | }, 128 | "pyasn1": { 129 | "hashes": [ 130 | "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", 131 | "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde" 132 | ], 133 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 134 | "version": "==0.5.0" 135 | }, 136 | "python-dotenv": { 137 | "hashes": [ 138 | "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", 139 | "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" 140 | ], 141 | "index": "pypi", 142 | "version": "==1.0.0" 143 | }, 144 | "requests": { 145 | "hashes": [ 146 | "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294", 147 | "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4" 148 | ], 149 | "index": "pypi", 150 | "version": "==2.30.0" 151 | }, 152 | "rsa": { 153 | "hashes": [ 154 | "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", 155 | "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" 156 | ], 157 | "markers": "python_version >= '3.6' and python_version < '4'", 158 | "version": "==4.9" 159 | }, 160 | "soupsieve": { 161 | "hashes": [ 162 | "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8", 163 | "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea" 164 | ], 165 | "markers": "python_version >= '3.7'", 166 | "version": "==2.4.1" 167 | }, 168 | "telethon": { 169 | "hashes": [ 170 | "sha256:b3990ec22351a3f3e1af376729c985025bbdd3bdabdde8c156112c3d3dfe1941", 171 | "sha256:edc42fd58b8e1569830d3ead564cafa60fd51d684f03ee2a1fdd5f77a5a10438" 172 | ], 173 | "index": "pypi", 174 | "version": "==1.28.5" 175 | }, 176 | "urllib3": { 177 | "hashes": [ 178 | "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", 179 | "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" 180 | ], 181 | "markers": "python_version >= '3.7'", 182 | "version": "==2.0.2" 183 | } 184 | }, 185 | "develop": { 186 | "astroid": { 187 | "hashes": [ 188 | "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324", 189 | "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f" 190 | ], 191 | "markers": "python_full_version >= '3.7.2'", 192 | "version": "==2.15.5" 193 | }, 194 | "autopep8": { 195 | "hashes": [ 196 | "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1", 197 | "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c" 198 | ], 199 | "index": "pypi", 200 | "version": "==2.0.2" 201 | }, 202 | "black": { 203 | "hashes": [ 204 | "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5", 205 | "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915", 206 | "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326", 207 | "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940", 208 | "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b", 209 | "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30", 210 | "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c", 211 | "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c", 212 | "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab", 213 | "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27", 214 | "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2", 215 | "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961", 216 | "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9", 217 | "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb", 218 | "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70", 219 | "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331", 220 | "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2", 221 | "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266", 222 | "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d", 223 | "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6", 224 | "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b", 225 | "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925", 226 | "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8", 227 | "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4", 228 | "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3" 229 | ], 230 | "index": "pypi", 231 | "version": "==23.3.0" 232 | }, 233 | "click": { 234 | "hashes": [ 235 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 236 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 237 | ], 238 | "markers": "python_version >= '3.7'", 239 | "version": "==8.1.3" 240 | }, 241 | "colorama": { 242 | "hashes": [ 243 | "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", 244 | "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" 245 | ], 246 | "markers": "sys_platform == 'win32'", 247 | "version": "==0.4.6" 248 | }, 249 | "dill": { 250 | "hashes": [ 251 | "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", 252 | "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" 253 | ], 254 | "markers": "python_version < '3.11'", 255 | "version": "==0.3.6" 256 | }, 257 | "isort": { 258 | "hashes": [ 259 | "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", 260 | "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" 261 | ], 262 | "markers": "python_full_version >= '3.8.0'", 263 | "version": "==5.12.0" 264 | }, 265 | "lazy-object-proxy": { 266 | "hashes": [ 267 | "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", 268 | "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", 269 | "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", 270 | "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", 271 | "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", 272 | "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", 273 | "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", 274 | "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", 275 | "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", 276 | "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", 277 | "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", 278 | "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", 279 | "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", 280 | "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", 281 | "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", 282 | "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", 283 | "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", 284 | "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", 285 | "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", 286 | "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", 287 | "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", 288 | "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", 289 | "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", 290 | "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", 291 | "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", 292 | "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", 293 | "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", 294 | "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", 295 | "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", 296 | "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", 297 | "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", 298 | "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", 299 | "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", 300 | "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", 301 | "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", 302 | "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" 303 | ], 304 | "markers": "python_version >= '3.7'", 305 | "version": "==1.9.0" 306 | }, 307 | "mccabe": { 308 | "hashes": [ 309 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 310 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 311 | ], 312 | "markers": "python_version >= '3.6'", 313 | "version": "==0.7.0" 314 | }, 315 | "mypy-extensions": { 316 | "hashes": [ 317 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 318 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 319 | ], 320 | "markers": "python_version >= '3.5'", 321 | "version": "==1.0.0" 322 | }, 323 | "packaging": { 324 | "hashes": [ 325 | "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", 326 | "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" 327 | ], 328 | "markers": "python_version >= '3.7'", 329 | "version": "==23.1" 330 | }, 331 | "pathspec": { 332 | "hashes": [ 333 | "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", 334 | "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" 335 | ], 336 | "markers": "python_version >= '3.7'", 337 | "version": "==0.11.1" 338 | }, 339 | "platformdirs": { 340 | "hashes": [ 341 | "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f", 342 | "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5" 343 | ], 344 | "markers": "python_version >= '3.7'", 345 | "version": "==3.5.1" 346 | }, 347 | "pycodestyle": { 348 | "hashes": [ 349 | "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", 350 | "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" 351 | ], 352 | "markers": "python_version >= '3.6'", 353 | "version": "==2.10.0" 354 | }, 355 | "pylint": { 356 | "hashes": [ 357 | "sha256:eb035800b371862e783f27067b3fc00c6b726880cacfed101b619f366bb813f6", 358 | "sha256:f0b0857f6fba90527a30f39937a9f66858b59c5dcbb8c062821ad665637bb742" 359 | ], 360 | "index": "pypi", 361 | "version": "==3.0.0a6" 362 | }, 363 | "tomli": { 364 | "hashes": [ 365 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 366 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 367 | ], 368 | "markers": "python_version < '3.11'", 369 | "version": "==2.0.1" 370 | }, 371 | "tomlkit": { 372 | "hashes": [ 373 | "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171", 374 | "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3" 375 | ], 376 | "markers": "python_version >= '3.7'", 377 | "version": "==0.11.8" 378 | }, 379 | "typing-extensions": { 380 | "hashes": [ 381 | "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", 382 | "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" 383 | ], 384 | "markers": "python_version < '3.11'", 385 | "version": "==4.5.0" 386 | }, 387 | "wrapt": { 388 | "hashes": [ 389 | "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", 390 | "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", 391 | "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", 392 | "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", 393 | "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", 394 | "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", 395 | "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", 396 | "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", 397 | "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", 398 | "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", 399 | "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", 400 | "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", 401 | "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", 402 | "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", 403 | "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", 404 | "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", 405 | "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", 406 | "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", 407 | "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", 408 | "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", 409 | "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", 410 | "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", 411 | "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", 412 | "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", 413 | "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", 414 | "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", 415 | "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", 416 | "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", 417 | "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", 418 | "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", 419 | "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", 420 | "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", 421 | "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", 422 | "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", 423 | "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", 424 | "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", 425 | "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", 426 | "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", 427 | "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", 428 | "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", 429 | "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", 430 | "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", 431 | "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", 432 | "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", 433 | "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", 434 | "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", 435 | "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", 436 | "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", 437 | "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", 438 | "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", 439 | "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", 440 | "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", 441 | "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", 442 | "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", 443 | "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", 444 | "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", 445 | "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", 446 | "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", 447 | "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", 448 | "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", 449 | "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", 450 | "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", 451 | "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", 452 | "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", 453 | "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", 454 | "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", 455 | "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", 456 | "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", 457 | "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", 458 | "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", 459 | "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", 460 | "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", 461 | "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", 462 | "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", 463 | "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" 464 | ], 465 | "markers": "python_version < '3.11'", 466 | "version": "==1.15.0" 467 | } 468 | } 469 | } 470 | --------------------------------------------------------------------------------