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