diff --git a/README.md b/README.md index be06735..7fbeab6 100644 --- a/README.md +++ b/README.md @@ -110,18 +110,21 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | | `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use, i.e. `Europe/Rome` (required only for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | - | +| `DUCKDUCKGO_SAFESEARCH` | DuckDuckGo safe search (`on`, `off` or `moderate`) (optional, applies to `ddg_web_search` and `ddg_image_search`) | `moderate` | #### Available plugins -| Name | Description | Required environment variable(s) | -|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | -| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | -| `ddg_web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `ddg_translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `ddg_image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | -| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | -| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | +| Name | Description | Required environment variable(s) | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | +| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | +| `ddg_web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `ddg_translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `ddg_image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | +| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | +| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | +| `dice` | Send a dice in the chat! | - | +| `youtube_audio_extractor` | Extract audio from YouTube videos | - | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/openai_helper.py b/bot/openai_helper.py index 67ab768..61fabf5 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,6 +14,7 @@ from calendar import monthrange from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type +from bot.utils import is_direct_result from plugin_manager import PluginManager # Models can be found here: https://platform.openai.com/docs/models/overview @@ -118,6 +119,9 @@ class OpenAIHelper: response = await self.__common_get_chat_response(chat_id, query) if self.config['enable_functions']: response, plugins_used = await self.__handle_function_call(chat_id, response) + if is_direct_result(response): + return response, '0' + answer = '' if len(response.choices) > 1 and self.config['n_choices'] > 1: @@ -158,6 +162,9 @@ class OpenAIHelper: response = await self.__common_get_chat_response(chat_id, query, stream=True) if self.config['enable_functions']: response, plugins_used = await self.__handle_function_call(chat_id, response, stream=True) + if is_direct_result(response): + yield response, '0' + return answer = '' async for item in response: @@ -283,7 +290,16 @@ class OpenAIHelper: logging.info(f'Calling function {function_name} with arguments {arguments}') function_response = await self.plugin_manager.call_function(function_name, arguments) - logging.info(f'Got response {function_response}') + + if function_name not in plugins_used: + plugins_used += (function_name,) + + if is_direct_result(function_response): + self.__add_function_call_to_history(chat_id=chat_id, function_name=function_name, + content=json.dumps({'result': 'Done, the content has been sent' + 'to the user.'})) + return function_response, plugins_used + self.__add_function_call_to_history(chat_id=chat_id, function_name=function_name, content=function_response) response = await openai.ChatCompletion.acreate( model=self.config['model'], @@ -292,8 +308,6 @@ class OpenAIHelper: function_call='auto' if times < self.config['functions_max_consecutive_calls'] else 'none', stream=stream ) - if function_name not in plugins_used: - plugins_used += (function_name,) return await self.__handle_function_call(chat_id, response, stream, times + 1, plugins_used) async def generate_image(self, prompt: str) -> tuple[str, str]: diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 2f37e78..e54a7d4 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,5 +1,7 @@ import json +from bot.plugins.dice import DicePlugin +from bot.plugins.youtube_audio_extractor import YouTubeAudioExtractorPlugin from plugins.ddg_image_search import DDGImageSearchPlugin from plugins.ddg_translate import DDGTranslatePlugin from plugins.spotify import SpotifyPlugin @@ -26,6 +28,8 @@ class PluginManager: 'ddg_image_search': DDGImageSearchPlugin, 'spotify': SpotifyPlugin, 'worldtimeapi': WorldTimeApiPlugin, + 'youtube_audio_extractor': YouTubeAudioExtractorPlugin, + 'dice': DicePlugin, } self.plugins = [plugin_mapping[plugin]() for plugin in enabled_plugins if plugin in plugin_mapping] diff --git a/bot/plugins/ddg_image_search.py b/bot/plugins/ddg_image_search.py index 8ef96de..6191824 100644 --- a/bot/plugins/ddg_image_search.py +++ b/bot/plugins/ddg_image_search.py @@ -1,3 +1,5 @@ +import os +import random from itertools import islice from typing import Dict @@ -10,6 +12,9 @@ class DDGImageSearchPlugin(Plugin): """ A plugin to search images and GIFs for a given query, using DuckDuckGo """ + def __init__(self): + self.safesearch = os.getenv('DUCKDUCKGO_SAFESEARCH', 'moderate') + def get_source_name(self) -> str: return "DuckDuckGo Images" @@ -25,19 +30,45 @@ class DDGImageSearchPlugin(Plugin): "type": "string", "enum": ["photo", "gif"], "description": "The type of image to search for. Default to `photo` if not specified", + }, + "region": { + "type": "string", + "enum": ['xa-ar', 'xa-en', 'ar-es', 'au-en', 'at-de', 'be-fr', 'be-nl', 'br-pt', 'bg-bg', + 'ca-en', 'ca-fr', 'ct-ca', 'cl-es', 'cn-zh', 'co-es', 'hr-hr', 'cz-cs', 'dk-da', + 'ee-et', 'fi-fi', 'fr-fr', 'de-de', 'gr-el', 'hk-tzh', 'hu-hu', 'in-en', 'id-id', + 'id-en', 'ie-en', 'il-he', 'it-it', 'jp-jp', 'kr-kr', 'lv-lv', 'lt-lt', 'xl-es', + 'my-ms', 'my-en', 'mx-es', 'nl-nl', 'nz-en', 'no-no', 'pe-es', 'ph-en', 'ph-tl', + 'pl-pl', 'pt-pt', 'ro-ro', 'ru-ru', 'sg-en', 'sk-sk', 'sl-sl', 'za-en', 'es-es', + 'se-sv', 'ch-de', 'ch-fr', 'ch-it', 'tw-tzh', 'th-th', 'tr-tr', 'ua-uk', 'uk-en', + 'us-en', 'ue-es', 've-es', 'vn-vi', 'wt-wt'], + "description": "The region to use for the search. Infer this from the language used for the" + "query. Default to `wt-wt` if not specified", } }, - "required": ["query", "type"], + "required": ["query", "type", "region"], }, }] async def execute(self, function_name, **kwargs) -> Dict: with DDGS() as ddgs: + image_type = kwargs.get('type', 'photo') ddgs_images_gen = ddgs.images( kwargs['query'], - region="wt-wt", - safesearch='off', - type_image=kwargs.get('type', 'photo'), + region=kwargs.get('region', 'wt-wt'), + safesearch=self.safesearch, + type_image=image_type, ) - results = list(islice(ddgs_images_gen, 1)) - return {"result": results[0]["image"]} + results = list(islice(ddgs_images_gen, 10)) + if not results or len(results) == 0: + return {"result": "No results found"} + + # Shuffle the results to avoid always returning the same image + random.shuffle(results) + + return { + 'direct_result': { + 'kind': image_type, + 'format': 'url', + 'value': results[0]['image'] + } + } diff --git a/bot/plugins/ddg_web_search.py b/bot/plugins/ddg_web_search.py index 6cc84ae..fbd3d78 100644 --- a/bot/plugins/ddg_web_search.py +++ b/bot/plugins/ddg_web_search.py @@ -1,3 +1,4 @@ +import os from itertools import islice from typing import Dict @@ -10,6 +11,8 @@ class DDGWebSearchPlugin(Plugin): """ A plugin to search the web for a given query, using DuckDuckGo """ + def __init__(self): + self.safesearch = os.getenv('DUCKDUCKGO_SAFESEARCH', 'moderate') def get_source_name(self) -> str: return "DuckDuckGo" @@ -24,9 +27,22 @@ class DDGWebSearchPlugin(Plugin): "query": { "type": "string", "description": "the user query" + }, + "region": { + "type": "string", + "enum": ['xa-ar', 'xa-en', 'ar-es', 'au-en', 'at-de', 'be-fr', 'be-nl', 'br-pt', 'bg-bg', + 'ca-en', 'ca-fr', 'ct-ca', 'cl-es', 'cn-zh', 'co-es', 'hr-hr', 'cz-cs', 'dk-da', + 'ee-et', 'fi-fi', 'fr-fr', 'de-de', 'gr-el', 'hk-tzh', 'hu-hu', 'in-en', 'id-id', + 'id-en', 'ie-en', 'il-he', 'it-it', 'jp-jp', 'kr-kr', 'lv-lv', 'lt-lt', 'xl-es', + 'my-ms', 'my-en', 'mx-es', 'nl-nl', 'nz-en', 'no-no', 'pe-es', 'ph-en', 'ph-tl', + 'pl-pl', 'pt-pt', 'ro-ro', 'ru-ru', 'sg-en', 'sk-sk', 'sl-sl', 'za-en', 'es-es', + 'se-sv', 'ch-de', 'ch-fr', 'ch-it', 'tw-tzh', 'th-th', 'tr-tr', 'ua-uk', 'uk-en', + 'us-en', 'ue-es', 've-es', 'vn-vi', 'wt-wt'], + "description": "The region to use for the search. Infer this from the language used for the" + "query. Default to `wt-wt` if not specified", } }, - "required": ["query"], + "required": ["query", "region"], }, }] @@ -34,8 +50,8 @@ class DDGWebSearchPlugin(Plugin): with DDGS() as ddgs: ddgs_gen = ddgs.text( kwargs['query'], - region='wt-wt', - safesearch='off' + region=kwargs.get('region', 'wt-wt'), + safesearch=self.safesearch ) results = list(islice(ddgs_gen, 3)) diff --git a/bot/plugins/dice.py b/bot/plugins/dice.py new file mode 100644 index 0000000..1f2a090 --- /dev/null +++ b/bot/plugins/dice.py @@ -0,0 +1,38 @@ +from typing import Dict + +from .plugin import Plugin + + +class DicePlugin(Plugin): + """ + A plugin to send a die in the chat + """ + def get_source_name(self) -> str: + return "Dice" + + def get_spec(self) -> [Dict]: + return [{ + "name": "send_dice", + "description": "Send a dice in the chat, with a random number between 1 and 6", + "parameters": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "enum": ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"], + "description": "Emoji on which the dice throw animation is based." + "Dice can have values 1-6 for “🎲”, “🎯” and “🎳”, values 1-5 for “🏀” " + "and “⚽”, and values 1-64 for “🎰”. Defaults to “🎲”.", + } + }, + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + return { + 'direct_result': { + 'kind': 'dice', + 'format': 'dice', + 'value': kwargs.get('emoji', '🎲') + } + } diff --git a/bot/plugins/worldtimeapi.py b/bot/plugins/worldtimeapi.py index 9c86658..9ec15d7 100644 --- a/bot/plugins/worldtimeapi.py +++ b/bot/plugins/worldtimeapi.py @@ -9,7 +9,6 @@ class WorldTimeApiPlugin(Plugin): """ A plugin to get the current time from a given timezone, using WorldTimeAPI """ - def __init__(self): default_timezone = os.getenv('WORLDTIME_DEFAULT_TIMEZONE') if not default_timezone: diff --git a/bot/plugins/youtube_audio_extractor.py b/bot/plugins/youtube_audio_extractor.py new file mode 100644 index 0000000..5d61fd5 --- /dev/null +++ b/bot/plugins/youtube_audio_extractor.py @@ -0,0 +1,44 @@ +from typing import Dict + +from pytube import YouTube + +from .plugin import Plugin + + +class YouTubeAudioExtractorPlugin(Plugin): + """ + A plugin to extract audio from a YouTube video + """ + + def get_source_name(self) -> str: + return "YouTube Audio Extractor" + + def get_spec(self) -> [Dict]: + return [{ + "name": "extract_youtube_audio", + "description": "Extract audio from a YouTube video", + "parameters": { + "type": "object", + "properties": { + "youtube_link": {"type": "string", "description": "YouTube video link to extract audio from"} + }, + "required": ["youtube_link"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + link = kwargs['youtube_link'] + try: + video = YouTube(link) + audio = video.streams.filter(only_audio=True, file_extension='mp4').first() + output = video.title + '.mp4' + audio.download(filename=output) + return { + 'direct_result': { + 'kind': 'file', + 'format': 'path', + 'value': output + } + } + except: + return {'result': 'Failed to extract audio'} diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index 5385977..0d4266e 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -16,7 +16,8 @@ from pydub import AudioSegment from utils import is_group_chat, get_thread_id, message_text, wrap_with_indicator, split_into_chunks, \ edit_message_with_retry, get_stream_cutoff_values, is_allowed, get_remaining_budget, is_admin, is_within_budget, \ - get_reply_to_message_id, add_chat_request_to_usage_tracker, error_handler + get_reply_to_message_id, add_chat_request_to_usage_tracker, error_handler, is_direct_result, handle_direct_result, \ + cleanup_intermediate_files from openai_helper import OpenAIHelper, localized_text from usage_tracker import UsageTracker @@ -401,6 +402,9 @@ class ChatGPTTelegramBot: stream_chunk = 0 async for content, tokens in stream_response: + if is_direct_result(content): + return await handle_direct_result(self.config, update, content) + if len(content.strip()) == 0: continue @@ -472,6 +476,9 @@ class ChatGPTTelegramBot: nonlocal total_tokens response, total_tokens = await self.openai.get_chat_response(chat_id=chat_id, query=prompt) + if is_direct_result(response): + return await handle_direct_result(self.config, update, response) + # Split into chunks of 4096 characters (Telegram's message limit) chunks = split_into_chunks(response) @@ -585,12 +592,21 @@ class ChatGPTTelegramBot: is_inline=True) return + unavailable_message = localized_text("function_unavailable_in_inline_mode", bot_language) if self.config['stream']: stream_response = self.openai.get_chat_response_stream(chat_id=user_id, query=query) i = 0 prev = '' backoff = 0 async for content, tokens in stream_response: + if is_direct_result(content): + cleanup_intermediate_files(content) + await edit_message_with_retry(context, chat_id=None, + message_id=inline_message_id, + text=f'{query}\n\n_{answer_tr}:_\n{unavailable_message}', + is_inline=True) + return + if len(content.strip()) == 0: continue @@ -648,6 +664,14 @@ class ChatGPTTelegramBot: logging.info(f'Generating response for inline query by {name}') response, total_tokens = await self.openai.get_chat_response(chat_id=user_id, query=query) + if is_direct_result(response): + cleanup_intermediate_files(response) + await edit_message_with_retry(context, chat_id=None, + message_id=inline_message_id, + text=f'{query}\n\n_{answer_tr}:_\n{unavailable_message}', + is_inline=True) + return + text_content = f'{query}\n\n_{answer_tr}:_\n{response}' # We only want to send the first 4096 characters. No chunking allowed in inline mode. diff --git a/bot/utils.py b/bot/utils.py index 7dafc12..8220325 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -2,7 +2,9 @@ from __future__ import annotations import asyncio import itertools +import json import logging +import os import telegram from telegram import Message, MessageEntity, Update, ChatMember, constants @@ -173,6 +175,7 @@ async def is_allowed(config, update: Update, context: CallbackContext, is_inline f'(id: {user_id}) are not allowed') return False + def is_admin(config, user_id: int, log_no_admin=False) -> bool: """ Checks if the user is the admin of the bot. @@ -284,6 +287,9 @@ def add_chat_request_to_usage_tracker(usage, config, user_id, used_tokens): :param used_tokens: The number of tokens used """ try: + if int(used_tokens) == 0: + logging.warning('No tokens used. Not adding chat request to usage tracker.') + return # add chat request to users usage tracker usage[user_id].add_chat_tokens(used_tokens, config['token_price']) # add guest chat request to guest usage tracker @@ -305,3 +311,66 @@ def get_reply_to_message_id(config, update: Update): if config['enable_quoting'] or is_group_chat(update): return update.message.message_id return None + + +def is_direct_result(response: any) -> bool: + """ + Checks if the dict contains a direct result that can be sent directly to the user + :param response: The response value + :return: Boolean indicating if the result is a direct result + """ + if type(response) is not dict: + try: + json_response = json.loads(response) + return json_response.get('direct_result', False) + except: + return False + else: + return response.get('direct_result', False) + + +async def handle_direct_result(config, update: Update, response: any): + """ + Handles a direct result from a plugin + """ + if type(response) is not dict: + response = json.loads(response) + + result = response['direct_result'] + kind = result['kind'] + format = result['format'] + value = result['value'] + + common_args = { + 'message_thread_id': get_thread_id(update), + 'reply_to_message_id': get_reply_to_message_id(config, update), + } + + if kind == 'photo': + if format == 'url': + await update.effective_message.reply_photo(**common_args, photo=value) + elif kind == 'gif': + if format == 'url': + await update.effective_message.reply_document(**common_args, document=value) + elif kind == 'file': + if format == 'path': + await update.effective_message.reply_document(**common_args, document=open(value, 'rb')) + cleanup_intermediate_files(response) + elif kind == 'dice': + await update.effective_message.reply_dice(**common_args, emoji=value) + + +def cleanup_intermediate_files(response: any): + """ + Deletes intermediate files created by plugins + """ + if type(response) is not dict: + response = json.loads(response) + + result = response['direct_result'] + kind = result['kind'] + format = result['format'] + value = result['value'] + + if kind == 'file' and format == 'path': + os.remove(value) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7070ed3..bb30eed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ requests~=2.31.0 tenacity==8.2.2 wolframalpha==5.0.0 duckduckgo_search==3.8.3 -spotipy==2.23.0 \ No newline at end of file +spotipy==2.23.0 +pytube==15.0.0 \ No newline at end of file diff --git a/translations.json b/translations.json index 35ad1ac..ce7c299 100644 --- a/translations.json +++ b/translations.json @@ -39,7 +39,8 @@ "try_again":"Please try again in a while", "answer_with_chatgpt":"Answer with ChatGPT", "ask_chatgpt":"Ask ChatGPT", - "loading":"Loading..." + "loading":"Loading...", + "function_unavailable_in_inline_mode": "This function is unavailable in inline mode" }, "es": { "help_description":"Muestra el mensaje de ayuda", @@ -81,9 +82,10 @@ "try_again":"Por favor, inténtalo de nuevo más tarde", "answer_with_chatgpt":"Responder con ChatGPT", "ask_chatgpt":"Preguntar a ChatGPT", - "loading":"Cargando..." + "loading":"Cargando...", + "function_unavailable_in_inline_mode": "Esta función no está disponible en el modo inline" }, - "pt-br":{ + "pt-br": { "help_description": "Mostra a mensagem de ajuda", "reset_description": "Redefine a conversa. Opcionalmente, passe instruções de alto nível (por exemplo, /reset Você é um assistente útil)", "image_description": "Gera uma imagem a partir do prompt (por exemplo, /image gato)", @@ -123,7 +125,9 @@ "try_again": "Por favor, tente novamente mais tarde", "answer_with_chatgpt": "Responder com ChatGPT", "ask_chatgpt": "Perguntar ao ChatGPT", - "loading": "Carregando..."}, + "loading": "Carregando...", + "function_unavailable_in_inline_mode": "Esta função não está disponível no modo inline" + }, "de": { "help_description":"Zeige die Hilfenachricht", "reset_description":"Setze die Konversation zurück. Optionale Eingabe einer grundlegenden Anweisung (z.B. /reset Du bist ein hilfreicher Assistent)", @@ -164,7 +168,8 @@ "try_again":"Bitte versuche es später erneut", "answer_with_chatgpt":"Antworte mit ChatGPT", "ask_chatgpt":"Frage ChatGPT", - "loading":"Lade..." + "loading":"Lade...", + "function_unavailable_in_inline_mode": "Diese Funktion ist im Inline-Modus nicht verfügbar" }, "fi": { "help_description":"Näytä ohjeet", @@ -206,7 +211,8 @@ "try_again":"Yritä myöhemmin uudelleen", "answer_with_chatgpt":"Vastaa ChatGPT:n avulla", "ask_chatgpt":"Kysy ChatGPT:ltä", - "loading":"Lataa..." + "loading":"Lataa...", + "function_unavailable_in_inline_mode": "Tämä toiminto ei ole käytettävissä sisäisessä tilassa" }, "ru": { "help_description":"Показать справочное сообщение", @@ -248,7 +254,8 @@ "try_again":"Пожалуйста, повторите попытку позже", "answer_with_chatgpt":"Ответить с помощью ChatGPT", "ask_chatgpt":"Спросить ChatGPT", - "loading":"Загрузка..." + "loading":"Загрузка...", + "function_unavailable_in_inline_mode": "Эта функция недоступна в режиме inline" }, "tr": { "help_description":"Yardım mesajını göster", @@ -290,7 +297,8 @@ "try_again":"Lütfen birazdan tekrar deneyiniz", "answer_with_chatgpt":"ChatGPT ile cevapla", "ask_chatgpt":"ChatGPT'ye sor", - "loading":"Yükleniyor..." + "loading":"Yükleniyor...", + "function_unavailable_in_inline_mode": "Bu işlev inline modda kullanılamaz" }, "it": { "help_description":"Mostra il messaggio di aiuto", @@ -332,7 +340,8 @@ "try_again":"Riprova più tardi", "answer_with_chatgpt":"Rispondi con ChatGPT", "ask_chatgpt":"Chiedi a ChatGPT", - "loading":"Carico..." + "loading":"Carico...", + "function_unavailable_in_inline_mode": "Questa funzione non è disponibile in modalità inline" }, "id": { "help_description": "Menampilkan pesan bantuan", @@ -374,7 +383,8 @@ "try_again": "Silakan coba lagi nanti", "answer_with_chatgpt": "Jawaban dengan ChatGPT", "ask_chatgpt": "Tanya ChatGPT", - "loading": "Sedang memuat..." + "loading": "Sedang memuat...", + "function_unavailable_in_inline_mode": "Fungsi ini tidak tersedia dalam mode inline" }, "nl": { "help_description":"Toon uitleg", @@ -416,7 +426,8 @@ "try_again":"Probeer het a.u.b. later opnieuw", "answer_with_chatgpt":"Antwoord met ChatGPT", "ask_chatgpt":"Vraag ChatGPT", - "loading":"Laden..." + "loading":"Laden...", + "function_unavailable_in_inline_mode": "Deze functie is niet beschikbaar in de inline modus" }, "zh-cn": { "help_description":"显示帮助信息", @@ -458,7 +469,8 @@ "try_again":"请稍后再试", "answer_with_chatgpt":"使用ChatGPT回答", "ask_chatgpt":"询问ChatGPT", - "loading":"载入中..." + "loading":"载入中...", + "function_unavailable_in_inline_mode": "此功能在内联模式下不可用" }, "zh-tw": { "help_description":"顯示幫助訊息", @@ -500,7 +512,8 @@ "try_again":"請稍後重試", "answer_with_chatgpt":"使用 ChatGPT 回答", "ask_chatgpt":"詢問 ChatGPT", - "loading":"載入中…" + "loading":"載入中…", + "function_unavailable_in_inline_mode": "此功能在內嵌模式下不可用" }, "vi": { "help_description":"Hiển thị trợ giúp", @@ -542,7 +555,8 @@ "try_again":"Vui lòng thử lại sau một lúc", "answer_with_chatgpt":"Trả lời với ChatGPT", "ask_chatgpt":"Hỏi ChatGPT", - "loading":"Đang tải..." + "loading":"Đang tải...", + "function_unavailable_in_inline_mode": "Chức năng này không khả dụng trong chế độ nội tuyến" }, "fa": { "help_description":"نمایش پیغام راهنما", @@ -584,7 +598,8 @@ "try_again":"لطفا بعد از مدتی دوباره امتحان کنید", "answer_with_chatgpt":"با ChatGPT پاسخ دهید", "ask_chatgpt":"از ChatGPT بپرسید", - "loading":"در حال بارگذاری..." + "loading":"در حال بارگذاری...", + "function_unavailable_in_inline_mode": "این عملکرد در حالت آنلاین در دسترس نیست" }, "uk": { "help_description":"Показати повідомлення допомоги", @@ -626,6 +641,7 @@ "try_again":"Будь ласка, спробуйте знову через деякий час", "answer_with_chatgpt":"Відповідь за допомогою ChatGPT", "ask_chatgpt":"Запитати ChatGPT", - "loading":"Завантаження..." + "loading":"Завантаження...", + "function_unavailable_in_inline_mode": "Ця функція недоступна в режимі Inline" } }