@@ -79,6 +79,7 @@ TOOLS = [ 'tools.apps.PublicAPIConfig', 'tools.apps.ChatsConfig', 'tools.apps.MediaConfig', + 'tools.apps.ShareConfig' ] @@ -274,6 +275,10 @@ CELERY_BEAT_SCHEDULE = { 'task': 'payments.tasks.send_low_balance_message', 'schedule': crontab(0, 8), }, + 'delete_expired_shares': { + 'task': 'tools.share.tasks.delete_expired_shares', + 'schedule': crontab('0', '*/6', '*', '*', '*'), + }, 'execute_recurring_payments': { 'task': 'payments.tasks.execute_recurring_payments', 'schedule': crontab(*env.list('RECURRING_PAYMENT_CRONTAB_SCHEDULE', [])), @@ -460,7 +465,7 @@ if (SENTRY_URL := env.str('SENTRY_URL', '')) and RELEASE and ENVIRONMENT: 'PromptLengthExceeded', 'InvalidParameterError', 'UnsupportedSize', - 'OutputSensitiveImageContentError' + 'OutputSensitiveImageContentError', ], ) @@ -474,6 +479,7 @@ if CACHEOPS_REDIS: 'ml_model.*': {'ops': 'all', 'timeout': 60 * 60}, 'tools.chats.*': {'ops': 'all', 'timeout': 60 * 60}, 'tools.media.*': {'ops': 'all', 'timeout': 60 * 60}, + 'tools.share.*': {'ops': 'all', 'timeout': 60 * 60}, 'payments.paymentplan': {'ops': 'all', 'timeout': 60 * 60}, 'payments.invoice': {'ops': 'all', 'timeout': 60 * 60 * 24 * 7}, 'messages.*': {'ops': 'all', 'timeout': 60 * 60}, @@ -496,3 +502,6 @@ RECURRING_FAILED_CHARGE_EMAIL_DAYS = env.list('RECURRING_FAILED_CHARGE_EMAIL_DAY # SSE STREAMING FF__STREAMING_ENABLED = env.bool('FF__STREAMING_ENABLED', False) + +# MESSAGE SHARING +SHARE_LIFETIME = env.int('SHARE_LIFETIME', 1800) \ No newline at end of file @@ -30,6 +30,7 @@ public_api = NinjaAPI( api.add_router('users/', 'users.routes.v1.router') api.add_router('chats/', 'tools.chats.routes.v1.router') api.add_router('media/', 'tools.media.routes.v1.router') +api.add_router('share/', 'tools.share.routes.v1.router') compatibility_api.add_router('auth/', 'authentication.routes.v1.router') compatibility_api.add_router('payments/', 'payments.routes.v1.router') @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-20 11:08+0300\n" +"POT-Creation-Date: 2026-07-30 17:11+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -116,7 +116,7 @@ msgstr "Email токен не найден" msgid "Wrong email" msgstr "Неверный email" -#: authentication/exceptions/user.py:11 backend/urls.py:97 +#: authentication/exceptions/user.py:11 backend/urls.py:98 msgid "Wrong password" msgstr "Неверный пароль" @@ -205,8 +205,8 @@ msgstr "Бизнес Группы" #: authentication/models/business_host.py:22 #: authentication/models/email_token.py:13 authentication/models/user.py:233 #: authentication/models/user.py:234 authentication/models/user_telegram.py:22 -#: authentication/models/user_vk.py:12 payments/admin.py:37 -#: payments/admin.py:124 payments/models/invoice.py:15 +#: authentication/models/user_vk.py:12 payments/admin.py:38 +#: payments/admin.py:167 payments/models/invoice.py:15 #: payments/models/payment.py:26 payments/models/payment_plan.py:43 #: tools/media/models.py:108 msgid "User" @@ -383,7 +383,7 @@ msgstr "Имя" msgid "Last name" msgstr "Фамилия" -#: authentication/models/user.py:133 +#: authentication/models/user.py:133 payments/admin.py:131 msgid "Email" msgstr "Email" @@ -515,27 +515,27 @@ msgstr "Аккаунт уже подтвержден" msgid "Could not confirm email, please try again." msgstr "Невозможно подтвердить email, попробуйте позже" -#: authentication/security.py:34 +#: authentication/security.py:36 #, fuzzy #| msgid "Hidden" msgid "Forbidden" msgstr "Скрытый" -#: authentication/security.py:47 +#: authentication/security.py:49 msgid "Access token is expired" msgstr "Срок действия токена доступа истек" -#: authentication/security.py:49 +#: authentication/security.py:51 #, fuzzy #| msgid "Access token is expired" msgid "Access token invalid" msgstr "Срок действия токена доступа истек" -#: authentication/security.py:62 +#: authentication/security.py:79 msgid "User not found" msgstr "Пользователь не найден" -#: authentication/security.py:97 +#: authentication/security.py:114 msgid "Access token expired or does not exist" msgstr "Токен доступа просрочен или не существует" @@ -549,7 +549,7 @@ msgstr "" msgid "Host user is not registered for this account" msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" -#: authentication/selectors/user_selector.py:75 +#: authentication/selectors/user_selector.py:77 msgid "No user with this uid found" msgstr "Не найден пользователь с данным ID" @@ -565,23 +565,23 @@ msgstr "Приглашенный аккаунт может принять или msgid "Error occured when proceed email sending" msgstr "Случилась ошибка во время отправки email" -#: authentication/services/user_services.py:167 +#: authentication/services/user_services.py:169 msgid "No user like this in a database" msgstr "Такой пользователь отсутствует" -#: authentication/services/user_services.py:184 +#: authentication/services/user_services.py:186 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:208 +#: authentication/services/user_services.py:210 msgid "No email token provided" msgstr "Токен не получен" -#: authentication/services/user_services.py:218 +#: authentication/services/user_services.py:220 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: authentication/services/user_services.py:256 +#: authentication/services/user_services.py:258 msgid "Current password is wrong" msgstr "Текущий пароль неверен" @@ -610,15 +610,15 @@ msgstr "Повторное приглашение сотруднику успе msgid "Business account password has been updated" msgstr "Пароль сотрудника успешно обновлен" -#: backend/urls.py:79 +#: backend/urls.py:80 msgid "Requested object does not exists" msgstr "" -#: backend/urls.py:86 +#: backend/urls.py:87 msgid "Data size limit exceeded. Please reduce the size" msgstr "Превышен лимит размера данных. Уменьшите размер" -#: backend/urls.py:92 +#: backend/urls.py:93 msgid "Token is invalid" msgstr "" @@ -638,7 +638,7 @@ msgstr "Уже существует %(model)s с полями %(fields)s" msgid "Invalid JSON payload" msgstr "Некорректные данные в поле info" -#: messages/serializers.py:44 ml_model/exceptions.py:86 +#: messages/serializers.py:44 ml_model/exceptions.py:87 #: tools/chats/schemas.py:23 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" @@ -653,16 +653,16 @@ msgstr "Версия %(version)s уже имеет входные данные msgid "Neuron Models" msgstr "Нейронные Модели" -#: ml_model/exceptions.py:20 +#: ml_model/exceptions.py:21 msgid "The model is currently disabled. Please try again later." msgstr "" "Модель в настоящее время неактивна. Пожалуйста, повторите попытку позже." -#: ml_model/exceptions.py:25 +#: ml_model/exceptions.py:26 msgid "Your request was blocked by our moderation system" msgstr "Ваш запрос был заблокирован нашей системой модерации" -#: ml_model/exceptions.py:35 +#: ml_model/exceptions.py:36 #, python-format msgid "" "Image size %(cw)dx%(ch)d is not supported. Please rotate image to " @@ -671,25 +671,25 @@ msgstr "" "Размер изображения %(cw)dx%(ch)d не поддерживается. Пожалуйста, переверните " "до %(rw)dx%(rh)d" -#: ml_model/exceptions.py:38 +#: ml_model/exceptions.py:39 #, python-format msgid "Image size %(cw)sx%(ch)s is not supported. Required size: %(rw)sx%(rh)s" msgstr "" "Размер изображения %(cw)sx%(ch)s не поддерживается. Требуемый размер: " "%(rw)sx%(rh)s" -#: ml_model/exceptions.py:47 +#: ml_model/exceptions.py:48 #, python-format msgid "Image exceeds the maximum allowed pixel count (%(max_pixels)d)." msgstr "" "Размер изображения превышает максимально допустимое количество пикселей " "(%(max_pixels)d)." -#: ml_model/exceptions.py:53 +#: ml_model/exceptions.py:54 msgid "The model is not responding" msgstr "Модель не отвечает" -#: ml_model/exceptions.py:62 +#: ml_model/exceptions.py:63 #, python-format msgid "" "The attached file format is not supported. Available formats: " @@ -698,96 +698,96 @@ msgstr "" "Формат вложенного файла не поддерживается. Доступные форматы: " "%(available_extensions)s." -#: ml_model/exceptions.py:68 +#: ml_model/exceptions.py:69 msgid "The file may be corrupted. Please try another one." msgstr "Возможно, файл повреждён. Попробуйте загрузить другой файл." -#: ml_model/exceptions.py:73 +#: ml_model/exceptions.py:74 msgid "File Uploading Not supported" msgstr "Загрузка файлов не поддерживается" -#: ml_model/exceptions.py:78 +#: ml_model/exceptions.py:79 msgid "Unable to recognize the file" msgstr "Не удаётся распознать файл" -#: ml_model/exceptions.py:91 +#: ml_model/exceptions.py:92 msgid "The length of the context has been exceeded." msgstr "Длина контекста превышена." -#: ml_model/exceptions.py:96 +#: ml_model/exceptions.py:97 msgid "Jinja template not found" msgstr "Jinja-шаблон не найден" -#: ml_model/exceptions.py:101 +#: ml_model/exceptions.py:102 msgid "There was an unknown error while rendering a template" msgstr "При рендеринге шаблона произошла неизвестная ошибка" -#: ml_model/exceptions.py:106 +#: ml_model/exceptions.py:107 msgid "The neuron model does not exist" msgstr "Нейронная модель не существует" -#: ml_model/exceptions.py:114 +#: ml_model/exceptions.py:115 #, python-format msgid "The %(file_type)s is not attached" msgstr "Файл (%(file_type)s) не прикреплен" -#: ml_model/exceptions.py:119 +#: ml_model/exceptions.py:120 msgid "No image content found in response. Try a different request" msgstr "В промпте отсутствует описание изображения. Попробуйте другой запрос" -#: ml_model/exceptions.py:124 +#: ml_model/exceptions.py:125 msgid "" "The model could not analyze your request. Please rephrase it and try again" msgstr "" "Модель не смогла проанализировать ваш запрос. Перефразируйте его и " "попробуйте снова" -#: ml_model/exceptions.py:129 +#: ml_model/exceptions.py:130 msgid "Image analysis error. Please try another image." msgstr "Ошибка анализа изображения. Попробуйте другую картинку." -#: ml_model/exceptions.py:134 +#: ml_model/exceptions.py:135 msgid "Use style type AUTO or GENERAL when a style preset is selected" msgstr "При выбранном стиле используйте тип стиля AUTO или GENERAL" -#: ml_model/exceptions.py:139 +#: ml_model/exceptions.py:140 msgid "Prediction interrupted. Please retry again" msgstr "Генерация прервана. Пожалуйста, повторите попытку еще раз" -#: ml_model/exceptions.py:154 +#: ml_model/exceptions.py:155 #, python-format msgid "Prompt is too long. Maximum length is %(max_length)s characters." msgstr "Промпт слишком длинный. Максимальная длина — %(max_length)s символов." -#: ml_model/exceptions.py:161 +#: ml_model/exceptions.py:162 msgid "" "Service is currently unavailable due to high demand. Please try again later" msgstr "" "Сервис временно недоступен из-за высокой нагрузки. Пожалуйста, попробуйте " "позже" -#: ml_model/exceptions.py:169 +#: ml_model/exceptions.py:170 #, python-format msgid "%(feature)s is available only in paid plan." msgstr "%(feature)s доступно только в платном тарифном плане." -#: ml_model/exceptions.py:176 +#: ml_model/exceptions.py:177 msgid "Face not found in the image. Please try another image with a face." msgstr "Не найдено лицо на картинке. Попробуйте другую картинку с лицом." -#: ml_model/exceptions.py:181 +#: ml_model/exceptions.py:182 msgid "The input image may contain real person." msgstr "Загруженное изображение может содержать реального человека." -#: ml_model/exceptions.py:186 +#: ml_model/exceptions.py:187 msgid "The generated image may contain private or prohibited content" msgstr "Готовое изображение может содержать приватный или запрещённый контент" -#: ml_model/exceptions.py:195 +#: ml_model/exceptions.py:196 msgid "not specified" msgstr "не указана" -#: ml_model/exceptions.py:197 +#: ml_model/exceptions.py:198 #, python-format msgid "" "Version \"%(version)s\" is not available. Available versions: " @@ -853,7 +853,7 @@ msgstr "Теги" msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:166 ml_model/models.py:413 payments/admin.py:130 +#: ml_model/models.py:166 ml_model/models.py:413 payments/admin.py:173 msgid "Model" msgstr "Модель" @@ -1082,26 +1082,26 @@ msgstr "Инструкция Модели" msgid "Model Instructions" msgstr "Инструкции Моделей" -#: ml_model/selectors/ml_models_selector.py:114 +#: ml_model/selectors/ml_models_selector.py:116 msgid "no model by this id" msgstr "Не найдено моделей по этому ID" -#: ml_model/services/FileService.py:110 tools/media/apis.py:280 +#: ml_model/services/FileService.py:110 tools/media/apis.py:295 #: tools/public_api/views/ml_service.py:56 #: tools/public_api/views/providers/openai_compatible.py:208 msgid "Voice not found." msgstr "Голос не найден." -#: ml_model/services/chatgpt.py:244 +#: ml_model/services/chatgpt.py:245 msgid "Image is ready" msgstr "Изображение готово" -#: ml_model/services/chatgpt.py:360 ml_model/services/claude.py:266 -#: ml_model/services/grok.py:190 +#: ml_model/services/chatgpt.py:361 ml_model/services/claude.py:277 +#: ml_model/services/grok.py:191 msgid "File analysis" msgstr "Анализ файлов" -#: ml_model/services/chatgpt.py:382 ml_model/services/chatgpt_5.py:133 +#: ml_model/services/chatgpt.py:383 ml_model/services/chatgpt_5.py:133 msgid "The \"Use code\" option cannot be used together with an attached image." msgstr "" "Нельзя одновременно использовать параметр «Использовать код» вместе с " @@ -1132,20 +1132,32 @@ msgstr "Нет изображения для улучшения" msgid "Lyrics is too long" msgstr "Текст песни слишком длинный" +#: ml_model/validators.py:48 tools/public_api/routes/providers/openai.py:58 +msgid "The request must not be empty" +msgstr "Запрос не должен быть пустым" + #: ml_model/views.py:65 msgid "Model data cannot be retrieved" msgstr "Невозможно получить данные модели" -#: payments/admin.py:35 payments/admin.py:76 payments/admin.py:122 +#: payments/admin.py:36 payments/admin.py:85 payments/admin.py:165 msgid "You can search by user email, exacted company name" msgstr "" "Вы можете осуществлять поиск по e-mail пользователя, точному названию " "компании" -#: payments/admin.py:40 payments/admin.py:127 +#: payments/admin.py:41 payments/admin.py:170 msgid "Missing" msgstr "Отсутствующий" +#: payments/admin.py:144 payments/models/user_payment_method.py:23 +msgid "Gateway" +msgstr "Шлюз" + +#: payments/admin.py:148 payments/models/user_payment_method.py:24 +msgid "Payment method UID" +msgstr "UID платёжного метода" + #: payments/apps.py:11 payments/models/payment.py:60 msgid "Payments" msgstr "Платежи" @@ -1167,6 +1179,32 @@ msgstr "" msgid "The payer does not exist" msgstr "Плательщик не существует" +#: payments/models/attempt.py:12 payments/models/user_payment_method.py:48 +msgid "Payment Method" +msgstr "Платежный метод" + +#: payments/models/attempt.py:15 +#, fuzzy +#| msgid "Cancelled" +msgid "Cancel Reason" +msgstr "Отменено" + +#: payments/models/attempt.py:16 +msgid "In Cycle" +msgstr "" + +#: payments/models/attempt.py:22 +#, fuzzy +#| msgid "Payment Datetime" +msgid "Payment Attempt" +msgstr "Дата и время платежа" + +#: payments/models/attempt.py:23 +#, fuzzy +#| msgid "Payment Methods" +msgid "Payment Attempts" +msgstr "Платежные методы" + #: payments/models/invoice.py:23 msgid "Generative Model" msgstr "Генеративная модель" @@ -1232,19 +1270,15 @@ msgstr "Последнее время платежа" msgid "Next payment at" msgstr "Следующее время платежа" -#: payments/models/payment_plan.py:56 payments/models/user_payment_method.py:25 -msgid "Payment Method" -msgstr "Платежный метод" - -#: payments/models/payment_plan.py:62 +#: payments/models/payment_plan.py:54 msgid "Current balance" msgstr "Текущий баланс" -#: payments/models/payment_plan.py:68 +#: payments/models/payment_plan.py:60 msgid "Referral balance" msgstr "Реферальный баланс" -#: payments/models/payment_plan.py:89 payments/models/payment_plan.py:90 +#: payments/models/payment_plan.py:87 payments/models/payment_plan.py:88 msgid "User Balance" msgstr "Баланс пользователя" @@ -1316,63 +1350,67 @@ msgstr "Активация Промокода" msgid "Promocode Activations" msgstr "Активации Промокодов" -#: payments/models/user_payment_method.py:9 +#: payments/models/user_payment_method.py:10 msgid "Bank Card" msgstr "Банковская карта" -#: payments/models/user_payment_method.py:10 +#: payments/models/user_payment_method.py:11 msgid "Mir Pay" msgstr "Mir Pay" -#: payments/models/user_payment_method.py:11 +#: payments/models/user_payment_method.py:12 msgid "Sberbank" msgstr "Сбербанк" -#: payments/models/user_payment_method.py:12 +#: payments/models/user_payment_method.py:13 msgid "YooMoney" msgstr "ЮMoney" -#: payments/models/user_payment_method.py:13 +#: payments/models/user_payment_method.py:14 msgid "T-bank" msgstr "Т-банк" -#: payments/models/user_payment_method.py:14 +#: payments/models/user_payment_method.py:15 msgid "SBP" msgstr "СБП" -#: payments/models/user_payment_method.py:16 -msgid "Gateway" -msgstr "Шлюз" - -#: payments/models/user_payment_method.py:17 -msgid "Payment method UID" -msgstr "UID платёжного метода" +#: payments/models/user_payment_method.py:20 +#, fuzzy +#| msgid "User not found" +msgid "User Plan Info" +msgstr "Пользователь не найден" -#: payments/models/user_payment_method.py:18 tools/media/models.py:78 +#: payments/models/user_payment_method.py:25 tools/media/models.py:78 msgid "Meta" msgstr "Метаданные" -#: payments/models/user_payment_method.py:19 -msgid "Attempts" -msgstr "Попытки" - #: payments/models/user_payment_method.py:26 +#, fuzzy +#| msgid "Is active" +msgid "Active" +msgstr "Является активной" + +#: payments/models/user_payment_method.py:27 +msgid "Primary" +msgstr "" + +#: payments/models/user_payment_method.py:49 msgid "Payment Methods" msgstr "Платежные методы" -#: payments/routes/v1.py:93 +#: payments/routes/v1.py:113 msgid "You do not have an active subscription to cancel" msgstr "У вас нет активной подписки для отмены" -#: payments/routes/v1.py:94 +#: payments/routes/v1.py:114 msgid "The recurring payment is successfully cancelled" msgstr "Автоплатежи успешно отключены" -#: payments/routes/v1.py:144 +#: payments/routes/v1.py:164 msgid "Expenses" msgstr "Затраты" -#: payments/routes/v1.py:148 +#: payments/routes/v1.py:168 msgid "Refills" msgstr "Пополнения" @@ -1422,8 +1460,12 @@ msgstr "Публичный API" msgid "Media" msgstr "Медиа" -#: tools/chats/apis.py:201 tools/media/apis.py:229 -#: tools/public_api/views/base.py:102 +#: tools/apps.py:32 tools/share/models.py:17 +msgid "Share" +msgstr "Шеринг" + +#: tools/chats/apis.py:217 tools/media/apis.py:244 +#: tools/public_api/views/base.py:108 msgid "" "An unexpected generation error has occurred. Please try again later or use a " "different model" @@ -1431,7 +1473,7 @@ msgstr "" "Произошла непредвиденная ошибка при генерации. Пожалуйста попробуйте позже " "или используйте другую модель" -#: tools/chats/apis.py:257 +#: tools/chats/apis.py:273 msgid "The message has already been deleted" msgstr "Сообщение уже было удалено" @@ -1444,21 +1486,21 @@ msgstr "Чат %(id)s" msgid "Chat" msgstr "Чат" -#: tools/chats/routes/v1.py:37 tools/public_api/routes/v1.py:66 +#: tools/chats/routes/v1.py:39 tools/public_api/routes/v1.py:68 #, fuzzy #| msgid "User not found" msgid "Stream not found" msgstr "Пользователь не найден" -#: tools/chats/routes/v1.py:51 +#: tools/chats/routes/v1.py:53 msgid "Chat not found" msgstr "Чат не найден" -#: tools/chats/routes/v1.py:54 tools/public_api/routes/v1.py:106 +#: tools/chats/routes/v1.py:56 tools/public_api/routes/v1.py:108 msgid "Stream not supported for this model" msgstr "Стриминг не поддерживается для этой модели" -#: tools/chats/routes/v1.py:58 +#: tools/chats/routes/v1.py:60 msgid "Stream already in progress" msgstr "" @@ -1470,7 +1512,7 @@ msgstr "Некорректные данные в поле info" msgid "Stream timeout" msgstr "" -#: tools/media/apis.py:221 +#: tools/media/apis.py:236 msgid "" "Temporary issues with the service, we are already working on a solution." msgstr "Временные неполадки с сервисом, мы уже работаем над их решением." @@ -1568,11 +1610,6 @@ msgstr "Отсутствует обязательный параметр: 'model msgid "Invalid input payload" msgstr "Некорректные данные в поле info" -#: tools/public_api/routes/providers/openai.py:58 -#: tools/public_api/routes/v1.py:109 tools/public_api/views/base.py:75 -msgid "The request must not be empty" -msgstr "Запрос не должен быть пустым" - #: tools/public_api/routes/providers/openai.py:85 #: tools/public_api/views/providers/elevenlabs_compatible.py:147 #: tools/public_api/views/providers/openai_compatible.py:110 @@ -1599,41 +1636,41 @@ msgstr "" msgid "message_uuid is not provided" msgstr "" -#: tools/public_api/routes/v1.py:31 +#: tools/public_api/routes/v1.py:33 msgid "No API Key in Authorization header" msgstr "" -#: tools/public_api/routes/v1.py:43 +#: tools/public_api/routes/v1.py:45 #, fuzzy #| msgid "API Key not found" msgid "API key not found" msgstr "API-ключ не найден" -#: tools/public_api/routes/v1.py:46 +#: tools/public_api/routes/v1.py:48 #, fuzzy #| msgid "Access token is expired" msgid "API key expired" msgstr "Срок действия токена доступа истек" -#: tools/public_api/routes/v1.py:48 +#: tools/public_api/routes/v1.py:50 #, fuzzy #| msgid "Key limit exceeded" msgid "API key limit exceeded" msgstr "Превышен лимит по ключу" -#: tools/public_api/routes/v1.py:62 tools/public_api/routes/v1.py:96 +#: tools/public_api/routes/v1.py:64 tools/public_api/routes/v1.py:98 #, fuzzy #| msgid "Host user is not registered for this account" msgid "API key is not available for this account type" msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" -#: tools/public_api/routes/v1.py:104 tools/public_api/views/base.py:68 +#: tools/public_api/routes/v1.py:106 tools/public_api/views/base.py:69 msgid "Model is blocked by outdating or temporary block, please retry later" msgstr "" "Модель заблокирована, т.к закончила обновляться или временно заблокирована, " "попробуйте позже" -#: tools/public_api/views/base.py:63 +#: tools/public_api/views/base.py:64 msgid "Key limit exceeded" msgstr "Превышен лимит по ключу" @@ -1678,6 +1715,53 @@ msgstr "Название голоса успешно обновлено" msgid "Preset voices are shared and cannot be deleted. Use your own voice id." msgstr "Пресеты общие и не удаляются. Используйте id собственного голоса." +#: tools/share/exceptions.py:6 +msgid "Some messages are unavailable to you" +msgstr "Некоторые сообщения вам недоступны" + +#: tools/share/exceptions.py:11 +msgid "Link access can only include messages from one store" +msgstr "Можно открыть доступ по ссылке только к сообщениям из одного хранилища" + +#: tools/share/exceptions.py:16 +msgid "Link access is not available for this store type" +msgstr "Доступ по ссылке для этого типа хранилища недоступен" + +#: tools/share/exceptions.py:21 +msgid "Link access for this store can only include model messages" +msgstr "Для этого хранилища в ссылку можно добавить только сообщения от модели" + +#: tools/share/exceptions.py:26 +msgid "Not all messages were found. Some may have been deleted" +msgstr "Не все сообщения найдены. Возможно, часть уже удалена" + +#: tools/share/exceptions.py:31 +msgid "The link was not found or is no longer available" +msgstr "Ссылка не найдена или больше не действует" + +#: tools/share/models.py:8 +msgid "Code" +msgstr "Код" + +#: tools/share/models.py:9 +msgid "Messages" +msgstr "Сообщения" + +#: tools/share/models.py:10 +msgid "Created At" +msgstr "Создано" + +#: tools/share/models.py:11 +msgid "Expires At" +msgstr "Истекает" + +#: tools/share/models.py:18 +msgid "Shares" +msgstr "Шеринги" + +#~ msgid "Attempts" +#~ msgstr "Попытки" + #, python-format #~ msgid "This video duration is not allowed for %(quality)s quality." #~ msgstr "" @@ -1724,9 +1808,6 @@ msgstr "Пресеты общие и не удаляются. Используй #~ msgid "Is recurrent" #~ msgstr "Рекуррентный" -#~ msgid "Payment Datetime" -#~ msgstr "Дата и время платежа" - #~ msgid "Recurring Payment" #~ msgstr "Автоплатеж" @@ -1827,9 +1908,6 @@ msgstr "Пресеты общие и не удаляются. Используй #~ msgid "Token prefix is missing" #~ msgstr "Отсутствует префикс токена" -#~ msgid "Message" -#~ msgstr "Сообщение" - #~ msgid "Detail" #~ msgstr "Подробности" @@ -29,7 +29,11 @@ class PaymentMethodService: elif gateway == 'yoo_money': metadata = {'account_number': yookassa_payment_method.account_number} elif gateway == 'sbp': - metadata = {'sbp_operation_id': yookassa_payment_method.sbp_operation_id} + metadata = { + 'sbp_operation_id': yookassa_payment_method.sbp_operation_id, + 'bic': yookassa_payment_method.payer_bank_details.bic, + 'bank_id': yookassa_payment_method.payer_bank_details.bank_id, + } else: metadata = {} payment_method, created = PaymentMethod.objects.update_or_create( @@ -64,7 +68,7 @@ class PaymentMethodService: self.user.email, method.uid, cancel_reason, - method.total_attempts+1, + method.total_attempts + 1, ) return payment_attempt @@ -0,0 +1,30 @@ +# Generated by Django 5.0 on 2026-07-30 14:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('msgs', '0010_message_msgs_messag_object__09670f_idx'), + ] + + operations = [ + migrations.CreateModel( + name='Share', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=8, unique=True, verbose_name='Code')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('expires_at', models.DateTimeField(verbose_name='Expires At')), + ('messages', models.ManyToManyField(to='msgs.message', verbose_name='Messages')), + ], + options={ + 'verbose_name': 'Share', + 'verbose_name_plural': 'Shares', + 'ordering': ('expires_at',), + }, + ), + ] @@ -0,0 +1,40 @@ +from ninja import Router +from ninja.errors import HttpError + +from authentication.security import SyncAuthBearer +from tools.share.exceptions import ( + IncompleteSetError, + MultiStoreShareError, + OnlyModelMessagesShareError, + OwnershipError, + ShareDoesNotExist, + StoreShareError, +) +from tools.share.schemas import ShareResultSchema, ShareSchema, ShareMessagesSchema +from tools.share.services.share_service import ShareService + +router = Router(auth=SyncAuthBearer(), tags=['share']) + + +@router.post('/', tags=['share/'], response=ShareSchema) +def add_share_messages(request, payload: ShareMessagesSchema): + try: + share = ShareService(request.auth, list(set(payload.messages_uids))).share() + return ShareSchema(code=share.code, expires_at=share.expires_at) + except OwnershipError as exc: + raise HttpError(403, str(exc)) + except (MultiStoreShareError, StoreShareError, IncompleteSetError, OnlyModelMessagesShareError) as exc: + raise HttpError(400, str(exc)) + except Exception as exc: + raise HttpError(400, str(exc)) + + +@router.get('{code}/', auth=None, tags=['share/'], response=ShareResultSchema) +def list_share_messages(request, code: str): + try: + store_type, messages = ShareService.get_share_messages(code) + return ShareResultSchema(store_type=store_type, messages=messages) + except ShareDoesNotExist as exc: + raise HttpError(404, str(exc)) + except Exception as exc: + raise HttpError(400, str(exc)) @@ -0,0 +1,101 @@ +import base64 +import hashlib +from datetime import timedelta + +from uuid import UUID + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.db import transaction +from django.db.models import Prefetch +from django.utils import timezone + +from authentication.models import CustomUserModel +from core.service import BaseService +from messages.models import Message +from tools.chats.models import Chat +from tools.media.models import Gallery +from tools.public_api.models import APIStore +from tools.share.exceptions import ( + IncompleteSetError, + MultiStoreShareError, + OnlyModelMessagesShareError, + OwnershipError, + ShareDoesNotExist, + StoreShareError, +) +from tools.share.models import Share + + +class ShareService(BaseService): + SHARE_LIFETIME = settings.SHARE_LIFETIME + + def __init__(self, user: CustomUserModel, message_uids: list[UUID]) -> None: + super().__init__(user) + self.message_uids = message_uids + + def _generate_code(self): + return base64.urlsafe_b64encode( + hashlib.sha256((','.join(sorted(map(str, self.message_uids)))).encode()).digest() + ).decode()[:8] + + def _validate_share_messages(self) -> None: + """ + Проверяет, что все сообщения существуют, принадлежат одному стору текущего пользователя + и могут быть расшарены. Исключает сообщения из неподдерживаемых типов сторов. + """ + qs = Message.objects.filter(uid__in=self.message_uids, is_deleted=False) + raw_pairs = list(qs.values_list('content_type', 'object_id')) + if not raw_pairs or len(raw_pairs) != len(self.message_uids): + raise IncompleteSetError + pairs = set(raw_pairs) + if len(pairs) > 1: + raise MultiStoreShareError + for content_type, object_id in pairs: + model = ContentType.objects.get_for_id(content_type).model_class() + if model is APIStore: + raise StoreShareError + model_filter = {'pk': object_id, 'user': self.user} + if model is Chat: + model_filter['is_deleted'] = False + if not model.objects.filter(**model_filter).exists(): + raise OwnershipError + if issubclass(model, Gallery) and qs.filter(from_model=False).exists(): + raise OnlyModelMessagesShareError + + def _create_share(self, code: str) -> Share: + with transaction.atomic(): + share, created = Share.objects.update_or_create( + code=code, defaults=dict(expires_at=timezone.now() + timedelta(seconds=self.SHARE_LIFETIME)) + ) + if created: + share.messages.add(*self.message_uids) + return share + + def share(self) -> Share: + self._validate_share_messages() + code = self._generate_code() + share = self._create_share(code) + return share + + @classmethod + def get_share_messages(cls, code: str) -> tuple[str, list[Message]]: + share = ( + Share.objects.filter(code=code, expires_at__gt=timezone.now()) + .prefetch_related( + Prefetch( + 'messages', + queryset=Message.objects.filter(is_deleted=False).select_related('content_type'), + ) + ) + .first() + ) + if not share: + raise ShareDoesNotExist + + messages = list(share.messages.all()) + if not messages: + raise ShareDoesNotExist + + store_type = messages[0].content_type.model + return store_type, messages @@ -0,0 +1,22 @@ +from django.contrib import admin + +from tools.share.models import Share + + +class ShareMessageInline(admin.TabularInline): + model = Share.messages.through + extra = 0 + can_delete = False + readonly_fields = ('message',) + fields = ('message',) + + def has_add_permission(self, request, obj=None) -> bool: + return False + + +@admin.register(Share) +class ShareAdmin(admin.ModelAdmin): + list_display = ('code', 'created_at', 'expires_at') + search_fields = ('code',) + readonly_fields = ('code', 'created_at', 'expires_at') + inlines = [ShareMessageInline] @@ -0,0 +1,31 @@ +from django.utils.translation import gettext as _ + + +class OwnershipError(Exception): + def __str__(self) -> str: + return _('Some messages are unavailable to you') + + +class MultiStoreShareError(Exception): + def __str__(self) -> str: + return _('Link access can only include messages from one store') + + +class StoreShareError(Exception): + def __str__(self) -> str: + return _('Link access is not available for this store type') + + +class OnlyModelMessagesShareError(Exception): + def __str__(self) -> str: + return _('Link access for this store can only include model messages') + + +class IncompleteSetError(Exception): + def __str__(self) -> str: + return _('Not all messages were found. Some may have been deleted') + + +class ShareDoesNotExist(Exception): + def __str__(self) -> str: + return _('The link was not found or is no longer available') @@ -0,0 +1,19 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from messages.models import Message + + +class Share(models.Model): + code = models.CharField(max_length=8, unique=True, verbose_name=_('Code')) + messages = models.ManyToManyField(Message, blank=False, verbose_name=_('Messages')) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created At')) + expires_at = models.DateTimeField(verbose_name=_('Expires At')) + + def __str__(self) -> str: + return self.code + + class Meta: + verbose_name = _('Share') + verbose_name_plural = _('Shares') + ordering = ('expires_at',) @@ -0,0 +1,20 @@ +from datetime import datetime +from uuid import UUID + +from ninja import Schema + +from tools.chats.schemas import MessageSchema + + +class ShareMessagesSchema(Schema): + messages_uids: list[UUID] + + +class ShareSchema(Schema): + code: str + expires_at: datetime + + +class ShareResultSchema(Schema): + store_type: str + messages: list[MessageSchema] @@ -0,0 +1,9 @@ +from celery import shared_task +from django.utils import timezone + +from tools.share.models import Share + + +@shared_task +def delete_expired_shares() -> None: + Share.objects.filter(expires_at__lte=timezone.now()).delete() @@ -24,3 +24,9 @@ class MediaConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'tools.media' verbose_name = _('Media') + + +class ShareConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'tools.share' + verbose_name = _('Share') \ No newline at end of file @@ -119,4 +119,7 @@ PYTHONWARNINGS=ignore::UserWarning:polymorphic # temporarily # SSE STREAMING FF__STREAMING_ENABLED=True +# MESSAGE SHARING +SHARE_LIFETIME=1800 + DATA_UPLOAD_MAX_MEMORY_SIZE=5 # MB \ No newline at end of file