├── 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.
--------------------------------------------------------------------------------