├── .gitignore
├── Dockerfile
├── README.md
├── docker-compose.yml
├── languages
├── de_DE.json
├── en_US.json
├── es_ES.json
├── fr_FR.json
├── it_IT.json
└── ru_RU.json
├── requirements.txt
└── src
├── languagefixer.py
├── run.py
└── texttospeech
├── __init__.py
├── audio
├── __init__.py
└── tts.py
├── bot
├── __init__.py
├── bot.py
├── cfilters.py
├── handlers.py
├── inline
│ ├── __init__.py
│ ├── inline_query_result_voice.py
│ └── inline_query_result_voice_cached.py
└── plugin
│ ├── __init__.py
│ ├── broadcast.py
│ ├── callback
│ ├── __init__.py
│ ├── about_me.py
│ ├── always_speak.py
│ ├── slow_mode.py
│ └── stats.py
│ ├── group
│ ├── __init__.py
│ ├── migration.py
│ ├── new_members.py
│ ├── redirects.py
│ └── welcome.py
│ ├── inline
│ ├── __init__.py
│ └── inline.py
│ ├── language.py
│ ├── main_menu.py
│ ├── prehandler.py
│ ├── settings.py
│ └── speak.py
├── db
├── __init__.py
└── models.py
├── localization
├── __init__.py
└── languages.py
├── util
├── __init__.py
├── antiflood.py
├── config.sample.py
├── emojifier.py
├── files.py
└── formatting.py
└── web
├── __init__.py
└── web.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /venv/
2 | /.idea/
3 | src/texttospeech/util/config.py
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9-buster
2 |
3 | COPY requirements.txt .
4 |
5 | RUN apt-get update -y
6 | RUN apt-get install -y opus-tools libopus0 ffmpeg lame flac vorbis-tools gcc g++ cmake python3 musl postgresql libxml2-dev libxslt-dev
7 |
8 | RUN pip install -U -r requirements.txt
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TextToSpeech 🔊
2 | This bot allows you to turn any text into an audio directly on Telegram.
3 | It works in private chats, in groups, and even inline!
4 |
5 | It is available on Telegram at https://t.me/TTSBot
6 |
7 |
8 | ### Does the bot save my audios?
9 | The bot only stores audio files temporarily and deletes them immediately after they've been sent to Telegram.
10 | The bot never saves any of your audios or their text in its database, and never will.
11 | The only information that is stored in the DB is for statistics purposes only.
12 |
13 | ### Self Hosting
14 | If you decide to host the bot yourself, you will need:
15 | - Basic understanding of docker and docker-compose
16 | - Basic understanding of python
17 | - Basic understanding of postgresql
18 | - An API Key and API Hash (https://my.telegram.org)
19 | - A Telegram Bot API Token (https://t.me/BotFather)
20 |
21 | Once you have met all the requirements, you can simply edit the docker-compose.yml file and the config.py file according to your needs, and start up the bot by using `docker compose up -d`.
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | volumes:
4 | db_data: { }
5 |
6 | networks:
7 | nginx_net:
8 | external:
9 | name: nginx_net
10 | internal: { }
11 |
12 | services:
13 | bot:
14 | image: fumaz/tts-bot
15 | container_name: "tts_bot"
16 | build: .
17 | working_dir: /usr/src/app
18 | volumes:
19 | - ./src:/usr/src/app
20 | - ./languages:/usr/src/app/languages
21 | - ./audios:/usr/src/app/audios
22 | command: python3 run.py
23 | networks:
24 | - internal
25 | - nginx_net
26 | depends_on:
27 | - postgres
28 | postgres:
29 | image: postgres
30 | container_name: "tts_db"
31 | environment:
32 | POSTGRES_DB: 'tts'
33 | POSTGRES_HOST_AUTH_METHOD: 'trust'
34 | volumes:
35 | - db_data:/var/lib/postgresql/data
36 | networks:
37 | - internal
38 | restart: unless-stopped
--------------------------------------------------------------------------------
/languages/de_DE.json:
--------------------------------------------------------------------------------
1 | {
2 | "full_name": "Deutsche",
3 | "flag": ":FLAG_GERMANY:",
4 | "language_selection": [
5 | "Sprache {flag}\n",
6 | "\n",
7 | "Bitte w\u00e4hlen Sie Ihre Sprache.
\n"
8 | ],
9 | "language_set": "{flag} Sprache erfolgreich eingestellt.",
10 | "toggle_always_speak": "Sprechen Sie immer: {status}",
11 | "created_with": ":SPEAKER_HIGH_VOLUME: Hergestellt mit @TTSBot.",
12 | "awaiting_text": ":THOUGHT_BALLOON: Sendet den zu konvertierenden Text.
",
13 | "anti_flood": ":HOURGLASS_NOT_DONE: Warten Sie mal
5 sekunden vor Ihrem n\u00e4chsten audio.
",
14 | "creating_audio": ":THOUGHT_BALLOON: Audio erstellen ... Bitte warten.",
15 | "empty_text": ":THOUGHT_BALLOON: Bitte geben Sie Ihren Text ein...",
16 | "inline_create_audio": ":SPEAKER_HIGH_VOLUME: Audio erstellen!",
17 | "main_menu": [
18 | "Text to speech :SPEAKER_HIGH_VOLUME:{image}\n",
19 | "\n",
20 | "Willkommen im Bot!\n",
21 | "Mit diesem Bot k\u00f6nnen Sie erstellen\n",
22 | "audios aus jeder SMS!\n",
23 | "\n",
24 | ":INFORMATION: Verwenden Sie die Schaltfl\u00e4chen unten.\n"
25 | ],
26 | "inline_language": "{flag} Sprache \u00e4ndern...",
27 | "about_me": [
28 | "\u00dcber mich :PACKAGE:\n",
29 | "\n",
30 | "Bot entwickelt in Python
von Fumaz.\n"
31 | ],
32 | "language_button": "Sprache {flag}",
33 | "info_button": "Info :PACKAGE:",
34 | "back_button": "Back :BACK_ARROW:",
35 | "menu_button": "Zuruck :HOUSES:",
36 | "always_speak_button": "Sprich immer ",
37 | "speak_button": "Sprich :SPEAKER_HIGH_VOLUME:",
38 | "speak_again_button": "Sprich nochmal :SPEAKER_HIGH_VOLUME:",
39 | "slow_mode_button": "Slow Voice {status}",
40 | "share_button": "Share :LINK:",
41 | "refresh_button": "Refresh :COUNTERCLOCKWISE_ARROWS_BUTTON:",
42 | "settings_button": "Settings :GEAR:",
43 | "referral_button": "Referral :BUSTS_IN_SILHOUETTE:",
44 | "inline_button": "Inline :LINK:",
45 | "settings_message": [
46 | "Settings :GEAR:\n",
47 | "\n",
48 | "\u00bb Always Speak: {always_speak}\n",
49 | "\u00bb Slow Voice: {slow_mode}\n",
50 | ":RAINBOW_FLAG: Language: {language}",
51 | "\n",
52 | ":INFORMATION: More settings coming soon...\n"
53 | ],
54 | "toggle_slow_mode": "Slow Voice: {status}",
55 | "character_limit_message": ":CROSS_MARK: That message is too long.",
56 | "character_limit_inline": ":CROSS_MARK: That message is too long.",
57 | "group_speak_no_arguments": ":CROSS_MARK: Usage: /speak (text)",
58 | "group_self_was_added": [
59 | "Text To Speech :SPEAKER_HIGH_VOLUME:\n",
60 | "\n",
61 | "Thanks for adding me to the group!\n",
62 | "You can use /speak (text)
to create an audio.\n",
63 | "\n",
64 | ":INFORMATION: Settings aren't yet available in groups.\n"
65 | ],
66 | "group_redirect_message": ":UNLOCKED: To use that command...
",
67 | "group_redirect_button": "Click here!",
68 | "add_to_group_button": "Add me to a group! :BUSTS_IN_SILHOUETTE:",
69 | "stats_button": "Stats \ud83d\udcc8",
70 | "stats_message": [
71 | "Stats \ud83d\udcc8\n",
72 | "\n",
73 | ":BUSTS_IN_SILHOUETTE: Users \u00bb {users}
(+{users_today})\n",
74 | ":BALLOON: Active Users Today \u00bb {active_users}
\n",
75 | ":THOUGHT_BALLOON: Groups \u00bb {groups}
(+{groups_today})\n",
76 | ":SPEAKER_HIGH_VOLUME: Audios \u00bb {audios}
(+{audios_today})"
77 | ]
78 | }
--------------------------------------------------------------------------------
/languages/en_US.json:
--------------------------------------------------------------------------------
1 | {
2 | "full_name": "English",
3 | "flag": ":FLAG_UNITED_STATES:",
4 | "language_selection": [
5 | "Language {flag}\n",
6 | "\n",
7 | "Please select your language.
\n"
8 | ],
9 | "language_set": "{flag} Language set successfully.",
10 | "toggle_always_speak": "Always Speak: {status}",
11 | "created_with": ":SPEAKER_HIGH_VOLUME: Created with @TTSBot.",
12 | "awaiting_text": ":THOUGHT_BALLOON: Send the text you want to convert.
",
13 | "anti_flood": ":HOURGLASS_NOT_DONE: Please wait
5 seconds before your next audio.
",
14 | "creating_audio": ":THOUGHT_BALLOON: Creating audio... Please wait.",
15 | "empty_text": ":THOUGHT_BALLOON: Please type your text...",
16 | "inline_create_audio": ":SPEAKER_HIGH_VOLUME: Create audio!",
17 | "main_menu": [
18 | "Text To Speech :SPEAKER_HIGH_VOLUME:{image}\n",
19 | "\n",
20 | "Welcome to the bot!\n",
21 | "With this bot you can create\n",
22 | "audios from any text message!\n",
23 | "\n",
24 | ":INFORMATION: Use the buttons below.\n"
25 | ],
26 | "inline_language": "{flag} Change language...",
27 | "about_me": [
28 | "About me :PACKAGE:\n",
29 | "\n",
30 | "Bot developed in Python
by Fumaz.\n",
31 | "Source code available on GitHub."
32 | ],
33 | "language_button": "Language {flag}",
34 | "info_button": "Info :PACKAGE:",
35 | "back_button": "Back :BACK_ARROW:",
36 | "menu_button": "Menu :HOUSES:",
37 | "always_speak_button": "Always Speak {status}",
38 | "slow_mode_button": "Slow Voice {status}",
39 | "speak_button": "Speak :SPEAKER_HIGH_VOLUME:",
40 | "speak_again_button": "Speak Again :SPEAKER_HIGH_VOLUME:",
41 | "share_button": "Share :LINK:",
42 | "refresh_button": "Refresh :COUNTERCLOCKWISE_ARROWS_BUTTON:",
43 | "settings_button": "Settings :GEAR:",
44 | "referral_button": "Referral :BUSTS_IN_SILHOUETTE:",
45 | "inline_button": "Inline :LINK:",
46 | "settings_message": [
47 | "Settings :GEAR:\n",
48 | "\n",
49 | "» Always Speak: {always_speak}\n",
50 | "» Slow Voice: {slow_mode}\n",
51 | "» Language: {flag}\n",
52 | "\n",
53 | ":INFORMATION: More settings coming soon...\n"
54 | ],
55 | "toggle_slow_mode": "Slow Voice: {status}",
56 | "character_limit_message": ":CROSS_MARK: That message is too long.",
57 | "character_limit_inline": ":CROSS_MARK: That message is too long.",
58 | "group_speak_no_arguments": ":CROSS_MARK: Usage: /speak (text)",
59 | "group_self_was_added": [
60 | "Text To Speech :SPEAKER_HIGH_VOLUME:\n",
61 | "\n",
62 | "Thanks for adding me to the group!\n",
63 | "You can use /speak (text)
to create an audio.\n",
64 | "\n",
65 | ":INFORMATION: Settings aren't yet available in groups.\n"
66 | ],
67 | "group_redirect_message": ":UNLOCKED: To use that command...
",
68 | "group_redirect_button": "Click here!",
69 | "add_to_group_button": "Add me to a group! :BUSTS_IN_SILHOUETTE:",
70 | "stats_button": "Stats \ud83d\udcc8",
71 | "stats_message": [
72 | "Stats \ud83d\udcc8\n",
73 | "\n",
74 | ":BUSTS_IN_SILHOUETTE: Users \u00bb {users}
(+{users_today})\n",
75 | ":BALLOON: Active Users Today \u00bb {active_users}
\n",
76 | ":THOUGHT_BALLOON: Groups \u00bb {groups}
(+{groups_today})\n",
77 | ":SPEAKER_HIGH_VOLUME: Audios \u00bb {audios}
(+{audios_today})\n"
78 | ]
79 | }
--------------------------------------------------------------------------------
/languages/es_ES.json:
--------------------------------------------------------------------------------
1 | {
2 | "full_name": "Espanol",
3 | "flag": ":FLAG_SPAIN:",
4 | "language_selection": [
5 | "Idioma {flag}\n",
6 | "\n",
7 | "Seleccione su idioma.
\n"
8 | ],
9 | "language_set": "{flag} Idioma configurado correctamente.",
10 | "toggle_always_speak": "Habla siempre: {status}",
11 | "created_with": ":SPEAKER_HIGH_VOLUME: Creado con @TTSBot.",
12 | "awaiting_text": ":THOUGHT_BALLOON: Env\u00ede el texto que desea convertir.
",
13 | "anti_flood": ":HOURGLASS_NOT_DONE: Por favor espera
5 secondos antes de tu pr\u00f3ximo audio.
",
14 | "creating_audio": ":THOUGHT_BALLOON: Creando audio ... Espere.",
15 | "empty_text": ":THOUGHT_BALLOON: Por favor escriba su texto...",
16 | "inline_create_audio": ":SPEAKER_HIGH_VOLUME: Crear audio!",
17 | "main_menu": [
18 | "Text To Speech :SPEAKER_HIGH_VOLUME:{image}\n",
19 | "\n",
20 | "Bienvenido al bot!\n",
21 | "Con este bot puedes crear\n",
22 | "audios de cualquier mensaje de texto!\n",
23 | "\n",
24 | ":INFORMATION: Utilice los botones de abajo.\n"
25 | ],
26 | "inline_language": "{flag} Cambiar idioma...",
27 | "about_me": [
28 | "About me :PACKAGE:\n",
29 | "\n",
30 | "Bot desarrollado en Python
por Fumaz.\n"
31 | ],
32 | "language_button": "Idioma {flag}",
33 | "info_button": "Info :PACKAGE:",
34 | "back_button": "Atras :BACK_ARROW:",
35 | "menu_button": "Men\u00f9 :HOUSES:",
36 | "always_speak_button": "Habla siempre ",
37 | "speak_button": "Habla :SPEAKER_HIGH_VOLUME:",
38 | "speak_again_button": "Habla de nuevo :SPEAKER_HIGH_VOLUME:",
39 | "slow_mode_button": "Slow Voice {status}",
40 | "share_button": "Share :LINK:",
41 | "refresh_button": "Refresh :COUNTERCLOCKWISE_ARROWS_BUTTON:",
42 | "settings_button": "Settings :GEAR:",
43 | "referral_button": "Referral :BUSTS_IN_SILHOUETTE:",
44 | "inline_button": "Inline :LINK:",
45 | "settings_message": [
46 | "Settings :GEAR:\n",
47 | "\n",
48 | "\u00bb Always Speak: {always_speak}\n",
49 | "\u00bb Slow Voice: {slow_mode}\n",
50 | ":RAINBOW_FLAG: Language: {language}",
51 | "\n",
52 | ":INFORMATION: More settings coming soon...\n"
53 | ],
54 | "toggle_slow_mode": "Slow Voice: {status}",
55 | "character_limit_message": ":CROSS_MARK: That message is too long.",
56 | "character_limit_inline": ":CROSS_MARK: That message is too long.",
57 | "group_speak_no_arguments": ":CROSS_MARK: Usage: /speak (text)",
58 | "group_self_was_added": [
59 | "Text To Speech :SPEAKER_HIGH_VOLUME:\n",
60 | "\n",
61 | "Thanks for adding me to the group!\n",
62 | "You can use /speak (text)
to create an audio.\n",
63 | "\n",
64 | ":INFORMATION: Settings aren't yet available in groups.\n"
65 | ],
66 | "group_redirect_message": ":UNLOCKED: To use that command...
",
67 | "group_redirect_button": "Click here!",
68 | "add_to_group_button": "Add me to a group! :BUSTS_IN_SILHOUETTE:",
69 | "stats_button": "Stats \ud83d\udcc8",
70 | "stats_message": [
71 | "Stats \ud83d\udcc8\n",
72 | "\n",
73 | ":BUSTS_IN_SILHOUETTE: Users \u00bb {users}
(+{users_today})\n",
74 | ":BALLOON: Active Users Today \u00bb {active_users}
\n",
75 | ":THOUGHT_BALLOON: Groups \u00bb {groups}
(+{groups_today})\n",
76 | ":SPEAKER_HIGH_VOLUME: Audios \u00bb {audios}
(+{audios_today})\n"
77 | ]
78 | }
--------------------------------------------------------------------------------
/languages/fr_FR.json:
--------------------------------------------------------------------------------
1 | {
2 | "full_name": "Fran\u00e7ais",
3 | "flag": ":FLAG_FRANCE:",
4 | "language_selection": [
5 | "Langue {flag}\n",
6 | "\n",
7 | "Veuillez s\u00e9lectionner votre langue.
\n"
8 | ],
9 | "language_set": "{flag} Langue d\u00e9finie avec succ\u00e8s.",
10 | "toggle_always_speak": "Parlez toujours: {status}",
11 | "created_with": ":SPEAKER_HIGH_VOLUME: Cr\u00e9\u00e9 avec @TTSBot.",
12 | "awaiting_text": ":THOUGHT_BALLOON: Envoyez le texte que vous souhaitez convertir.
",
13 | "anti_flood": ":HOURGLASS_NOT_DONE: Veuillez patienter
5 secondes avant votre prochain audio.
",
14 | "creating_audio": ":THOUGHT_BALLOON: Cr\u00e9ation audio... Veuillez patienter.",
15 | "empty_text": ":THOUGHT_BALLOON: Veuillez saisir votre texte...",
16 | "inline_create_audio": ":SPEAKER_HIGH_VOLUME: Cr\u00e9ez de l'audio!",
17 | "main_menu": [
18 | "Text To Speech :SPEAKER_HIGH_VOLUME:{image}\n",
19 | "\n",
20 | "Bienvenue dans le bot!\n",
21 | "Avec ce bot, vous pouvez cr\u00e9er\n",
22 | "audios \u00e0 partir de n'importe quel message texte!\n",
23 | "\n",
24 | ":INFORMATION: Utilisez les boutons ci-dessous.\n"
25 | ],
26 | "inline_language": "{flag} Changer de langue...",
27 | "about_me": [
28 | "\u00c0 propos de moi :PACKAGE:\n",
29 | "\n",
30 | "Bot d\u00e9velopp\u00e9 en Python
par Fumaz.\n"
31 | ],
32 | "language_button": "Langue {flag}",
33 | "info_button": "Info :PACKAGE:",
34 | "back_button": "Retour :BACK_ARROW:",
35 | "menu_button": "Menu :HOUSES:",
36 | "always_speak_button": "Toujours parler ",
37 | "speak_button": "Parler :SPEAKER_HIGH_VOLUME:",
38 | "speak_again_button": "Parlez \u00e0 nouveau :SPEAKER_HIGH_VOLUME:",
39 | "slow_mode_button": "Slow Voice {status}",
40 | "share_button": "Share :LINK:",
41 | "refresh_button": "Refresh :COUNTERCLOCKWISE_ARROWS_BUTTON:",
42 | "settings_button": "Settings :GEAR:",
43 | "referral_button": "Referral :BUSTS_IN_SILHOUETTE:",
44 | "inline_button": "Inline :LINK:",
45 | "settings_message": [
46 | "Settings :GEAR:\n",
47 | "\n",
48 | "\u00bb Always Speak: {always_speak}\n",
49 | "\u00bb Slow Voice: {slow_mode}\n",
50 | ":RAINBOW_FLAG: Language: {language}",
51 | "\n",
52 | ":INFORMATION: More settings coming soon...\n"
53 | ],
54 | "toggle_slow_mode": "Slow Voice: {status}",
55 | "character_limit_message": ":CROSS_MARK: That message is too long.",
56 | "character_limit_inline": ":CROSS_MARK: That message is too long.",
57 | "group_speak_no_arguments": ":CROSS_MARK: Usage: /speak (text)",
58 | "group_self_was_added": [
59 | "Text To Speech :SPEAKER_HIGH_VOLUME:\n",
60 | "\n",
61 | "Thanks for adding me to the group!\n",
62 | "You can use /speak (text)
to create an audio.\n",
63 | "\n",
64 | ":INFORMATION: Settings aren't yet available in groups.\n"
65 | ],
66 | "group_redirect_message": ":UNLOCKED: To use that command...
",
67 | "group_redirect_button": "Click here!",
68 | "add_to_group_button": "Add me to a group! :BUSTS_IN_SILHOUETTE:",
69 | "stats_button": "Stats \ud83d\udcc8",
70 | "stats_message": [
71 | "Stats \ud83d\udcc8\n",
72 | "\n",
73 | ":BUSTS_IN_SILHOUETTE: Users \u00bb {users}
(+{users_today})\n",
74 | ":BALLOON: Active Users Today \u00bb {active_users}
\n",
75 | ":THOUGHT_BALLOON: Groups \u00bb {groups}
(+{groups_today})\n",
76 | ":SPEAKER_HIGH_VOLUME: Audios \u00bb {audios}
(+{audios_today})\n"
77 | ]
78 | }
--------------------------------------------------------------------------------
/languages/it_IT.json:
--------------------------------------------------------------------------------
1 | {
2 | "full_name": "Italiano",
3 | "flag": ":FLAG_ITALY:",
4 | "language_selection": [
5 | "Lingua {flag}\n",
6 | "\n",
7 | "Seleziona la tua lingua.
\n"
8 | ],
9 | "language_set": "{flag} Lingua impostata correttamente.",
10 | "toggle_always_speak": "Parla sempre: {status}",
11 | "created_with": ":SPEAKER_HIGH_VOLUME: Creato con @TTSBot.",
12 | "awaiting_text": ":THOUGHT_BALLOON: Ora invia il testo.
",
13 | "anti_flood": ":HOURGLASS_NOT_DONE: Devi aspettare
5 secondi tra un audio e l'altro.
",
14 | "creating_audio": ":THOUGHT_BALLOON: Creazione audio in corso...",
15 | "empty_text": ":THOUGHT_BALLOON: Inserisci un testo valido.",
16 | "inline_create_audio": ":SPEAKER_HIGH_VOLUME: Crea audio!",
17 | "main_menu": [
18 | "Text To Speech :SPEAKER_HIGH_VOLUME:{image}\n",
19 | "\n",
20 | "Benvenuto nel bot!\n",
21 | "Con questo bot puoi creare\n",
22 | "audio da qualsiasi messaggio!\n",
23 | "\n",
24 | ":INFORMATION: Usa i pulsanti qui sotto.\n"
25 | ],
26 | "inline_language": "{flag} Cambia lingua...",
27 | "about_me": [
28 | "Info :PACKAGE:\n",
29 | "\n",
30 | "Bot sviluppato in Python
da Fumaz.\n",
31 | "Sorgente disponibile su GitHub."
32 | ],
33 | "language_button": "Lingua {flag}",
34 | "back_button": "Indietro :BACK_ARROW:",
35 | "menu_button": "Men\u00f9 :HOUSES:",
36 | "always_speak_button": "Parla sempre {status}",
37 | "speak_button": "Parla :SPEAKER_HIGH_VOLUME:",
38 | "speak_again_button": "Parla di nuovo :SPEAKER_HIGH_VOLUME:",
39 | "settings_message": [
40 | "Impostazioni :GEAR:\n",
41 | "\n",
42 | "» Parla Sempre: {always_speak}\n",
43 | "» Voce Lenta: {slow_mode}\n",
44 | "» Lingua: {flag}\n",
45 | "\n",
46 | ":INFORMATION: Altre impostazioni in arrivo...\n"
47 | ],
48 | "toggle_slow_mode": "Voce Lenta: {status}",
49 | "character_limit_message": ":CROSS_MARK: Messaggio troppo lungo.",
50 | "character_limit_inline": ":CROSS_MARK: Messaggio troppo lungo.",
51 | "slow_mode_button": "Voce Lenta {status}",
52 | "share_button": "Condividi :LINK:",
53 | "refresh_button": "Aggiorna :COUNTERCLOCKWISE_ARROWS_BUTTON:",
54 | "settings_button": "Impostazioni :GEAR:",
55 | "group_speak_no_arguments": ":CROSS_MARK: Utilizzo: /speak (testo)",
56 | "group_self_was_added": [
57 | "Text To Speech :SPEAKER_HIGH_VOLUME:\n",
58 | "\n",
59 | "Grazie per avermi aggiunto al gruppo!\n",
60 | "Puoi usare /speak (testo)
per creare un audio.\n",
61 | "\n",
62 | ":INFORMATION: Le impostazioni non sono ancora disponibili nei gruppi.\n"
63 | ],
64 | "group_redirect_message": ":UNLOCKED: Per usare quel comando...
",
65 | "group_redirect_button": "Clicca qui!",
66 | "add_to_group_button": "Aggiungimi a un gruppo! :BUSTS_IN_SILHOUETTE:",
67 | "stats_button": "Statistiche \ud83d\udcc8",
68 | "info_button": "Info :PACKAGE:",
69 | "referral_button": "Referral :BUSTS_IN_SILHOUETTE:",
70 | "inline_button": "Inline :LINK:",
71 | "stats_message": [
72 | "Stats \ud83d\udcc8\n",
73 | "\n",
74 | ":BUSTS_IN_SILHOUETTE: Utenti \u00bb {users}
(+{users_today})\n",
75 | ":BALLOON: Utenti attivi oggi \u00bb {active_users}
\n",
76 | ":THOUGHT_BALLOON: Gruppi \u00bb {groups}
(+{groups_today})\n",
77 | ":SPEAKER_HIGH_VOLUME: Audio \u00bb {audios}
(+{audios_today})\n"
78 | ]
79 | }
--------------------------------------------------------------------------------
/languages/ru_RU.json:
--------------------------------------------------------------------------------
1 | {
2 | "full_name": "\u0440\u0443\u0441\u0441\u043a\u0438\u0439",
3 | "flag": ":FLAG_RUSSIA:",
4 | "language_selection": [
5 | "\u042f\u0437\u044b\u043a {flag}\n",
6 | "\n",
7 | "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u044f\u0437\u044b\u043a.
\n"
8 | ],
9 | "language_set": "{flag} \u042f\u0437\u044b\u043a \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u0443\u0441\u043f\u0435\u0448\u043d\u043e.",
10 | "toggle_always_speak": "\u0412\u0441\u0435\u0433\u0434\u0430 \u0433\u043e\u0432\u043e\u0440\u0438\u0442\u044c: {status}",
11 | "created_with": ":SPEAKER_HIGH_VOLUME: \u0421\u043e\u0437\u0434\u0430\u043d\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e @TTSBot.",
12 | "awaiting_text": ":THOUGHT_BALLOON: \u041e\u0442\u043f\u0440\u0430\u0432\u044c\u0442\u0435 \u0442\u0435\u043a\u0441\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c.
",
13 | "anti_flood": ":HOURGLASS_NOT_DONE: \u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435
5 \u0441\u0435\u043a\u0443\u043d\u0434\u044b \u043f\u0435\u0440\u0435\u0434 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u0437\u0432\u0443\u043a\u043e\u043c.
",
14 | "creating_audio": ":THOUGHT_BALLOON: \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0430\u0443\u0434\u0438\u043e... \u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435.",
15 | "empty_text": ":THOUGHT_BALLOON: \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043a\u0441\u0442...",
16 | "inline_create_audio": ":SPEAKER_HIGH_VOLUME: \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0430\u0443\u0434\u0438\u043e!",
17 | "main_menu": [
18 | "Text To Speech :SPEAKER_HIGH_VOLUME:{image}\n",
19 | "\n",
20 | "\u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 \u0431\u043e\u0442!\n",
21 | "\u0421 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0433\u043e \u0431\u043e\u0442\u0430 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c\n",
22 | "\u0430\u0443\u0434\u0438\u043e \u0438\u0437 \u043b\u044e\u0431\u043e\u0433\u043e \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f!\n",
23 | "\n",
24 | ":INFORMATION: \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0438\u0436\u0435.\n"
25 | ],
26 | "inline_language": "{flag} \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u044f\u0437\u044b\u043a...",
27 | "about_me": [
28 | "\u041e\u0431\u043e \u043c\u043d\u0435 :PACKAGE:\n",
29 | "\n",
30 | "\u0411\u043e\u0442, \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 Python
Fumaz.\n"
31 | ],
32 | "language_button": "\u042f\u0437\u044b\u043a {flag}",
33 | "info_button": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f :PACKAGE:",
34 | "back_button": "\u041d\u0430\u0437\u0430\u0434 :BACK_ARROW:",
35 | "menu_button": "\u041c\u0435\u043d\u044e :HOUSES:",
36 | "always_speak_button": "\u0412\u0441\u0435\u0433\u0434\u0430 \u0433\u043e\u0432\u043e\u0440\u0438 ",
37 | "speak_button": "\u0413\u043e\u0432\u043e\u0440\u0438 :SPEAKER_HIGH_VOLUME:",
38 | "speak_again_button": "\u0413\u043e\u0432\u043e\u0440\u0438 \u0435\u0449\u0435 \u0440\u0430\u0437 :SPEAKER_HIGH_VOLUME:",
39 | "slow_mode_button": "Slow Voice {status}",
40 | "share_button": "Share :LINK:",
41 | "refresh_button": "Refresh :COUNTERCLOCKWISE_ARROWS_BUTTON:",
42 | "settings_button": "Settings :GEAR:",
43 | "referral_button": "Referral :BUSTS_IN_SILHOUETTE:",
44 | "inline_button": "Inline :LINK:",
45 | "settings_message": [
46 | "Settings :GEAR:\n",
47 | "\n",
48 | "\u00bb Always Speak: {always_speak}\n",
49 | "\u00bb Slow Voice: {slow_mode}\n",
50 | ":RAINBOW_FLAG: Language: {language}",
51 | "\n",
52 | ":INFORMATION: More settings coming soon...\n"
53 | ],
54 | "toggle_slow_mode": "Slow Voice: {status}",
55 | "character_limit_message": ":CROSS_MARK: That message is too long.",
56 | "character_limit_inline": ":CROSS_MARK: That message is too long.",
57 | "group_speak_no_arguments": ":CROSS_MARK: Usage: /speak (text)",
58 | "group_self_was_added": [
59 | "Text To Speech :SPEAKER_HIGH_VOLUME:\n",
60 | "\n",
61 | "Thanks for adding me to the group!\n",
62 | "You can use /speak (text)
to create an audio.\n",
63 | "\n",
64 | ":INFORMATION: Settings aren't yet available in groups.\n"
65 | ],
66 | "group_redirect_message": ":UNLOCKED: To use that command...
",
67 | "group_redirect_button": "Click here!",
68 | "add_to_group_button": "Add me to a group! :BUSTS_IN_SILHOUETTE:",
69 | "stats_button": "Stats \ud83d\udcc8",
70 | "stats_message": [
71 | "Stats \ud83d\udcc8\n",
72 | "\n",
73 | ":BUSTS_IN_SILHOUETTE: Users \u00bb {users}
(+{users_today})\n",
74 | ":BALLOON: Active Users Today \u00bb {active_users}
\n",
75 | ":THOUGHT_BALLOON: Groups \u00bb {groups}
(+{groups_today})\n",
76 | ":SPEAKER_HIGH_VOLUME: Audios \u00bb {audios}
(+{audios_today})\n"
77 | ]
78 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | psycopg2
3 | pony
4 | gTTS
5 | ffmpeg-python
6 | httpx
7 | apscheduler
8 | tgcrypto
9 | pyrogram
10 | plate
11 | sentry-sdk
--------------------------------------------------------------------------------
/src/languagefixer.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | if __name__ == '__main__':
5 | languages = os.listdir('../languages/')
6 | english = json.load(open('../languages/en_US.json', 'r'))
7 |
8 | for language in languages:
9 | lang = json.load(open('../languages/' + language, 'r'))
10 |
11 | for k, v in english.items():
12 | if k not in lang:
13 | lang[k] = v
14 |
15 | json.dump(lang, open('../languages/' + language, 'w'))
16 |
--------------------------------------------------------------------------------
/src/run.py:
--------------------------------------------------------------------------------
1 | import sentry_sdk
2 |
3 | from texttospeech.audio import tts
4 | from texttospeech.bot import bot
5 | from texttospeech.db import models
6 | from texttospeech.web import web
7 | from texttospeech.util import config
8 |
9 | if __name__ == '__main__':
10 | sentry_sdk.init(
11 | config.SENTRY_URL,
12 |
13 | # Set traces_sample_rate to 1.0 to capture 100%
14 | # of transactions for performance monitoring.
15 | # We recommend adjusting this value in production.
16 | traces_sample_rate=1.0,
17 | )
18 |
19 | try:
20 | 1/0
21 | except Exception as e:
22 | sentry_sdk.capture_exception(e)
23 |
24 | models.setup()
25 | tts.setup()
26 | web.run()
27 | bot.run()
28 |
--------------------------------------------------------------------------------
/src/texttospeech/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/audio/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/audio/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/audio/tts.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from threading import Thread
4 | from time import sleep
5 |
6 | import ffmpeg
7 | from gtts import gTTS
8 | from gtts.lang import tts_langs
9 |
10 | from texttospeech.util import config, files
11 |
12 | langs = None
13 |
14 |
15 | def setup():
16 | """
17 | Fetches the available languages from google's TTS API.
18 | """
19 | global langs
20 |
21 | try:
22 | langs = tts_langs()
23 | except RuntimeError as e:
24 | logging.error('An error occured whilst fetching the langs.', exc_info=e)
25 | exit(0)
26 |
27 |
28 | def is_valid(language: str) -> bool:
29 | """
30 | Checks if a language is valid
31 |
32 | :param language: the language to check
33 | :return: true if the language is valid, false otherwise
34 | """
35 |
36 | return language in langs
37 |
38 |
39 | def create_mp3(text: str, language: str = 'en_US', slow: bool = False) -> str:
40 | """
41 | Creates an mp3 file.
42 |
43 | :param text: the text of the audio
44 | :param language: the language of the audio
45 | :param slow: if the audio should be slowed down
46 | :return: the created file's path
47 | """
48 |
49 | filename = os.path.join(config.AUDIOS_DIR, files.random_name(extension='mp3'))
50 | gtts = gTTS(text=text, lang=language[:2], slow=slow, lang_check=False)
51 |
52 | try:
53 | gtts.save(filename)
54 |
55 | return filename
56 | except RuntimeError as e:
57 | logging.error('An error occured whilst saving the audio.', exc_info=e)
58 |
59 |
60 | def convert_to_ogg(filename: str) -> str:
61 | """
62 | Converts a file to .ogg
63 |
64 | :param filename: the file to convert
65 | :return: the created file's path
66 | """
67 |
68 | output = filename[:-3] + 'ogg'
69 |
70 | try:
71 | stream = ffmpeg.input(filename).output(output, ar='48000', ac=2, acodec='libopus', ab='32k', threads=2)
72 | ffmpeg.run(stream, quiet=True)
73 |
74 | return output
75 | except RuntimeError as e:
76 | logging.error('An error occured whilst converting the file.', exc_info=e)
77 |
78 |
79 | def create_audio(text: str, language: str = 'en_US', slow: bool = False) -> str:
80 | """
81 | Creates an audio file with .ogg extension
82 |
83 | :param text: the text of the audio
84 | :param language: the language of the audio
85 | :param slow: if the audio should be slowed down
86 | :return: the created file's path
87 | """
88 |
89 | in_file = create_mp3(text=text, language=language, slow=slow)
90 | out_file = convert_to_ogg(in_file)
91 |
92 | os.remove(in_file)
93 |
94 | return out_file
95 |
96 |
97 | def create_link(text: str, language: str = 'en_US', slow: bool = False) -> str:
98 | """
99 | Creates an audio file and returns its link
100 |
101 | :param text: the text of the audio
102 | :param language: the language of the audio
103 | :param slow: if the audio should be slowed down
104 | :return: the url of the created file
105 | """
106 |
107 | filename = create_audio(text=text, language=language, slow=slow)
108 |
109 | def remove_audio():
110 | sleep(45)
111 | os.remove(filename)
112 |
113 | Thread(target=remove_audio).start()
114 |
115 | return f'{config.AUDIOS_DOMAIN}/{filename.split("/")[-1]}'
116 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/bot/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/bot/bot.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 |
4 | from pyrogram import Client
5 |
6 | from texttospeech.util import config
7 |
8 | client = Client(session_name=config.SESSION_NAME,
9 | api_id=config.API_ID,
10 | api_hash=config.API_HASH,
11 | plugins=dict(root=config.PLUGINS_DIR),
12 | bot_token=config.BOT_TOKEN,
13 | workers=16)
14 |
15 |
16 | def clear_audios():
17 | audios = os.listdir(config.AUDIOS_DIR)
18 | print(f'Clearing {len(audios)} old audios...')
19 |
20 | for audio in audios:
21 | try:
22 | os.remove(config.AUDIOS_DIR + audio)
23 | except:
24 | pass
25 |
26 |
27 | def run():
28 | clear_audios()
29 |
30 | client.start_time = datetime.now()
31 | client.run()
32 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/cfilters.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 |
3 | from pyrogram import filters
4 |
5 | from texttospeech.util import config
6 |
7 | admin = filters.create(lambda _, __, update: update.db_user.is_admin)
8 | banned = filters.create(lambda _, __, update: update.db_user.is_banned)
9 | added = filters.create(lambda _, __, update: getattr(update, 'self_was_added', False))
10 |
11 |
12 | def callback_data(data: str):
13 | async def func(_, __, callback):
14 | return callback.data == data
15 |
16 | return filters.create(func, "CallbackDataFilter")
17 |
18 |
19 | def action(action: str):
20 | async def func(_, __, update):
21 | return update.db_user and update.db_user.action == action
22 |
23 | return filters.create(func, "ActionFilter")
24 |
25 |
26 | def not_command(prefixes=None):
27 | if not prefixes:
28 | prefixes = ['/']
29 |
30 | async def func(_, __, message):
31 | for prefix in prefixes:
32 | if message.text.startswith(prefix):
33 | return False
34 |
35 | return True
36 |
37 | return filters.create(func, "NotCommandFilter")
38 |
39 |
40 | def setting(name: str):
41 | async def func(_, __, update):
42 | return bool(update.db_user) and bool(update.db_user.get_setting(name, bool))
43 |
44 | return filters.create(func, "SettingFilter")
45 |
46 |
47 | def group_setting(name: str):
48 | async def func(_, __, update):
49 | return bool(update.db_chat) and bool(update.db_chat.get_setting(name, bool))
50 |
51 | return filters.create(func, "SettingFilter")
52 |
53 |
54 | def group_command(commands: Union[str, list], prefixes: Union[str, list] = "/"):
55 | if not isinstance(commands, list):
56 | commands = [commands]
57 |
58 | for command in list(commands):
59 | commands.append(f'{command}@{config.BOT_USERNAME}')
60 |
61 | return filters.command(commands, prefixes)
62 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/handlers.py:
--------------------------------------------------------------------------------
1 | DATABASE = -5
2 | BANNED = -4
3 | NEW_CHAT_MEMBERS = -3
4 | MIGRATION = -2
5 | WELCOME = -1
6 | DEFAULT = 0
7 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/inline/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/bot/inline/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/bot/inline/inline_query_result_voice.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 |
3 | from pyrogram.parser import Parser
4 | from pyrogram.raw import types
5 | from pyrogram.types import InlineQueryResult, InlineKeyboardMarkup, InputMessageContent
6 |
7 |
8 | class InlineQueryResultVoice(InlineQueryResult):
9 | def __init__(
10 | self,
11 | voice_url: str,
12 | title: str,
13 | thumb_url: str = None,
14 | id: str = None,
15 | description: str = None,
16 | caption: str = None,
17 | parse_mode: Union[str, None] = object,
18 | reply_markup: InlineKeyboardMarkup = None,
19 | input_message_content: InputMessageContent = None
20 | ):
21 | super().__init__("voice", id, input_message_content, reply_markup)
22 |
23 | self.voice_url = voice_url
24 | self.thumb_url = thumb_url
25 | self.title = title
26 | self.description = description
27 | self.caption = caption
28 | self.parse_mode = parse_mode
29 | self.reply_markup = reply_markup
30 | self.input_message_content = input_message_content
31 |
32 | """Link to a voice file.
33 |
34 | By default, this voice file will be sent by the user with optional caption.
35 | Alternatively, you can use *input_message_content* to send a message with the specified content instead of the
36 | voice.
37 |
38 | Parameters:
39 | voice_url (``str``):
40 | A valid URL for the voice file.
41 | File size must not exceed 1 MB.
42 |
43 | thumb_url (``str``, *optional*):
44 | URL of the static thumbnail for the result (jpeg or gif)
45 | Defaults to the value passed in *voice_url*.
46 |
47 | id (``str``, *optional*):
48 | Unique identifier for this result, 1-64 bytes.
49 | Defaults to a randomly generated UUID4.
50 |
51 | title (``str``, *optional*):
52 | Title for the result.
53 |
54 | description (``str``, *optional*):
55 | Short description of the result.
56 |
57 | caption (``str``, *optional*):
58 | Caption of the photo to be sent, 0-1024 characters.
59 |
60 | parse_mode (``str``, *optional*):
61 | By default, texts are parsed using both Markdown and HTML styles.
62 | You can combine both syntaxes together.
63 | Pass "markdown" or "md" to enable Markdown-style parsing only.
64 | Pass "html" to enable HTML-style parsing only.
65 | Pass None to completely disable style parsing.
66 |
67 | reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
68 | An InlineKeyboardMarkup object.
69 |
70 | input_message_content (:obj:`InputMessageContent`):
71 | Content of the message to be sent instead of the voice file.
72 | """
73 |
74 | async def write(self, client: "pyrogram.Client"):
75 | voice = types.InputWebDocument(
76 | url=self.voice_url,
77 | size=0,
78 | mime_type="audio/ogg",
79 | attributes=[]
80 | )
81 |
82 | if self.thumb_url is None:
83 | thumb = None
84 | else:
85 | thumb = types.InputWebDocument(
86 | url=self.thumb_url,
87 | size=0,
88 | mime_type="image/jpeg",
89 | attributes=[]
90 | )
91 |
92 | return types.InputBotInlineResult(
93 | id=self.id,
94 | type=self.type,
95 | title=self.title,
96 | description=self.description,
97 | thumb=thumb,
98 | content=voice,
99 | send_message=(
100 | await self.input_message_content.write(client, self.reply_markup)
101 | if self.input_message_content
102 | else types.InputBotInlineMessageMediaAuto(
103 | reply_markup=await self.reply_markup.write(client) if self.reply_markup else None,
104 | **await(Parser(None)).parse(self.caption, self.parse_mode)
105 | )
106 | )
107 | )
108 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/inline/inline_query_result_voice_cached.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 |
3 | from pyrogram import raw
4 | from pyrogram import types
5 | from pyrogram.parser import Parser
6 | from pyrogram.types.inline_mode.inline_query_result import InlineQueryResult
7 | from pyrogram.utils import get_input_media_from_file_id
8 |
9 |
10 | class InlineQueryResultCachedDocument(InlineQueryResult):
11 | def __init__(
12 | self,
13 | title: str,
14 | file_id: str,
15 | file_ref: str = None,
16 | id: str = None,
17 | description: str = None,
18 | caption: str = "",
19 | parse_mode: Union[str, None] = object,
20 | reply_markup: "types.InlineKeyboardMarkup" = None,
21 | input_message_content: "types.InputMessageContent" = None
22 | ):
23 | super().__init__("file", id, input_message_content, reply_markup)
24 |
25 | self.file_id = file_id
26 | self.file_ref = file_ref
27 | self.title = title
28 | self.description = description
29 | self.caption = caption
30 | self.parse_mode = parse_mode
31 | self.reply_markup = reply_markup
32 | self.input_message_content = input_message_content
33 |
34 | async def write(self):
35 | document = get_input_media_from_file_id(self.file_id)
36 |
37 | return raw.types.InputBotInlineResultDocument(
38 | id=self.id,
39 | type=self.type,
40 | title=self.title,
41 | description=self.description,
42 | document=document.id,
43 | send_message=(
44 | await self.input_message_content.write(self.reply_markup)
45 | if self.input_message_content
46 | else raw.types.InputBotInlineMessageMediaAuto(
47 | reply_markup=self.reply_markup.write() if self.reply_markup else None,
48 | **await(Parser(None)).parse(self.caption, self.parse_mode)
49 | )
50 | )
51 | )
52 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/bot/plugin/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/broadcast.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from pyrogram import Client, filters
4 | from pyrogram.errors import FloodWait
5 | from pyrogram.methods.messages.send_chat_action import ChatAction
6 | from pyrogram.types import Message
7 |
8 | from texttospeech.bot import cfilters
9 | from texttospeech.db.models import *
10 |
11 |
12 | @Client.on_message(filters.command("activitycheck") & cfilters.admin)
13 | async def on_activity_check(client: Client, message: Message):
14 | with db_session:
15 | total = User.select().count()
16 |
17 | status = await message.reply_text('Checking... 0% (0/{})'.format(total))
18 | sent = 0
19 | success = 0
20 | fail = 0
21 |
22 | inactive = []
23 |
24 | with db_session:
25 | print('Opened DB Session', flush=True)
26 |
27 | for user_id in select(u.id for u in User if u.is_active):
28 | try:
29 | sent += 1
30 |
31 | await client.send_chat_action(user_id, 'typing')
32 | await asyncio.sleep(0.03)
33 | success += 1
34 |
35 | if sent % 100 == 0:
36 | await status.edit_text('Checking... {}% ({}/{}) ({}/{})'.format(int(sent / total * 100), sent, total, success, fail))
37 | except FloodWait as e:
38 | fail += 1
39 | print('FloodWait: {}'.format(e), flush=True)
40 | await asyncio.sleep(e.x)
41 | except Exception as e:
42 | fail += 1
43 | await asyncio.sleep(0.03)
44 |
45 | inactive.append(user_id)
46 |
47 | if len(inactive) >= 100:
48 | print('dbing inactive users', flush=True)
49 |
50 | with db_session:
51 | for user in inactive:
52 | User.get(id=user).is_active = False
53 |
54 | commit()
55 |
56 | inactive = []
57 |
58 | if sent % 100 == 0:
59 | await status.edit_text('Checking... {}% ({}/{}) ({}/{})'.format(int(sent / total * 100), sent, total, success, fail))
60 |
61 |
62 | @Client.on_message(filters.command("broadcast") & cfilters.admin & filters.reply)
63 | async def on_broadcast(client: Client, message: Message):
64 | with db_session:
65 | total = User.select().count()
66 |
67 | starting = int(message.command[0]) if len(message.command) > 0 else 0
68 |
69 | broadcast = message.reply_to_message
70 | status = await message.reply_text('Broadcasting... 0% (0/{})'.format(total))
71 | sent = 0
72 | success = 0
73 | fail = 0
74 |
75 | with db_session:
76 | print('Opened DB Session', flush=True)
77 | for user_id in select(u.id for u in User if u.is_active):
78 | try:
79 | sent += 1
80 |
81 | if sent < starting:
82 | continue
83 | # print('Sending to {} ({}/{}) ({}/{})'.format(user_id, sent, total, success, fail))
84 | await broadcast.forward(user_id)
85 | await asyncio.sleep(0.05)
86 | success += 1
87 |
88 | if sent % 100 == 0:
89 | await status.edit_text('Broadcasting... {}% ({}/{}) ({}/{})'.format(int(sent / total * 100), sent, total, success, fail))
90 | except FloodWait as e:
91 | fail += 1
92 | print('FloodWait: {}'.format(e), flush=True)
93 | await asyncio.sleep(e.x)
94 | except Exception as e:
95 | fail += 1
96 | # print(e, flush=True)
97 | # await asyncio.sleep(0.05)
98 |
99 | if sent % 100 == 0:
100 | await status.edit_text('Broadcasting... {}% ({}/{}) ({}/{})'.format(int(sent / total * 100), sent, total, success, fail))
101 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/callback/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/bot/plugin/callback/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/callback/about_me.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client
2 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
3 |
4 | from texttospeech.bot import cfilters
5 |
6 |
7 | def create_keyboard(user) -> InlineKeyboardMarkup:
8 | menu = user.get_message('menu_button')
9 |
10 | return InlineKeyboardMarkup([[InlineKeyboardButton(menu, callback_data='main_menu')]])
11 |
12 |
13 | @Client.on_callback_query(cfilters.callback_data("info"))
14 | async def on_about_me(_, callback):
15 | user = callback.db_user
16 |
17 | await callback.answer()
18 | await callback.edit_message_text(user.get_message("about_me"), reply_markup=create_keyboard(user),
19 | disable_web_page_preview=True)
20 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/callback/always_speak.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client
2 |
3 | from texttospeech.bot import cfilters
4 | from texttospeech.bot.plugin import settings
5 | from texttospeech.db.models import Settings
6 | from texttospeech.util.emojifier import Emoji
7 |
8 |
9 | @Client.on_callback_query(cfilters.callback_data("toggle_always_speak"))
10 | async def on_always_speak(_, callback):
11 | user = callback.db_user
12 |
13 | always_speak = Emoji.from_boolean(user.toggle_setting(Settings.ALWAYS_SPEAK))
14 | message = user.get_message('toggle_always_speak', status=always_speak)
15 |
16 | await callback.edit_message_text(settings.create_message(user), reply_markup=settings.create_keyboard(user))
17 | await callback.answer(message, show_alert=True)
18 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/callback/slow_mode.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client
2 |
3 | from texttospeech.bot import cfilters
4 | from texttospeech.bot.plugin import settings
5 | from texttospeech.db.models import Settings
6 | from texttospeech.util.emojifier import Emoji
7 |
8 |
9 | @Client.on_callback_query(cfilters.callback_data("toggle_slow_mode"))
10 | async def on_slow_mode(_, callback):
11 | user = callback.db_user
12 |
13 | slow_mode = Emoji.from_boolean(user.toggle_setting(Settings.SLOW_MODE))
14 | message = user.get_message('toggle_slow_mode', status=slow_mode)
15 |
16 | await callback.edit_message_text(settings.create_message(user), reply_markup=settings.create_keyboard(user))
17 | await callback.answer(message, show_alert=True)
18 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/callback/stats.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from pyrogram import Client
4 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
5 |
6 | from texttospeech.bot import cfilters
7 | from texttospeech.db.models import *
8 |
9 |
10 | def create_keyboard(user) -> InlineKeyboardMarkup:
11 | menu = user.get_message('menu_button')
12 |
13 | return InlineKeyboardMarkup([[InlineKeyboardButton(menu, callback_data='main_menu')]])
14 |
15 |
16 | @Client.on_callback_query(cfilters.callback_data("stats"))
17 | async def on_stats(_, callback):
18 | if not callback.db_user.is_admin:
19 | await callback.answer(text='N/A', show_alert=True)
20 | return
21 |
22 | with db_session:
23 | user = callback.db_user
24 | users = User.select().count()
25 | groups = Chat.select().count()
26 | audios = Audio.select().count()
27 |
28 | users_today = User.select(lambda u: u.creation_date >= (datetime.now() - timedelta(hours=24))).count()
29 | groups_today = Chat.select(lambda c: c.creation_date >= (datetime.now() - timedelta(hours=24))).count()
30 | audios_today = Audio.select(lambda a: a.creation_date >= (datetime.now() - timedelta(hours=24))).count()
31 | active_users = User.select(lambda u: u.last_update >= (datetime.now() - timedelta(hours=24))).count()
32 |
33 | await callback.answer()
34 | await callback.edit_message_text(user.get_message("stats_message", users=users, groups=groups, audios=audios,
35 | users_today=users_today, groups_today=groups_today,
36 | audios_today=audios_today, active_users=active_users),
37 | reply_markup=create_keyboard(user))
38 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/group/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/bot/plugin/group/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/group/migration.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client, filters
2 |
3 | from texttospeech.db.models import *
4 | from ... import handlers
5 |
6 |
7 | @Client.on_message(filters.migrate_from_chat_id, group=handlers.MIGRATION)
8 | async def on_migrate_from_chat_id(_, message):
9 | message.db_chat.set_setting(Settings.WELCOME_SENT, True)
10 |
11 |
12 | @Client.on_message(filters.migrate_to_chat_id, group=handlers.MIGRATION)
13 | async def on_migrate_to_chat_id(_, message):
14 | with db_session:
15 | message.db_chat.current.delete()
16 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/group/new_members.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client, filters
2 |
3 | from texttospeech.db.models import *
4 | from ... import handlers
5 |
6 |
7 | @Client.on_message(filters.new_chat_members, group=handlers.NEW_CHAT_MEMBERS)
8 | async def on_new_chat_members(_, message):
9 | for member in message.new_chat_members:
10 | if member.is_self:
11 | message.self_was_added = True
12 | continue
13 |
14 | User.from_pyrogram(member)
15 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/group/redirects.py:
--------------------------------------------------------------------------------
1 | from pyrogram import filters, Client
2 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
3 |
4 | from texttospeech.bot import cfilters
5 | from texttospeech.util import formatting
6 |
7 |
8 | async def reply_redirect(message, deeplink):
9 | user = message.db_user
10 |
11 | msg = user.get_message('group_redirect_message')
12 | button = user.get_message('group_redirect_button')
13 |
14 | keyboard = InlineKeyboardMarkup([[InlineKeyboardButton(button, url=formatting.deeplink(deeplink))]])
15 |
16 | await message.reply_text(msg, reply_markup=keyboard, disable_web_page_preview=True)
17 |
18 |
19 | @Client.on_message(filters.group & cfilters.group_command('settings'))
20 | async def on_settings_command(_, message):
21 | await reply_redirect(message, 'settings')
22 |
23 |
24 | @Client.on_message(filters.group & cfilters.group_command('language'))
25 | async def on_language_command(_, message):
26 | await reply_redirect(message, 'language')
27 |
28 |
29 | @Client.on_message(filters.group & cfilters.group_command('start'))
30 | async def on_start_command(_, message):
31 | if 'added' in message.command:
32 | return
33 |
34 | await reply_redirect(message, 'start')
35 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/group/welcome.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client, filters
2 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup
3 |
4 | from texttospeech.bot import cfilters
5 | from texttospeech.db.models import Settings
6 | from texttospeech.localization import languages
7 | from texttospeech.util import formatting
8 |
9 |
10 | @Client.on_message(filters.group & ((~cfilters.group_setting(Settings.WELCOME_SENT)) | cfilters.added))
11 | async def on_send_welcome(_, message):
12 | language = 'en_US'
13 |
14 | if message.db_user:
15 | language = message.db_user.language
16 |
17 | message.db_chat.set_setting(Settings.WELCOME_SENT, True)
18 | msg = languages.get_message('group_self_was_added', language)
19 |
20 | await message.reply_text(msg, reply_markup=InlineKeyboardMarkup([
21 | [InlineKeyboardButton(languages.get_message('inline_create_audio', language),
22 | switch_inline_query_current_chat='')],
23 | [InlineKeyboardButton(languages.get_message('settings_button', language),
24 | url=formatting.deeplink('settings'))]
25 | ]))
26 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/inline/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/bot/plugin/inline/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/inline/inline.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 |
4 | from pyrogram import Client
5 | from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent, InlineKeyboardMarkup, InlineKeyboardButton
6 |
7 | from texttospeech.audio import tts
8 | from texttospeech.bot.inline.inline_query_result_voice import InlineQueryResultVoice
9 | from texttospeech.db.models import AudioOrigin, Settings, Audio
10 | from texttospeech.util import config
11 |
12 | latest = {}
13 |
14 |
15 | def create_keyboard(user) -> InlineKeyboardMarkup:
16 | return InlineKeyboardMarkup([
17 | [InlineKeyboardButton(user.get_message('inline_create_audio'), switch_inline_query='')]
18 | ])
19 |
20 |
21 | def create_error_result(user, message: str) -> InlineQueryResultArticle:
22 | msg = user.get_message(message)
23 | keyboard = create_keyboard(user)
24 |
25 | return InlineQueryResultArticle(title=msg, reply_markup=keyboard,
26 | thumb_url=config.ERROR_THUMB_URL,
27 | input_message_content=InputTextMessageContent(message_text=msg))
28 |
29 |
30 | def create_audio_result(text: str, user, keyboard: bool = True, language=None):
31 | if not language:
32 | language = user.language
33 |
34 | link = tts.create_link(text=text, language=language, slow=user.get_setting(Settings.SLOW_MODE, bool))
35 | title = user.get_message('inline_create_audio')
36 | caption = user.get_message('created_with')
37 | keyboard = create_keyboard(user) if keyboard else None
38 |
39 | return InlineQueryResultVoice(voice_url=link,
40 | title=title,
41 | caption=caption,
42 | reply_markup=keyboard)
43 |
44 |
45 | @Client.on_inline_query()
46 | async def on_inline_query(_, query):
47 | latest[query.from_user.id] = query.id
48 |
49 | user = query.db_user
50 | text = query.query.replace('\n', '').strip()
51 |
52 | switch_pm_text = user.get_message('inline_language')
53 | switch_pm_parameter = 'language'
54 | results = []
55 |
56 | if not text:
57 | results.append(create_error_result(user, 'empty_text'))
58 | else:
59 | language = user.language
60 |
61 | if len(text.split(' ')[0]) == 2 and tts.is_valid(text[:2]):
62 | language = text[:2]
63 | text = text[2:]
64 |
65 | if len(text) > config.AUDIO_CHARACTER_LIMIT:
66 | results.append(create_error_result(user, 'character_limit_inline'))
67 | else:
68 | try:
69 | await asyncio.sleep(1) # So that the user doesn't spam inline requests because TG is dumb
70 |
71 | if latest[query.from_user.id] != query.id:
72 | return
73 |
74 | result = create_audio_result(text, user, language=language)
75 |
76 | if result:
77 | results.append(result)
78 | except Exception as e:
79 | results.append(create_error_result(user, 'empty_text'))
80 | logging.error('An error occured whilst creating an inline audio.', exc_info=e)
81 |
82 | await query.answer(results=results, cache_time=0, is_personal=True,
83 | switch_pm_text=switch_pm_text, switch_pm_parameter=switch_pm_parameter)
84 |
85 |
86 | @Client.on_chosen_inline_result()
87 | async def on_chosen_inline_result(_, chosen):
88 | Audio.create(chosen.db_user, origin=AudioOrigin.INLINE)
89 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/language.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client, filters
2 | from pyrogram.errors import RPCError
3 | from pyrogram.types import InlineKeyboardButton
4 |
5 | from texttospeech.bot import cfilters
6 | from texttospeech.localization import languages
7 |
8 |
9 | @Client.on_message(filters.command('language') & filters.private)
10 | async def on_language_command(_, message):
11 | user = message.db_user
12 | user.reset_action()
13 |
14 | back = InlineKeyboardButton(user.get_message('back_button'), callback_data='main_menu')
15 | await message.reply_text(user.get_message('language_selection'), reply_markup=languages.create_keyboard(back))
16 |
17 |
18 | @Client.on_callback_query(cfilters.callback_data('change_language'))
19 | async def on_language_callback(_, callback):
20 | user = callback.db_user
21 | back = InlineKeyboardButton(user.get_message('back_button'), callback_data='settings')
22 |
23 | await callback.answer()
24 | await callback.edit_message_text(user.get_message('language_selection'),
25 | reply_markup=languages.create_keyboard(back))
26 |
27 |
28 | @Client.on_callback_query(filters.regex('^set_language_'))
29 | async def on_set_language_callback(_, callback):
30 | callback.db_user.set_language(callback.data[len('set_language_'):])
31 |
32 | try:
33 | await callback.answer(callback.db_user.get_message('language_set'), show_alert=True)
34 | await callback.edit_message_text(callback.db_user.get_message('language_selection'),
35 | reply_markup=callback.message.reply_markup)
36 | except RPCError: # If the language was the same (message wasn't edited)
37 | pass
38 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/main_menu.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client, filters
2 | from pyrogram.errors import RPCError
3 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
4 |
5 | from texttospeech.bot import cfilters
6 | from texttospeech.bot.plugin import settings, language
7 | from texttospeech.util import formatting
8 |
9 |
10 | def create_keyboard(user) -> InlineKeyboardMarkup:
11 | speak = InlineKeyboardButton(user.get_message('speak_button'),
12 | callback_data="create_audio")
13 |
14 | about_me = InlineKeyboardButton(user.get_message('info_button'),
15 | callback_data='info')
16 |
17 | stats = InlineKeyboardButton(user.get_message('stats_button'),
18 | callback_data='stats')
19 |
20 | settings = InlineKeyboardButton(user.get_message('settings_button'),
21 | callback_data='settings')
22 |
23 | inline = InlineKeyboardButton(user.get_message('inline_button'),
24 | switch_inline_query='')
25 |
26 | group = InlineKeyboardButton(user.get_message('add_to_group_button'),
27 | url=formatting.deepgroup('added'))
28 |
29 | return InlineKeyboardMarkup([[speak], [settings, inline], [about_me, stats], [group]])
30 |
31 |
32 | def create_message(user) -> str:
33 | return user.get_message('main_menu', image='')
34 |
35 |
36 | @Client.on_message(filters.command('start') & filters.private)
37 | async def on_start_command(client, message):
38 | user = message.db_user
39 | user.reset_action()
40 |
41 | if len(message.command) > 1:
42 | arg = message.command[1].lower()
43 |
44 | if arg == 'language':
45 | await language.on_language_command(client, message)
46 | return
47 | elif arg == 'settings':
48 | await settings.on_settings_command(client, message)
49 | return
50 |
51 | text = create_message(user)
52 | keyboard = create_keyboard(user)
53 |
54 | await message.reply_text(text, reply_markup=keyboard, disable_web_page_preview=False)
55 |
56 |
57 | @Client.on_callback_query(cfilters.callback_data('main_menu'))
58 | async def on_start_callback(_, callback):
59 | user = callback.db_user
60 | user.reset_action()
61 |
62 | await callback.answer()
63 |
64 | if callback.message.voice:
65 | try:
66 | await callback.edit_message_reply_markup(None)
67 | except RPCError: # Message already has empty keyboard or is deleted
68 | pass
69 |
70 | await callback.message.reply_text(create_message(user), reply_markup=create_keyboard(user),
71 | disable_web_page_preview=False)
72 | else:
73 | await callback.edit_message_text(create_message(user), reply_markup=create_keyboard(user),
74 | disable_web_page_preview=False)
75 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/prehandler.py:
--------------------------------------------------------------------------------
1 | from timeit import timeit
2 |
3 | from pyrogram import Client, StopPropagation
4 |
5 | from texttospeech.db.models import *
6 | from .. import handlers
7 |
8 |
9 | @Client.on_message(group=handlers.DATABASE)
10 | async def on_message(_, message):
11 | message.db_user = User.from_pyrogram(message)
12 | message.db_chat = Chat.from_pyrogram(message)
13 |
14 |
15 | @Client.on_callback_query(group=handlers.DATABASE)
16 | async def on_callback_query(_, callback):
17 | callback.db_user = User.from_pyrogram(callback)
18 |
19 |
20 | @Client.on_inline_query(group=handlers.DATABASE)
21 | async def on_inline_query(_, query):
22 | query.db_user = User.from_pyrogram(query)
23 |
24 |
25 | @Client.on_chosen_inline_result(group=handlers.DATABASE)
26 | async def on_chosen_inline_result(_, chosen):
27 | chosen.db_user = User.from_pyrogram(chosen)
28 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/settings.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client, filters
2 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
3 |
4 | from texttospeech.bot import cfilters
5 | from texttospeech.db.models import *
6 |
7 |
8 | @db_session
9 | def create_message(user) -> str:
10 | always_speak = user.get_setting(Settings.ALWAYS_SPEAK, Emoji)
11 | slow_mode = user.get_setting(Settings.SLOW_MODE, Emoji)
12 | language = user.get_message('full_name')
13 |
14 | return user.get_message('settings_message',
15 | always_speak=always_speak,
16 | slow_mode=slow_mode,
17 | language=language)
18 |
19 |
20 | @db_session
21 | def create_keyboard(user) -> InlineKeyboardMarkup:
22 | always_speak = user.get_setting(Settings.ALWAYS_SPEAK, Emoji)
23 | slow_mode = user.get_setting(Settings.SLOW_MODE, Emoji)
24 |
25 | always_speak_button = InlineKeyboardButton(user.get_message('always_speak_button', status=always_speak),
26 | callback_data='toggle_always_speak')
27 |
28 | slow_mode_button = InlineKeyboardButton(user.get_message('slow_mode_button', status=slow_mode),
29 | callback_data='toggle_slow_mode')
30 |
31 | language_button = InlineKeyboardButton(user.get_message('language_button'),
32 | callback_data='change_language')
33 |
34 | menu_button = InlineKeyboardButton(user.get_message('menu_button'),
35 | callback_data='main_menu')
36 |
37 | return InlineKeyboardMarkup([[always_speak_button, slow_mode_button], [language_button], [menu_button]])
38 |
39 |
40 | @Client.on_message(filters.command('settings') & filters.private)
41 | async def on_settings_command(_, message):
42 | user = message.db_user
43 |
44 | await message.reply_text(create_message(user), reply_markup=create_keyboard(user))
45 |
46 |
47 | @Client.on_callback_query(cfilters.callback_data('settings'))
48 | async def on_settings_callback(_, callback):
49 | user = callback.db_user
50 |
51 | await callback.answer()
52 | await callback.edit_message_text(create_message(user), reply_markup=create_keyboard(user))
53 |
--------------------------------------------------------------------------------
/src/texttospeech/bot/plugin/speak.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from pyrogram import Client, filters
4 | from pyrogram.errors import RPCError
5 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message
6 |
7 | from texttospeech.audio import tts
8 | from texttospeech.bot import cfilters
9 | from texttospeech.db.models import *
10 | from texttospeech.util import config
11 | from texttospeech.util.antiflood import AntiFlood
12 |
13 | antiflood = AntiFlood(max_amount=2, timeout=5)
14 |
15 |
16 | def create_input_keyboard(user) -> InlineKeyboardMarkup:
17 | return InlineKeyboardMarkup([[InlineKeyboardButton(user.get_message('back_button'), callback_data='main_menu')]])
18 |
19 |
20 | def create_keyboard(user) -> InlineKeyboardMarkup:
21 | return InlineKeyboardMarkup(
22 | [[InlineKeyboardButton(user.get_message('speak_again_button'), callback_data='create_audio')],
23 | [InlineKeyboardButton(user.get_message('menu_button'), callback_data='main_menu')]]
24 | )
25 |
26 |
27 | async def is_flooding(user, message) -> bool:
28 | if antiflood.is_flooding(user.id):
29 | if message:
30 | await message.reply_text(user.get_message('anti_flood'))
31 |
32 | return True
33 |
34 | return False
35 |
36 |
37 | async def is_above_char_limit(user, message, text) -> bool:
38 | if len(text) > config.AUDIO_CHARACTER_LIMIT:
39 | if message:
40 | await message.reply_text(user.get_message('character_limit_message'))
41 |
42 | return True
43 |
44 | return False
45 |
46 |
47 | async def send_audio(text: str, client: Client, user: User, chat: Chat = None, keyboard: bool = True,
48 | to_delete: Message = None, reply_to_message_id=None, language=None):
49 | if not language:
50 | language = user.language
51 |
52 | slow_mode = user.get_setting(Settings.SLOW_MODE, bool)
53 | file = tts.create_audio(text=text, language=language, slow=slow_mode)
54 |
55 | try:
56 | message = user.get_message('created_with')
57 | keyboard = create_keyboard(user) if keyboard else None
58 |
59 | if to_delete:
60 | await to_delete.delete(revoke=True)
61 |
62 | await client.send_voice(chat_id=chat.id if chat else user.id, voice=file, caption=message,
63 | reply_markup=keyboard, reply_to_message_id=reply_to_message_id)
64 | except:
65 | pass
66 |
67 | os.remove(file)
68 |
69 |
70 | async def reply_audio(user, client, message, text, origin, language, chat=None, keyboard=True,
71 | reply_to_message_id=None):
72 | user.reset_action()
73 | to_delete = await message.reply_text(user.get_message('creating_audio'))
74 |
75 | await send_audio(text=text, client=client, user=user, keyboard=keyboard, to_delete=to_delete,
76 | reply_to_message_id=reply_to_message_id, language=language, chat=chat)
77 |
78 | Audio.create(user=user, chat=chat, origin=origin)
79 |
80 |
81 | async def execute(client, message, origin, chat=None, keyboard=True, reply_to_message_id=None):
82 | user = message.db_user
83 | language = user.language
84 | text = ' '.join(message.command[1:]).strip() if message.command else message.text
85 |
86 | if await is_flooding(user, message) or await is_above_char_limit(user, message, text):
87 | return
88 |
89 | text = text.replace('\n', '').strip()
90 |
91 | if not text:
92 | return
93 |
94 | await reply_audio(user=user, client=client, message=message,
95 | text=text, origin=origin, language=language,
96 | keyboard=keyboard, reply_to_message_id=reply_to_message_id,
97 | chat=chat)
98 |
99 |
100 | @Client.on_message(filters.command(['tts', 'speak', 'audio']) & filters.private)
101 | async def on_speak_command_private(client, message):
102 | user = message.db_user
103 |
104 | if len(message.command) < 2:
105 | user.set_action('create_audio')
106 |
107 | msg = user.get_message('awaiting_text')
108 | keyboard = create_input_keyboard(user)
109 |
110 | await message.reply_text(msg, reply_markup=keyboard)
111 | else:
112 | await execute(client=client, message=message, origin=AudioOrigin.COMMAND)
113 |
114 |
115 | @Client.on_message(cfilters.group_command(['tts', 'speak', 'audio']) & filters.group)
116 | async def on_speak_command_group(client, message):
117 | user = message.db_user
118 | chat = message.db_chat
119 |
120 | if len(message.command) < 2:
121 | await message.reply_text(user.get_message('group_speak_no_arguments'))
122 | else:
123 | reply_to_message_id = message.reply_to_message.message_id if message.reply_to_message else message.message_id
124 |
125 | await execute(client=client, message=message, origin=AudioOrigin.GROUP, keyboard=False,
126 | chat=chat, reply_to_message_id=reply_to_message_id)
127 |
128 |
129 | @Client.on_callback_query(cfilters.callback_data('create_audio'))
130 | async def on_speak_callback(_, callback):
131 | user = callback.db_user
132 | user.set_action('create_audio')
133 |
134 | msg = user.get_message('awaiting_text')
135 | keyboard = create_input_keyboard(user)
136 |
137 | await callback.answer()
138 |
139 | if callback.message and callback.message.voice:
140 | try:
141 | await callback.edit_message_reply_markup(None)
142 | except RPCError: # Message not edited...
143 | pass
144 |
145 | await callback.message.reply_text(msg, reply_markup=keyboard)
146 | else:
147 | await callback.edit_message_text(msg, reply_markup=keyboard)
148 |
149 |
150 | @Client.on_message(filters.text & cfilters.not_command() & ~filters.via_bot & filters.private &
151 | (cfilters.action('create_audio') | cfilters.setting(Settings.ALWAYS_SPEAK)) &
152 | ~filters.edited)
153 | async def on_speak_text(client, message):
154 | keyboard = message.db_user.action == 'create_audio'
155 | origin = AudioOrigin.BUTTON if keyboard else AudioOrigin.ALWAYS_SPEAK
156 |
157 | await execute(client=client, message=message, origin=origin, keyboard=keyboard)
158 |
--------------------------------------------------------------------------------
/src/texttospeech/db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/db/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/db/models.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from datetime import datetime
3 | from typing import Union, Type
4 |
5 | from pony.orm import *
6 | # Still no enum support in pony :(
7 | from pyrogram import types
8 |
9 | from texttospeech.localization import languages
10 | from texttospeech.util import config
11 | from texttospeech.util.emojifier import Emoji
12 | from ..localization.languages import get_message
13 |
14 | db = Database()
15 | updates = Union[types.User, types.Message, types.InlineQuery,
16 | types.ChosenInlineResult, types.CallbackQuery]
17 |
18 |
19 | class Settings:
20 | """
21 | Available bot settings.
22 | """
23 |
24 | WELCOME_SENT = 'welcome_sent'
25 | ALWAYS_SPEAK = 'always_speak'
26 | SLOW_MODE = 'slow_mode'
27 | LANGUAGE = 'language'
28 | ACTION = 'action'
29 | BANNED = 'banned'
30 | ADMIN = 'admin'
31 |
32 |
33 | class ChatType:
34 | """
35 | Possible chat types.
36 | """
37 |
38 | GROUP = 'group'
39 | CHANNEL = 'channel'
40 | SUPERGROUP = 'supergroup'
41 |
42 | @staticmethod
43 | def from_chat(chat: types.Chat) -> str:
44 | return getattr(ChatType, chat.type.upper(), None)
45 |
46 |
47 | class AudioOrigin:
48 | """
49 | How was an audio generated?
50 | """
51 |
52 | UNKNOWN = 'unknown'
53 | ALWAYS_SPEAK = 'always_speak'
54 | INLINE = 'inline'
55 | BUTTON = 'button'
56 | COMMAND = 'command'
57 | GROUP = 'group'
58 |
59 |
60 | class StartReason:
61 | """
62 | How did a user find the bot?
63 | """
64 |
65 | UNKNOWN = 'unknown'
66 | MESSAGE = 'message'
67 | INLINE = 'inline'
68 | CALLBACK = 'callback'
69 | GROUP = 'group'
70 |
71 | @staticmethod
72 | def from_update(update: updates) -> str:
73 | if isinstance(update, types.Message):
74 | if update.chat.type != 'private' and update.chat.type != 'bot':
75 | return StartReason.GROUP
76 |
77 | return StartReason.MESSAGE
78 |
79 | if isinstance(update, types.InlineQuery) or isinstance(update, types.ChosenInlineResult):
80 | return StartReason.INLINE
81 |
82 | if isinstance(update, types.CallbackQuery):
83 | return StartReason.CALLBACK
84 |
85 | return StartReason.UNKNOWN
86 |
87 | # noinspection PyArgumentList
88 | class User(db.Entity):
89 | """
90 | A telegram user.
91 | """
92 |
93 | id = PrimaryKey(int, size=64)
94 | first_name = Required(str)
95 | last_name = Optional(str)
96 | username = Optional(str)
97 | is_bot = Required(bool, default=False)
98 | dc_id = Optional(int)
99 | start_reason = Required(str, default=StartReason.UNKNOWN)
100 | last_update = Required(datetime, default=datetime.now)
101 | creation_date = Required(datetime, default=datetime.now)
102 | is_active = Required(bool, default=True)
103 |
104 | audios = Set('Audio')
105 | settings = Set('Setting')
106 |
107 | @staticmethod
108 | @db_session
109 | def from_pyrogram(tg_user: updates) -> Union['User', None]:
110 | start_reason = StartReason.from_update(tg_user)
111 |
112 | if not isinstance(tg_user, types.User):
113 | tg_user = tg_user.from_user
114 |
115 | if not tg_user:
116 | return None
117 |
118 | user_id = tg_user.id
119 | first_name = tg_user.first_name
120 | last_name = tg_user.last_name or ''
121 | username = tg_user.username or ''
122 | is_bot = tg_user.is_bot
123 | language = languages.match_closest(tg_user.language_code)
124 | dc_id = tg_user.dc_id
125 |
126 | if not (db_user := User.get(id=user_id)):
127 | db_user = User(id=user_id,
128 | first_name=first_name,
129 | last_name=last_name,
130 | username=username,
131 | dc_id=dc_id,
132 | is_bot=is_bot,
133 | start_reason=start_reason)
134 |
135 | Setting(user=db_user, name=Settings.LANGUAGE, value=language)
136 | else:
137 | db_user.first_name = first_name
138 | db_user.last_name = last_name
139 | db_user.username = username
140 |
141 | return db_user
142 |
143 | def before_update(self):
144 | self.last_update = datetime.now()
145 |
146 | def get_message(self, name: str, **kwargs) -> str:
147 | return get_message(message_name=name, language_name=self.language, user=self, **kwargs)
148 |
149 | @db_session
150 | def get_setting(self, name: str, type: Type = str):
151 | setting = self.current.settings.select(lambda s: s.name == name)
152 |
153 | value = None
154 |
155 | if len(setting) > 0:
156 | value = setting.first().value
157 |
158 | if type is bool or type is Emoji:
159 | value = value and value.lower() in ('true', 't', 'yes')
160 |
161 | if type is Emoji:
162 | value = Emoji.from_boolean(value)
163 |
164 | return value
165 |
166 | @db_session
167 | def set_setting(self, name: str, value):
168 | user = self.current
169 | setting = user.settings.select(lambda s: s.name == name).first()
170 |
171 | if not setting:
172 | if not value:
173 | return value
174 |
175 | Setting(user=user, name=name, value=str(value))
176 | else:
177 | if not value:
178 | setting.delete()
179 | return value
180 |
181 | setting.value = str(value)
182 |
183 | return value
184 |
185 | def toggle_setting(self, name: str):
186 | return self.set_setting(name=name, value=not self.get_setting(name, bool))
187 |
188 | def remove_setting(self, name: str):
189 | self.set_setting(name=name, value=None)
190 |
191 | def set_action(self, action: str):
192 | self.set_setting(Settings.ACTION, action)
193 |
194 | def set_language(self, language: str):
195 | self.set_setting(Settings.LANGUAGE, language)
196 |
197 | def ban(self):
198 | self.set_setting(Settings.BANNED, True)
199 |
200 | def unban(self):
201 | self.set_setting(Settings.BANNED, False)
202 |
203 | def promote(self):
204 | self.set_setting(Settings.ADMIN, True)
205 |
206 | def demote(self):
207 | self.set_setting(Settings.ADMIN, False)
208 |
209 | def reset_action(self):
210 | self.remove_setting(Settings.ACTION)
211 |
212 | @property
213 | def current(self) -> 'User':
214 | return User.get(id=self.id)
215 |
216 | @property
217 | def language(self) -> str:
218 | return self.get_setting(Settings.LANGUAGE)
219 |
220 | @property
221 | def is_banned(self) -> bool:
222 | return self.get_setting(Settings.BANNED, bool)
223 |
224 | @property
225 | def is_admin(self) -> bool:
226 | return self.get_setting(Settings.ADMIN, bool)
227 |
228 | @property
229 | def action(self) -> str:
230 | return self.get_setting(Settings.ACTION)
231 |
232 | @property
233 | def full_name(self) -> str:
234 | return f'{self.first_name}{" " + self.last_name if self.last_name else ""}'
235 |
236 | @property
237 | def mention(self) -> str:
238 | return f" Union['Chat', None]:
262 | if not isinstance(tg_chat, types.Chat):
263 | tg_chat = tg_chat.chat
264 |
265 | if not tg_chat:
266 | return None
267 |
268 | if not (chat_type := ChatType.from_chat(tg_chat)):
269 | return None
270 |
271 | chat_id = tg_chat.id
272 | title = tg_chat.title
273 | username = tg_chat.username or ''
274 | description = tg_chat.description or ''
275 | members_count = tg_chat.members_count or 0
276 |
277 | if not (db_chat := Chat.get(id=chat_id)):
278 | db_chat = Chat(id=chat_id, title=title, username=username,
279 | description=description, members_count=members_count,
280 | type=chat_type)
281 | else:
282 | db_chat.title = title
283 | db_chat.username = username
284 | db_chat.description = description
285 | db_chat.members_count = members_count
286 |
287 | return db_chat
288 |
289 | @db_session
290 | def get_setting(self, name: str, type: Type = str):
291 | setting = self.current.settings.select(lambda s: s.name == name)
292 |
293 | value = None
294 |
295 | if len(setting) > 0:
296 | value = setting.first().value
297 |
298 | if type is bool or type is Emoji:
299 | value = value and value.lower() in ('true', 't', 'yes')
300 |
301 | if type is Emoji:
302 | value = Emoji.from_boolean(value)
303 |
304 | return value
305 |
306 | @db_session
307 | def set_setting(self, name: str, value):
308 | chat = self.current
309 | setting = chat.settings.select(lambda s: s.name == name).first()
310 |
311 | if not setting:
312 | if not value:
313 | return value
314 |
315 | Setting(chat=chat, name=name, value=str(value))
316 | else:
317 | if not value:
318 | setting.delete()
319 | return value
320 |
321 | setting.value = str(value)
322 |
323 | return value
324 |
325 | def toggle_setting(self, name: str):
326 | return self.set_setting(name=name, value=not self.get_setting(name, bool))
327 |
328 | def before_update(self):
329 | self.last_update = datetime.now()
330 |
331 | @property
332 | def current(self):
333 | return Chat.get(id=self.id)
334 |
335 |
336 | class Setting(db.Entity):
337 | id = PrimaryKey(int, auto=True)
338 | user = Optional(User)
339 | chat = Optional(Chat)
340 | name = Required(str)
341 | value = Required(str)
342 |
343 |
344 | class Audio(db.Entity):
345 | id = PrimaryKey(int, auto=True)
346 | user = Required(User)
347 | chat = Optional(Chat)
348 | language = Required(str)
349 | origin = Required(str, default=StartReason.UNKNOWN)
350 | creation_date = Required(datetime, default=datetime.now)
351 |
352 | @staticmethod
353 | @db_session
354 | def create(user: User, chat: Chat = None, origin: str = None, language: str = None) -> 'Audio':
355 | user = user.current
356 | chat = chat.current if chat else None
357 |
358 | if not language:
359 | language = user.language
360 |
361 | return Audio(user=user, chat=chat, language=language, origin=origin)
362 |
363 |
364 | def setup():
365 | db.bind(config.DB_CON)
366 | db.generate_mapping(create_tables=True)
367 |
368 | logging.warning('Database connected successfully.')
369 |
--------------------------------------------------------------------------------
/src/texttospeech/localization/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/localization/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/localization/languages.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Optional
3 |
4 | from plate import Plate
5 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
6 |
7 | from ..util import config
8 |
9 |
10 | plate = Plate(root=config.LANGUAGES_DIR)
11 |
12 |
13 | def match_closest(language: str) -> str:
14 | """
15 | Gets the closest language that matches the given string.
16 |
17 | :param language: the language to match
18 | :return: the language that was found
19 | """
20 |
21 | if not language:
22 | return 'en_US'
23 |
24 | for locale in plate.locales.keys():
25 | if locale[:2] == language[:2]:
26 | return locale
27 |
28 | return 'en_US'
29 |
30 |
31 | def get_message(message_name: str, language_name: str = 'en_US', **kwargs) -> Optional[str]:
32 | """
33 | Gets a localized message. If the message does not exist in the selected language, it will use English.
34 |
35 | :param message_name: the message
36 | :param language_name: the language
37 | :param kwargs: additional arguments passed to the message
38 | :return: the localized message
39 | """
40 |
41 | language_name = match_closest(language_name)
42 |
43 | if 'user' in kwargs:
44 | kwargs['first_name'] = kwargs['user'].first_name
45 | kwargs['full_name'] = kwargs['user'].full_name
46 | kwargs['mention'] = kwargs['user'].mention
47 |
48 | try:
49 | kwargs['flag'] = plate('flag', language_name)
50 | return plate(message_name, language_name, **kwargs)
51 | except ValueError as e:
52 | logging.error(f'An error occured whilst fetching {message_name} in language {language_name}', exc_info=e)
53 |
54 | if language_name == 'en_US':
55 | return None
56 |
57 | kwargs['flag'] = plate('flag', 'en_US')
58 | return plate(message_name, 'en_US', **kwargs)
59 |
60 |
61 | def create_keyboard(back: InlineKeyboardButton = None) -> InlineKeyboardMarkup:
62 | """
63 | Creates an inline keyboard for language selection.
64 |
65 | :param back: The back button on the keyboard (to go back to the previous menu)
66 | :return: the keyboard
67 | """
68 |
69 | keyboard = [[]]
70 |
71 | for lang in plate.locales:
72 | if len(keyboard[-1]) >= 3:
73 | keyboard.append([])
74 |
75 | flag = plate('flag', lang)
76 | keyboard[-1].append(InlineKeyboardButton(flag, callback_data=f'set_language_{lang}'))
77 |
78 | if back:
79 | keyboard.append([back])
80 |
81 | return InlineKeyboardMarkup(keyboard)
82 |
83 |
84 | def create_message_data(user) -> dict:
85 | """
86 | Creates the language selection message data.
87 |
88 | :param user: the user
89 | :return: a dict containing text and reply_markup
90 | """
91 |
92 | msg = user.get_message('language_selection')
93 | back = InlineKeyboardButton(user.get_message('back_button'), callback_data='settings')
94 | keyboard = create_keyboard(back=back)
95 |
96 | return dict(text=msg, reply_markup=keyboard)
97 |
--------------------------------------------------------------------------------
/src/texttospeech/util/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/util/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/util/antiflood.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 |
4 | class AntiFlood:
5 | def __init__(self, max_amount: int, timeout: int):
6 | self.cache = {}
7 | self.max_amount = max_amount
8 | self.timeout = timeout
9 |
10 | def is_flooding(self, key) -> bool:
11 | if key not in self.cache:
12 | self.cache[key] = user_cache = []
13 | else:
14 | user_cache = self.cache[key]
15 |
16 | for dt in user_cache:
17 | if dt < datetime.now() - timedelta(seconds=self.timeout):
18 | user_cache.remove(dt)
19 |
20 | user_cache.append(datetime.now())
21 |
22 | return len(user_cache) > self.max_amount
23 |
--------------------------------------------------------------------------------
/src/texttospeech/util/config.sample.py:
--------------------------------------------------------------------------------
1 | API_ID = -1 # Insert your API ID
2 | API_HASH = "" # Insert your API Hash
3 |
4 | BOT_TOKEN = "" # Insert your bot token
5 | BOT_USERNAME = "" # Insert your bot's username
6 | BOT_WORKERS = 8 # Insert the workers amount
7 |
8 | SESSION_NAME = "session"
9 |
10 | DB_CON = {
11 | 'provider': 'postgres',
12 | 'host': 'postgres',
13 | 'user': 'postgres',
14 | 'password': '',
15 | 'database': 'tts'
16 | }
17 |
18 | LANGUAGES_DIR = "languages/" # Your languages directory
19 | PLUGINS_DIR = 'texttospeech/bot/plugin'
20 | AUDIOS_DIR = 'audios/' # Your audios directory
21 |
22 | AUDIOS_DOMAIN = 'https://audio.example.org' # Domain for audios (inline)
23 | AUDIO_THUMB_URL = 'https://i.imgur.com/Ginyq2C.png' # Thumbnail for inline audios
24 | ERROR_THUMB_URL = 'https://i.imgur.com/RARF2nv.png' # Thumbnail for inline errors
25 | AUDIO_CHARACTER_LIMIT = 500 # The character limit for an audio
26 |
--------------------------------------------------------------------------------
/src/texttospeech/util/emojifier.py:
--------------------------------------------------------------------------------
1 | class Emoji:
2 | @staticmethod
3 | def from_boolean(value: bool) -> str:
4 | return '✅' if value else '❌'
5 |
--------------------------------------------------------------------------------
/src/texttospeech/util/files.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | CHARACTERS = string.ascii_letters + string.digits
5 |
6 |
7 | def random_name(length: int = 16, extension: str = '') -> str:
8 | return ''.join(random.choices(CHARACTERS, k=length)) + ('.' + extension if extension else '')
--------------------------------------------------------------------------------
/src/texttospeech/util/formatting.py:
--------------------------------------------------------------------------------
1 | from texttospeech.util import config
2 |
3 | DEEP_LINKING = f'https://t.me/{config.BOT_USERNAME}?start='
4 | DEEP_GROUPING = f'https://t.me/{config.BOT_USERNAME}?startgroup='
5 | INVISIBLE_CHAR = '⠀'
6 |
7 | HTML_LINK = "{text}"
8 |
9 |
10 | def link(url: str, text: str) -> str:
11 | return HTML_LINK.format(url=url, text=text)
12 |
13 |
14 | def invisible_link(url: str) -> str:
15 | return link(url=url, text=INVISIBLE_CHAR)
16 |
17 |
18 | def deeplink(path: str) -> str:
19 | return DEEP_LINKING + path
20 |
21 |
22 | def deepgroup(path: str) -> str:
23 | return DEEP_GROUPING + path
24 |
--------------------------------------------------------------------------------
/src/texttospeech/web/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdotpink/TTSBotPython/61d532b553ddfe97e3b15b4432fc6700cab67857/src/texttospeech/web/__init__.py
--------------------------------------------------------------------------------
/src/texttospeech/web/web.py:
--------------------------------------------------------------------------------
1 | import os
2 | from threading import Thread
3 | from time import sleep
4 |
5 | from flask import Flask, jsonify, send_file
6 |
7 | from texttospeech.util import config
8 |
9 | app = Flask(__name__)
10 |
11 | FILE_EXTENSION = '.ogg'
12 | AUDIO_FOLDER = '../../' + config.AUDIOS_DIR
13 |
14 |
15 | @app.route('/', methods=['GET'])
16 | def audio(filename: str):
17 | if not filename.endswith(FILE_EXTENSION) or '/' in filename:
18 | return jsonify(status=403, message='Invalid file!')
19 |
20 | file = os.path.join(AUDIO_FOLDER, filename)
21 |
22 | return send_file(file, mimetype='audio/ogg')
23 |
24 |
25 | def run():
26 | thread = Thread(target=lambda: app.run(host='0.0.0.0', debug=False))
27 | thread.start()
28 |
--------------------------------------------------------------------------------