├── app ├── __init__.py ├── routers │ ├── __init__.py │ ├── telegram.py │ └── whatsapp.py ├── main.py └── dependecies.py ├── .env.example ├── Dockerfile ├── fly.toml ├── requirements.txt ├── LICENSE ├── .gitignore └── readme.md /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPEN_AI_API_KEY= 2 | 3 | TELEGRAM_BOT_TOKEN= 4 | TELEGRAM_WEBHOOK_SECRET= 5 | TELEGRAM_ALLOWED_USER_IDS= 6 | 7 | WA_API_KEY= 8 | WA_PHONE_NUMBER_ID= 9 | WA_VERIFY_TOKEN= -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from .routers import telegram, whatsapp 4 | 5 | app = FastAPI(docs_url=None, redoc_url=None) 6 | app.include_router(telegram.router) 7 | app.include_router(whatsapp.router) 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 python:3.12 2 | 3 | WORKDIR /app 4 | 5 | COPY ./requirements.txt /app/requirements.txt 6 | RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt 7 | 8 | COPY ./app /app 9 | 10 | CMD ["fastapi", "run", "main.py", "--proxy-headers", "--port", "8080"] -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for listen4me-bot on 2024-05-31T14:26:48+02:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'listen4me-bot' 7 | primary_region = 'cdg' 8 | 9 | [build] 10 | 11 | [http_service] 12 | internal_port = 8080 13 | force_https = true 14 | auto_stop_machines = true 15 | auto_start_machines = true 16 | min_machines_running = 0 17 | processes = ['app'] 18 | 19 | [[vm]] 20 | memory = '1gb' 21 | cpu_kind = 'shared' 22 | cpus = 1 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.4.0 3 | certifi==2024.2.2 4 | charset-normalizer==3.3.2 5 | click==8.1.7 6 | distro==1.9.0 7 | dnspython==2.6.1 8 | email_validator==2.1.1 9 | fastapi==0.111.0 10 | fastapi-cli==0.0.4 11 | h11==0.14.0 12 | httpcore==1.0.5 13 | httptools==0.6.1 14 | httpx==0.27.0 15 | idna==3.7 16 | Jinja2==3.1.4 17 | markdown-it-py==3.0.0 18 | MarkupSafe==2.1.5 19 | mdurl==0.1.2 20 | openai==1.30.5 21 | orjson==3.10.3 22 | pydantic==2.7.2 23 | pydantic_core==2.18.3 24 | Pygments==2.18.0 25 | python-dotenv==1.0.1 26 | python-multipart==0.0.9 27 | PyYAML==6.0.1 28 | requests==2.32.3 29 | rich==13.7.1 30 | shellingham==1.5.4 31 | sniffio==1.3.1 32 | starlette==0.37.2 33 | tqdm==4.66.4 34 | typer==0.12.3 35 | typing_extensions==4.12.0 36 | ujson==5.10.0 37 | urllib3==2.2.1 38 | uvicorn==0.30.0 39 | uvloop==0.19.0 40 | watchfiles==0.22.0 41 | websockets==12.0 42 | -------------------------------------------------------------------------------- /app/dependecies.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from dotenv import load_dotenv 6 | from openai import OpenAI 7 | 8 | load_dotenv() 9 | 10 | 11 | def get_console_logger(name: str) -> logging.Logger: 12 | logger = logging.getLogger(name) 13 | logger.setLevel(logging.DEBUG) 14 | 15 | console_log_handler = logging.StreamHandler(sys.stdout) 16 | console_log_handler.setFormatter(logging.Formatter("%(asctime)s.%(msecs)03.0f [%(threadName)s] %(levelname)s %(module)s - %(message)s", 17 | "%Y-%m-%d %H:%M:%S")) 18 | logger.addHandler(console_log_handler) 19 | 20 | return logger 21 | 22 | 23 | client = OpenAI(api_key=os.getenv("OPEN_AI_API_KEY")) 24 | 25 | 26 | def transcribe_audio(file_name: str, file_content: bytes) -> str: 27 | transcription = client.audio.transcriptions.create(model="whisper-1", file=(file_name, file_content)) 28 | if transcription.text == "": 29 | return "Sorry, the audio file has no speech." 30 | 31 | return transcription.text 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Uladzislau Radkevich 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. -------------------------------------------------------------------------------- /app/routers/telegram.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Annotated 3 | 4 | import requests 5 | from dotenv import load_dotenv 6 | from fastapi import BackgroundTasks, Body, Header, HTTPException, APIRouter 7 | 8 | from ..dependecies import get_console_logger, transcribe_audio 9 | 10 | load_dotenv() 11 | 12 | router = APIRouter(prefix="/telegram", tags=["telegram"]) 13 | 14 | logger = get_console_logger("transcribe-bot-telegram") 15 | 16 | token = os.getenv("TELEGRAM_BOT_TOKEN") 17 | webhook_secret = os.getenv("TELEGRAM_WEBHOOK_SECRET") 18 | allowed_user_ids = set(map(int, os.getenv("TELEGRAM_ALLOWED_USER_IDS").split(","))) if os.getenv("TELEGRAM_ALLOWED_USER_IDS") else None 19 | 20 | 21 | def send_message(chat_id: str, text: str, reply_to_message_id: str | None = None) -> None: 22 | if reply_to_message_id is None: 23 | data = { 24 | "chat_id": chat_id, 25 | "text": text 26 | } 27 | else: 28 | data = { 29 | "chat_id": chat_id, 30 | "text": text, 31 | "reply_parameters": { 32 | "message_id": reply_to_message_id 33 | } 34 | } 35 | response = requests.post(f"https://api.telegram.org/bot{token}/sendMessage", json=data) 36 | response.raise_for_status() 37 | 38 | 39 | def load_file_content(url): 40 | r = requests.get(url) 41 | r.raise_for_status() 42 | return r.content 43 | 44 | 45 | class AudioProcessingTask: 46 | def __init__(self, chat_id: str, message_id: str, file_id: str): 47 | self.chat_id = chat_id 48 | self.message_id = message_id 49 | self.file_id = file_id 50 | 51 | 52 | # TODO: make this more robust, handle errors and retries 53 | def process_audio(task: AudioProcessingTask) -> None: 54 | response = requests.get(f"https://api.telegram.org/bot{token}/getFile?file_id={task.file_id}") 55 | response.raise_for_status() 56 | 57 | file_path = response.json()["result"]["file_path"] 58 | file_content = load_file_content(f"https://api.telegram.org/file/bot{token}/{file_path}") 59 | 60 | transcription = transcribe_audio(file_path.split("/")[-1], file_content) 61 | send_message(task.chat_id, transcription, task.message_id) 62 | 63 | 64 | @router.post("/l4me_bot/webhook") 65 | async def handle_webhook(background_tasks: BackgroundTasks, 66 | x_telegram_bot_api_secret_token: Annotated[str | None, Header()] = None, 67 | payload: Any = Body(None)): 68 | if x_telegram_bot_api_secret_token != webhook_secret: 69 | logger.error("Invalid secret token provided: %s", x_telegram_bot_api_secret_token) 70 | raise HTTPException(status_code=400, detail="Invalid secret token provided") 71 | 72 | message = payload["message"] 73 | chat_id = str(message["chat"]["id"]) 74 | message_id = str(message["message_id"]) 75 | user_id = message["from"]["id"] 76 | 77 | if allowed_user_ids and user_id not in allowed_user_ids: 78 | logger.warning(f"Unauthorized access attempt from user {user_id}") 79 | send_message(chat_id, "Sorry, you are not authorized to use this bot.") 80 | return {} 81 | 82 | if message.get("voice"): 83 | voice = message["voice"] 84 | task = AudioProcessingTask(chat_id, message_id, voice["file_id"]) 85 | background_tasks.add_task(process_audio, task) 86 | elif message.get("video_note"): 87 | video_note = message["video_note"] 88 | task = AudioProcessingTask(chat_id, message_id, video_note["file_id"]) 89 | background_tasks.add_task(process_audio, task) 90 | elif message.get("text") == "/start": 91 | send_message(chat_id, "Hello there! 👋 Send or forward me a voice message and I will transcribe it for you.") 92 | else: 93 | send_message(chat_id, "At the moment, I can only process and understand audio messages. 🙈", message_id) 94 | 95 | return {} 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ -------------------------------------------------------------------------------- /app/routers/whatsapp.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Annotated 3 | 4 | import requests 5 | from dotenv import load_dotenv 6 | from fastapi import BackgroundTasks, Body, Query, HTTPException, APIRouter 7 | 8 | from ..dependecies import transcribe_audio, get_console_logger 9 | 10 | load_dotenv() 11 | 12 | router = APIRouter(prefix="/whatsapp", tags=["whatsapp"]) 13 | 14 | logger = get_console_logger("transcribe-bot-whatsapp") 15 | 16 | phone_number_id = os.getenv("WA_PHONE_NUMBER_ID") 17 | api_key = os.getenv("WA_API_KEY") 18 | headers = { 19 | "Accept": "application/json", 20 | "Content-Type": "application/json", 21 | "Authorization": f"Bearer {api_key}" 22 | } 23 | wa_verify_token = os.getenv("WA_VERIFY_TOKEN") 24 | 25 | 26 | def load_file_content(url): 27 | r = requests.get(url, headers={"Authorization": f"Bearer {api_key}"}) 28 | r.raise_for_status() 29 | return r.content 30 | 31 | 32 | class AudioProcessingTask: 33 | def __init__(self, wa_id: str, audio_id: str, message_id: str): 34 | self.wa_id = wa_id 35 | self.audio_id = audio_id 36 | self.message_id = message_id 37 | 38 | 39 | # todo: make this more robust, handle errors and retries 40 | def process_audio(task: AudioProcessingTask) -> None: 41 | response = requests.get(f"https://graph.facebook.com/v18.0/{task.audio_id}", headers=headers) 42 | response.raise_for_status() 43 | response_json = response.json() 44 | url = response_json["url"] 45 | file_type = response_json["mime_type"].split("/")[-1] 46 | 47 | file_content = load_file_content(url) 48 | 49 | transcription = transcribe_audio(f"file.{file_type}", file_content) 50 | send_message(task.wa_id, transcription, task.message_id) 51 | 52 | 53 | def mark_message_as_read(message_id: str) -> None: 54 | body = { 55 | "messaging_product": "whatsapp", 56 | "status": "read", 57 | "message_id": message_id 58 | } 59 | response = requests.post(f"https://graph.facebook.com/v18.0/{phone_number_id}/messages", headers=headers, json=body) 60 | response.raise_for_status() 61 | 62 | 63 | def send_message(wa_id: str, text: str, message_id: str | None = None) -> None: 64 | if message_id: 65 | body = { 66 | "messaging_product": "whatsapp", 67 | "recipient_type:": "individual", 68 | "to": wa_id, 69 | "type": "text", 70 | "text": { 71 | "body": text 72 | }, 73 | "context": { 74 | "message_id": message_id 75 | } 76 | } 77 | else: 78 | body = { 79 | "messaging_product": "whatsapp", 80 | "recipient_type:": "individual", 81 | "to": wa_id, 82 | "type": "text", 83 | "text": { 84 | "body": text 85 | } 86 | } 87 | response = requests.post(f"https://graph.facebook.com/v18.0/{phone_number_id}/messages", headers=headers, json=body) 88 | response.raise_for_status() 89 | 90 | 91 | @router.get("/l4me_bot/webhook") 92 | async def handle_webhook_validation(mode: Annotated[str, Query(alias="hub.mode")], 93 | verify_token: Annotated[str, Query(alias="hub.verify_token")], 94 | challenge: Annotated[int, Query(alias="hub.challenge")]): 95 | if mode != "subscribe" or verify_token != wa_verify_token: 96 | raise HTTPException(status_code=400, detail="Invalid mode or token provided") 97 | 98 | return challenge 99 | 100 | 101 | @router.post("/l4me_bot/webhook") 102 | async def handle_webhook(background_tasks: BackgroundTasks, payload: Any = Body(None)): 103 | value = payload["entry"][0]["changes"][0]["value"] 104 | if value.get("statuses"): 105 | logger.debug("Received status change: %s", value["statuses"]) 106 | pass 107 | elif value.get("messages"): 108 | message = value["messages"][0] 109 | wa_id = message["from"] 110 | message_type = message["type"] 111 | message_id = message["id"] 112 | 113 | mark_message_as_read(message_id) 114 | if message_type == "text": 115 | send_message(wa_id, "Hello there! 👋 Send or forward me a voice message and I will transcribe it for you.") 116 | elif message_type == "audio": 117 | background_tasks.add_task(process_audio, AudioProcessingTask(wa_id, message["audio"]["id"], message_id)) 118 | else: 119 | raise HTTPException(status_code=400, detail="Unknown message type") 120 | 121 | else: 122 | raise HTTPException(status_code=400, detail="Unknown webhook request type") 123 | 124 | return {} 125 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Listen4Me bot repository 2 | 3 | The repository contains the code for the Listen4Me bot. The bot is available on Telegram and WhatsApp and designed to transcribe audio 4 | messages to text. 5 | 6 | Telegram bot: [@Listen4Me](https://t.me/l4me_bot) 7 | 8 | WhatsApp bot: [@Listen4Me](https://wa.me/message/CZS3B3D7YNL3C1) 9 | 10 | ## Self-hosting options 11 | 12 | The bot can be run on a local machine or deployed to a cloud platform. 13 | There is an option to run the bot only for one of the platforms (Telegram or WhatsApp) or for both of them. 14 | 15 | ### Prerequisites 16 | 17 | 1. OpenAI API key. You can get it by signing up on the [Open AI website](https://platform.openai.com/signup). 18 | 2. Telegram bot token. You can create a new bot using [@BotFather](https://t.me/botfather). If you plan to run the bot only for WhatsApp, 19 | you can omit this step. 20 | 3. WhatsApp API key and phone number id. You can get it by creating WhatsApp app 21 | on [Meta for Developers](https://developers.facebook.com/apps) platform. If you plan to run the bot only for Telegram, you can omit this 22 | step. 23 | 4. Two random string. First one to use as a webhook secret for Telegram, and the second one for verify token for WhatsApp. 24 | 25 | There are several ways to run the bot locally: 26 |
27 | Local run with ngrok endpoint 28 | 29 | 1. Clone the repository: 30 | 31 | ```bash 32 | git clone https://github.com/vlad324/listen4me-bot 33 | ``` 34 | 35 | 2. Install the necessary dependencies: 36 | 37 | ```bash 38 | pip install -r requirements.txt 39 | ``` 40 | 41 | 3. Create a `.env` file in the root of the project and specify the following environment variables in it: 42 | 43 | ``` 44 | OPEN_AI_API_KEY=your_open_ai_api_key 45 | 46 | TELEGRAM_BOT_TOKEN=your_telegram_bot_token 47 | TELEGRAM_WEBHOOK_SECRET=first_random_string_generated_in_prerequisites_section 48 | 49 | WA_API_KEY=api_key_of_your_whatsapp_app 50 | WA_PHONE_NUMBER_ID=phone_number_id_of_your_whatsapp_app 51 | WA_VERIFY_TOKEN=second_random_string_generated_in_prerequisites_section 52 | ``` 53 | 54 | If you plan to run the bot only for Telegram or WhatsApp, you can omit the corresponding variables. `OPEN_AI_API_KEY` is required for both 55 | platforms. 56 | 57 | 4. Run the bot: 58 | 59 | ```bash 60 | fastapi run app/main.py --port 8000 61 | ``` 62 | 63 | The bot will be waiting for webhooks at `http://localhost:8000/telegram/l4me_bot/webhook` for Telegram 64 | and `http://localhost:8000/whatsapp/l4me_bot/webhook` for WhatsApp. 65 | 66 | 5. Using `ngrok`, expose your local server to the internet: 67 | 68 | ```bash 69 | ngrok http 8000 70 | ``` 71 | 72 | 6. Set the webhook for Telegram the bot: 73 | 74 | ```bash 75 | curl -X POST --location "https://api.telegram.org/bot/setWebhook" \ 76 | -H "Content-Type: application/json" \ 77 | -d '{ 78 | "url": "/telegram/l4me_bot/webhook", 79 | "secret_token": "" 80 | }' 81 | ``` 82 | 83 | 7. Use Meta for Developers platform to set the webhook for WhatsApp bot. URL should be `/whatsapp/l4me_bot/webhook` 84 | and verify token should be ``. 85 | 86 |
87 | 88 |
89 | Local run with Docker and ngrok endpoint 90 | 91 | 1. Clone the repository: 92 | 93 | ```bash 94 | git clone https://github.com/vlad324/listen4me-bot 95 | ``` 96 | 97 | 2. Build the Docker image: 98 | 99 | ```bash 100 | docker build -t listen4me-bot . 101 | ``` 102 | 103 | 3. Create a `.env` file in the root of the project and specify the following environment variables in it: 104 | 105 | ``` 106 | OPEN_AI_API_KEY=your_open_ai_api_key 107 | 108 | TELEGRAM_BOT_TOKEN=your_telegram_bot_token 109 | TELEGRAM_WEBHOOK_SECRET=first_random_string_generated_in_prerequisites_section 110 | 111 | WA_API_KEY=api_key_of_your_whatsapp_app 112 | WA_PHONE_NUMBER_ID=phone_number_id_of_your_whatsapp_app 113 | WA_VERIFY_TOKEN=second_random_string_generated_in_prerequisites_section 114 | ``` 115 | 116 | If you plan to run the bot only for Telegram or WhatsApp, you can omit the corresponding variables. `OPEN_AI_API_KEY` is required for both 117 | platforms. 118 | 119 | 4. Run the Docker container: 120 | 121 | ```bash 122 | docker run --env-file .env -p 8080:8080 listen4me-bot 123 | ``` 124 | 125 | 5. Using `ngrok`, expose your local server to the internet: 126 | 127 | ```bash 128 | ngrok http 8080 129 | ``` 130 | 131 | 6. Set the webhook for Telegram the bot: 132 | 133 | ```bash 134 | curl -X POST --location "https://api.telegram.org/bot/setWebhook" \ 135 | -H "Content-Type: application/json" \ 136 | -d '{ 137 | "url": "/telegram/l4me_bot/webhook", 138 | "secret_token": "" 139 | }' 140 | ``` 141 | 142 | 7. Use Meta for Developers platform to set the webhook for WhatsApp bot. URL should be `/whatsapp/l4me_bot/webhook` 143 | and verify token should be ``. 144 | 145 |
146 | 147 | ### Deployment to [fly.io](https://fly.io) 148 | 149 | The bot can be deployed to the [fly.io](https://fly.io) platform. The platform provides a free tier for small applications and could be a 150 | good option to host your own instance of the bot. 151 | 152 | 1. Install the `flyctl` CLI tool by following the instructions on 153 | the [official website](https://fly.io/docs/getting-started/installing-flyctl/). 154 | 2. Sign in or sing up to the [fly.io](https://fly.io/docs/hands-on/sign-up-sign-in/) platform. 155 | 3. Launch a new app: 156 | 157 | ```bash 158 | flyctl launch 159 | ``` 160 | 161 | 4. Set the necessary secrets for the bot: 162 | 163 | ```bash 164 | flyctl secrets set OPEN_AI_API_KEY= 165 | ``` 166 | 167 | Telegram related secrets: 168 | 169 | ```bash 170 | flyctl secrets set TELEGRAM_BOT_TOKEN= \ 171 | TELEGRAM_WEBHOOK_SECRET= 172 | ``` 173 | 174 | WhatsApp related secrets: 175 | 176 | ```bash 177 | flyctl secrets set WA_API_KEY= \ 178 | WA_PHONE_NUMBER_ID= \ 179 | WA_VERIFY_TOKEN= 180 | ``` 181 | 182 | If you plan to run the bot only for Telegram or WhatsApp, you can omit the corresponding secrets. `OPEN_AI_API_KEY` is required for both 183 | 184 | 5. Deploy the bot to the platform: 185 | 186 | ```bash 187 | fly deploy 188 | ``` 189 | 190 | 6. Set the new webhook URL for Telegram bot: 191 | 192 | ```bash 193 | curl -X POST --location "https://api.telegram.org/bot/setWebhook" \ 194 | -H "Content-Type: application/json" \ 195 | -d '{ 196 | "url": "/telegram/l4me_bot/webhook", 197 | "secret_token": "" 198 | }' 199 | ``` 200 | 201 | 7. Use Meta for Developers platform to set the webhook for WhatsApp bot. URL should be `/whatsapp/l4me_bot/webhook` 202 | and verify token should be ``. 203 | 204 | ## Restricting access to your Telegram bot 205 | 206 | To limit access to your version of the Telegram bot, you'll need to know your Telegram id or the Telegram ids of users you want to grant 207 | access to. 208 | Here's how to obtain this information: 209 | 210 | 1. Start a conversation with [@GetIDs Bot](https://t.me/getidsbot) on Telegram. 211 | 2. The bot will send you a message containing your Telegram id. Copy this id. 212 | 213 | Once you have the necessary ids, follow these steps to restrict access: 214 | 215 | ### For local deployment: 216 | 217 | Add the following line to your `.env` file: 218 | 219 | ``` 220 | TELEGRAM_ALLOWED_USER_IDS= 221 | ``` 222 | 223 | Replace `` with a single Telegram ID or a comma-separated list of allowed Telegram IDs. For example: 224 | 225 | - For a single user: `TELEGRAM_ALLOWED_USER_IDS=123456789` 226 | - For multiple users: `TELEGRAM_ALLOWED_USER_IDS=123456789,987654321` 227 | 228 | ### For fly.io deployment: 229 | 230 | Execute the following command: 231 | 232 | ```bash 233 | flyctl secrets set TELEGRAM_ALLOWED_USER_IDS= 234 | ``` 235 | 236 | Replace `` with the comma-separated list of Telegram IDs for users you want to allow access to your bot. 237 | 238 | By setting this environment variable, you'll ensure that only specified users can interact with your Telegram bot. --------------------------------------------------------------------------------