From 9609efedc0583c29e30590debc2a5df60801334e Mon Sep 17 00:00:00 2001 From: sp3rtah Date: Sun, 16 Jul 2023 08:14:25 +0300 Subject: [PATCH] production checkout --- .gitignore | 142 +++++++++++++++ LICENSE | 21 +++ Makefile | 21 +++ README.md | 22 +++ autogram.png | Bin 0 -> 42055 bytes autogram/__init__.py | 25 +++ autogram/app.py | 74 ++++++++ autogram/base.py | 342 +++++++++++++++++++++++++++++++++++ autogram/config.py | 53 ++++++ autogram/updates/__init__.py | 0 autogram/updates/base.py | 0 autogram/webserver.py | 29 +++ pyproject.toml | 32 ++++ requirements.txt | 4 + start.py | 49 +++++ 15 files changed, 814 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 autogram.png create mode 100644 autogram/__init__.py create mode 100644 autogram/app.py create mode 100644 autogram/base.py create mode 100644 autogram/config.py create mode 100644 autogram/updates/__init__.py create mode 100644 autogram/updates/base.py create mode 100644 autogram/webserver.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 start.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4db2d10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.vscode +# C extensions +*.so + +# config files +*.json +.env + +# temp dir +Downloads/ + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +__pycache__/ +.env +venv/ +files/ +*.log +.venv diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5104229 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 drui9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..453bd9b --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +env := .venv +deps := requirements.txt + +run: + @clear;./$(env)/bin/python start.py + +build: + @./$(env)/bin/pip install build;./$(env)/bin/python -m build + +clean: + @rm -rf dist build *.egg-info **/__pycache__/ + +stable: clean build + git push;git checkout releases;git merge main;git push;twine upload dist/*;git checkout main; + +$(env): $(deps) + python -m venv $@ + +install: $(env) + @./$(env)/bin/pip install -r $(deps) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd29ff1 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +

+ Autogram +

+ +## Installation :: Python3 +`pip install autogram` + +## `An efficient asyncronous Telegram bot API wrapper!` +Autogram is a telegram BOT API wrapper with focus on simplicity and performance. + +## `Why AutoGram?` +I need a bot framework that makes it easy to administer control remotely. + +## `Project TODOs` +- Plans to cover the entire telegram API methods. + +### `footnotes` +- `Polling` can be implemented by the user, while feeding data to the bot through `bot.parseUpdate(...)` +- Don't run multiple bots with the same `TOKEN` as this will cause update problems +- Sending unescaped special characters when using MarkdownV2 will return HTTP400 +- Have `fun` with whatever you're building `;)` + diff --git a/autogram.png b/autogram.png new file mode 100644 index 0000000000000000000000000000000000000000..6404525a08e0f246a8c3a5f7fe6ddb353f4f9e40 GIT binary patch literal 42055 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 None: + """Initialize parent object""" + self.update_handler = None + super().__init__(config) + return + + def addHandler(self, function): + self.update_handler = function + return function + + def prepare(self): + """Confirm auth through getMe(), then check update methods""" + res = self.getMe() + if not res.ok: + self.do_err(msg=str(res.json())) + self.webhook_addr = self.config.get('AUTOGRAM_ENDPOINT') or os.getenv('AUTOGRAM_ENDPOINT') # noqa: E501 + if self.webhook_addr: + res = self.setWebhook(self.webhook_addr) + if not res.ok: + self.do_err(msg='/setWebhook failed!') + else: + res = self.deleteWebhook() + if not res.ok: + self.do_err('/deleteWebhook failed!') + else: + self.short_poll() + return + + def start(self): + """Launch the bot""" + try: + self.prepare() + while not self.terminate.is_set(): + try: + if not self.update_handler: + time.sleep(5) + continue + self.update_handler(self.updates.get()) + except queue.Empty: + continue + except ConnectionError: + self.terminate.set() + self.logger.critical('Connection Error!') + finally: + self.shutdown() + + def shutdown(self): + """Gracefully terminate the bot""" + if self.terminate.is_set(): + try: + res = self.getWebhookInfo() + if not res.ok: + return + if not res.json()['result']['url']: + return + except Exception: + return + # delete webhook and exit + try: + res = self.deleteWebhook() + if not res.ok: + raise RuntimeError() + except Exception: + self.logger.critical('/deleteWebhook failed!') + finally: + self.terminate.set() diff --git a/autogram/base.py b/autogram/base.py new file mode 100644 index 0000000..146bc0d --- /dev/null +++ b/autogram/base.py @@ -0,0 +1,342 @@ +import os +import re +import time +import json +import loguru +import threading +import requests +from typing import Any +from queue import Queue +from requests.models import Response +from autogram.webserver import WebServer +from bottle import request, response, post, run, get +from autogram.config import save_config +from . import chat_actions + + +class Bot(): + endpoint = 'https://api.telegram.org/' + + def __init__(self, config :dict) -> None: + """Initialize parent database object""" + super().__init__() + self.updates = Queue() + self.logger = loguru.logger + self.terminate = threading.Event() + self.requests = requests.session() + self.config = config or self.do_err(msg='Please pass a config !') + if not self.config.get("telegram-token"): + self.config.update({ + "telegram-token" : os.getenv('AUTOGRAM_TG_TOKEN') or self.do_err(msg='Missing bot token!') # noqa: E501 + }) + + def do_err(self, err_type =RuntimeError, msg ='Error!'): + """Clean terminate the program on errors.""" + self.terminate.set() + raise err_type(msg) + + def settings(self, key :str, val: Any|None=None): + """Get or set value of key in config""" + if val: + self.config.update({key: val}) + save_config(self.config) + return val + elif not (ret := self.config.get(key)): + self.do_err(msg=f'Missing key in config: {key}') + return ret + + def media_quality(self): + """Get preffered media quality.""" + if (quality := self.settings("media-quality").lower() or 'low') == 'low': + return 0 + elif quality == 'high': + return 2 + return 1 + + def setWebhook(self, hook_addr : str): + if not re.search('^(https?):\\/\\/[^\\s/$.?#].[^\\s]*$', hook_addr): + raise RuntimeError('Invalid webhook url. format ') + # ensure hook_addr stays reachable + @get('/') + def ping(): + return json.dumps({'ok': True}) + # keep-alive service + def keep_alive(): + self.logger.info('Keep-alive started.') + while not self.terminate.is_set(): + try: + res = self.requests.get(hook_addr) + if not res.ok: + self.logger.debug('Ngrok tunnel disconnected!') + except Exception: + self.logger.debug('Connection error.') + time.sleep(3) + # start keep-alive + alive_guard = threading.Thread(target=keep_alive) + alive_guard.name = 'Autogram:Keep-alive' + alive_guard.daemon = True + alive_guard.start() + # receive updates + @post('/') + def hookHandler(): + response.content_type = 'application/json' + self.updates.put(request.json) + return json.dumps({'ok': True}) + # + def runServer(server: Any): + return run(server=server, quiet=True) + # + server = WebServer(host="0.0.0.0", port=self.settings('lport')) + serv_thread = threading.Thread(target=runServer, args=(server,)) + serv_thread.name = 'Autogram:Bottle' + serv_thread.daemon = True + serv_thread.start() + # inform telegram + url = f'{self.endpoint}bot{self.settings("telegram-token")}/setWebhook' + params = { + 'url': hook_addr + } + return self.requests.get(url, params=params) + + def short_poll(self): + """Start fetching updates in seperate thread""" + def getter(): + failed = False + offset = 0 + while not self.terminate.is_set(): + try: + data = { + 'timeout': 3, + 'params': { + 'offset': offset, + 'limit': 10, + 'timeout': 1 + } + } + res = self.getUpdates(**data) + except Exception: + time.sleep(2) + continue + # + if not res.ok: + if not failed: + time.sleep(2) + failed = True + else: + self.terminate.set() + else: + updates = res.json()['result'] + for update in updates: + offset = update['update_id'] + 1 + self.updates.put(update) + # rate-limit + poll_interval = 2 + time.sleep(poll_interval) + return + poll = threading.Thread(target=getter) + poll.name = 'Autogram:short_polling' + poll.daemon = True + poll.start() + + def getMe(self) -> Response: + """Fetch `bot` information""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/getMe' + return self.requests.get(url) + + def getUpdates(self, **kwargs) -> Response: + url = f'{self.endpoint}bot{self.settings("telegram-token")}/getUpdates' + return self.requests.get(url, **kwargs) + + def downloadFile(self, file_path: str) -> Response: + """Downloads a file with file_path got from getFile(...)""" + url = f'https://api.telegram.org/file/bot{self.settings("telegram-token")}/{file_path}' + return self.requests.get(url) + + def getFile(self, file_id: str) -> Response: + """Gets details of file with file_id""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/getFile' + return self.requests.get(url, params={'file_id': file_id}) + + def getChat(self, chat_id: int) -> Response: + """Gets information on chat_id""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/getChat' + return self.requests.get(url, params={'chat_id': chat_id}) + + def getWebhookInfo(self) -> Response: + """Gets information on currently set webhook""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/getWebhookInfo' + return self.requests.get(url) + + def sendChatAction(self, chat_id: int, action: str) -> Response: + """Sends `action` to chat_id""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/sendChatAction' + params = { + 'chat_id': chat_id, + 'action': action + } + return self.requests.get(url, params=params) + + def sendMessage(self, chat_id :int|str, text :str, **kwargs) -> Response: + """Sends `text` to `chat_id`""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/sendMessage' + params = { + 'params': { + 'chat_id': chat_id, + 'text': text, + } | kwargs + } + return self.requests.get(url, **params) + + def deleteMessage(self, chat_id: int, msg_id: int) -> Response: + """Deletes message sent <24hrs ago""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/deleteMessage' + params= { + 'chat_id': chat_id, + 'message_id': msg_id + } + return self.requests.get(url, params=params) + + def deleteWebhook(self, drop_pending = False) -> Response: + """Deletes webhook value""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/deleteWebhook' + return self.requests.get(url, params={'drop_pending_updates': drop_pending}) + + def editMessageText(self, chat_id: int, msg_id: int, text: str, **kwargs) -> Response: + """Edit message sent <24hrs ago""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/editMessageText' + params = { + 'params': { + 'text':text, + 'chat_id': chat_id, + 'message_id': msg_id + } | kwargs + } + return self.requests.get(url, **params) + + def editMessageCaption(self, chat_id: int, msg_id: int, capt: str, params={}) -> Response: # noqa: E501 + """Edit message caption""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/editMessageCaption' + params = { + 'params': { + 'chat_id': chat_id, + 'message_id': msg_id, + 'caption': capt + }|params + } + return self.requests.get(url, **params) + + def editMessageReplyMarkup(self, chat_id: int, msg_id: int, markup: str, params={}) -> Response: # noqa: E501 + """Edit reply markup""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/editMessageReplyMarkup' + params = { + 'params': { + 'chat_id':chat_id, + 'message_id':msg_id, + 'reply_markup': markup + }|params + } + return self.requests.get(url, **params) + + def forwardMessage(self, chat_id: int, from_chat_id: int, msg_id: int) -> Response: + """Forward message with message_id from from_chat_id to chat_id""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/forwardMessage' + params = { + 'params': { + 'chat_id': chat_id, + 'from_chat_id': from_chat_id, + 'message_id': msg_id + } + } + return self.requests.get(url, **params) + + def answerCallbackQuery(self, query_id, text :str|None =None, params : dict|None =None) -> Response: # noqa: E501 + """Answers callback queries with text: str of len(text) < 200""" + url = f'{self.endpoint}bot{self.settings("telegram-token")}/answerCallbackQuery' + params = params or {} + text = text or 'Updated!' + params.update({ + 'callback_query_id':query_id, + 'text': text[:200] + }) + return self.requests.get(url, params=params) + + def sendPhoto(self,chat_id: int, photo_bytes: bytes, caption: str|None = None, params: dict|None = None) -> Response: # noqa: E501 + """Sends a photo to a telegram user""" + params = params or {} + url = f'{self.endpoint}bot{self.settings("telegram-token")}/sendPhoto' + params.update({ + 'chat_id':chat_id, + 'caption': caption, + }) + self.sendChatAction(chat_id, chat_actions.photo) + return self.requests.get(url, params=params, files={'photo': photo_bytes}) + + def sendAudio(self,chat_id: int,audio :bytes|str, caption: str|None = None, params: dict|None = None) -> Response: # noqa: E501 + """Sends an audio to a telegram user""" + params = params or {} + url = f'{self.endpoint}bot{self.settings("telegram-token")}/sendAudio' + params |= { + 'chat_id':chat_id, + 'caption': caption + } + self.sendChatAction(chat_id, chat_actions.audio) + if isinstance(audio, bytes): + return self.requests.get(url, params=params, files={'audio': audio}) + params.update({'audio': audio}) + return self.requests.get(url, params=params) + + def sendDocument(self,chat_id: int ,document_bytes: bytes, caption: str|None = None, params: dict|None = None) -> Response: # noqa: E501 + """Sends a document to a telegram user""" + params = params or {} + url = f'{self.endpoint}bot{self.settings("telegram-token")}/sendDocument' + params.update({ + 'chat_id':chat_id, + 'caption':caption + }) + self.sendChatAction(chat_id, chat_actions.document) + self.requests.get(url, params=params, files={'document': document_bytes}) + + def sendVideo(self,chat_id: int ,video_bytes: bytes, caption: str|None = None, params: dict|None = None ) -> Response: # noqa: E501 + """Sends a video to a telegram user""" + params = params or {} + url = f'{self.endpoint}bot{self.settings("telegram-token")}/sendVideo' + params.update({ + 'chat_id':chat_id, + 'caption':caption + }) + self.sendChatAction(chat_id, chat_actions.video) + return self.requests.get(url, params=params,files={'video':video_bytes}) + + def forceReply(self, params: dict|None = None) -> str: + """Returns forceReply value as string""" + params = params or {} + markup = { + 'force_reply': True, + }|params + return json.dumps(markup) + + def getKeyboardMarkup(self, keys: list, params: dict|None =None) -> str: + """Returns keyboard markup as string""" + params = params or {} + markup = { + "keyboard":[row for row in keys] + } | params + return json.dumps(markup) + + def getInlineKeyboardMarkup(self, keys: list, params: dict|None =None) -> str: + params = params or {} + markup = { + 'inline_keyboard':keys + }|params + return json.dumps(markup) + + def parseFilters(self, filters: dict|None =None) -> str: + filters = filters or {} + return json.dumps(filters.keys()) + + def removeKeyboard(self, params: dict|None =None) -> str: + params = params or {} + markup = { + 'remove_keyboard': True, + }|params + return json.dumps(markup) diff --git a/autogram/config.py b/autogram/config.py new file mode 100644 index 0000000..741e4d3 --- /dev/null +++ b/autogram/config.py @@ -0,0 +1,53 @@ +import os +import sys +import json +from loguru import logger +from typing import Callable, Dict + +default_config = { + 'lport': 4004, + 'media-quality': 'low', + 'telegram-token': None +} + +def load_config(config_file : str, config_path : str): + """Load configuration file from config_path dir""" + if not os.path.exists(config_path): + os.mkdir(config_path) + # + configuration = os.path.join(config_path, config_file) + if not os.path.exists(configuration): + with open(configuration, 'w') as conf: + json.dump(default_config, conf, indent=3) + logger.critical(f"Please edit [{configuration}]") + sys.exit(0) + config = {'config-file': configuration} + with open(configuration, 'r') as conf: + config |= json.load(conf) + return config + +def save_config(config :Dict): + """config-file must be in the dictionary""" + try: + conffile = config.pop('config-file') + with open(conffile, 'w') as conf: + json.dump(config, conf, indent=2) + conf.flush() + except Exception: + conffile = conffile or None + if conffile: + return config | {'config-file': conffile} + else: + logger.critical('Failed saving config file!') + +def Start(config_file :str|None =None, config_path :str|None =None): + """Call custom function with config as parameter""" + config_path = config_path or os.getcwd() + config_file = config_file or 'autogram.json' + # + def wrapper(func: Callable): + return func(load_config(config_file, config_path)) + return wrapper +# + +__all__ = [ "Start", "save_config", "load_config"] diff --git a/autogram/updates/__init__.py b/autogram/updates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autogram/updates/base.py b/autogram/updates/base.py new file mode 100644 index 0000000..e69de29 diff --git a/autogram/webserver.py b/autogram/webserver.py new file mode 100644 index 0000000..4ea0115 --- /dev/null +++ b/autogram/webserver.py @@ -0,0 +1,29 @@ +import socket +from bottle import WSGIRefServer +from wsgiref.simple_server import make_server +from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + +class WebServer(WSGIRefServer): + def run(self, app): + # + class FixedHandler(WSGIRequestHandler): + def address_string(self): + return self.client_address[0] + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + # + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + # + if ':' in self.host: + if getattr(server_cls, 'address_family') == socket.AF_INET: + class server_cls(server_cls): + address_family = socket.AF_INET6 + # + srv = make_server(self.host, self.port, app, server_cls, handler_cls) + self.srv = srv + srv.serve_forever() + + def shutdown(self): + self.srv.shutdown() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f0a58ea --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +# pyproject.toml + +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "autogram" +version = "3.4.5" +description = "An easily extensible telegram API wrapper" +readme = "README.md" +authors = [{ name = "droi9", email = "ngaira14nelson@gmail.com" }] +license = { file = "LICENSE" } +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", +] +keywords = ["telegram", "API", "wrapper"] +dependencies = [ + "SQLAlchemy==2.0.19", + "loguru==0.7.0", + "bottle==0.12.25", + "requests==2.31.0", +] +requires-python = ">=3.6" + +[project.optional-dependencies] +dev = ["build", "twine"] + +[project.urls] +Homepage = "https://github.com/droi9/autogram" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..906d8a3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +arrow>=1.3.0 +loguru>=0.7.2 +requests>=2.32.2 +python-dotenv>=1.0.1 diff --git a/start.py b/start.py new file mode 100644 index 0000000..fd82b22 --- /dev/null +++ b/start.py @@ -0,0 +1,49 @@ +""" +@author: drui9 +@config values: + - update-endpoint: optional + - ngrok-token: optional + - bot-token: required + - cava-auth: optional +""" +import os +import requests +from unittest import mock +from autogram import Autogram +from dotenv import load_dotenv +from autogram.config import load_config + +def get_update_endpoint(validator_fn, inject_key=None): + """Get updates endpoint, silently default to telegram servers""" + if key := os.getenv('ngrok-api-key', inject_key): + header = {'Authorization': f'Bearer {key}', 'Ngrok-Version': '2'} + try: + def getter(): + if os.getenv('TESTING') == '1': + return 'http://localhost:8000' + rep = requests.get('https://api.ngrok.com/tunnels', headers=header, timeout=6) + if rep.ok and (out := validator_fn(rep.json())): + return out + return getter + except Exception: + raise + return Autogram.api_endpoint + +# modify to select one ngrok tunnel from list of tunnels +def select_tunnel(tunnels): + for tunnel in tunnels['tunnels']: + if tunnel['forwards_to'] == 'http://api:8000': + return tunnel['public_url'] + +# launcher +if __name__ == '__main__': + load_dotenv() + config = Autogram.cfg_template() + with load_config(config): + if ngrok_token := config.get('ngrok-token'): + if getter := get_update_endpoint(select_tunnel, ngrok_token): + config['update-endpoint'] = getter() + bot = Autogram(config) + bot.getter = getter # fn to get updated endpoint + bot.loop() +