This commit is contained in:
2025-10-09 01:16:56 +03:00
32 changed files with 905 additions and 382 deletions

2
.gitignore vendored
View File

@@ -183,3 +183,5 @@ docs/progress/*
panel/graphql/generated panel/graphql/generated
test_e2e.db* test_e2e.db*
uv.lock

View File

@@ -1,6 +1,6 @@
# Changelog # Changelog
## [0.9.29] - 2025-10-08 ## [0.9.32] - 2025-10-08
### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS ### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS
@@ -68,6 +68,69 @@ SEARCH_MODEL_TYPE=biencoder
- pylate issue: https://github.com/lightonai/pylate/issues/142 - pylate issue: https://github.com/lightonai/pylate/issues/142
- Model: `answerdotai/answerai-colbert-small-v1` - Model: `answerdotai/answerai-colbert-small-v1`
## [0.9.32] - 2025-10-05
### ✨ Features
- **Редактирование мигрированных шаутов**: Добавлена мутация `create_draft_from_shout` для создания черновика из существующего опубликованного шаута
- Создаёт черновик со всеми данными из шаута (title, body, lead, topics, authors, media, etc.)
- Проверяет авторство перед созданием черновика
- Переиспользует существующий черновик если он уже создан для этого шаута
- Копирует все связи: авторов и темы (включая main topic)
### 🔧 Fixed
- **NotificationEntity enum**: Исправлена ошибка `NotificationEntity.FOLLOWER``NotificationEntity.AUTHOR`
- В enum не было значения `FOLLOWER`, используется `AUTHOR` для уведомлений о подписчиках
### Technical Details
- `core/schema/mutation.graphql`: добавлена мутация `create_draft_from_shout(shout_id: Int!): CommonResult!`
- `core/resolvers/draft.py`: добавлен resolver `create_draft_from_shout` с валидацией авторства
- `core/resolvers/notifier.py`: исправлено использование `NotificationEntity.AUTHOR` вместо несуществующего `FOLLOWER`
## [0.9.31] - 2025-10-04
### ✅ Fixed: Notifications TODOs
- **Уведомления о followers**: Добавлена обработка уведомлений о подписчиках в `notifications_seen_thread`
- Теперь при клике на группу "followers" все уведомления о подписках помечаются как прочитанные
- Исправлена обработка thread ID `"followers"` отдельно от shout/reaction threads
- **Уведомления о новых публикациях**: Добавлена обработка уведомлений о новых shouts в `notifications_seen_thread`
- При открытии публикации уведомления о ней тоже помечаются как прочитанные
- Исправлена логика парсинга thread ID для поддержки разных форматов
- **Code Quality**: Использованы enum константы (`NotificationAction`, `NotificationEntity`) вместо строк
- **Убраны устаревшие TODO**: Удален TODO про `notification_id` как offset (текущая логика с timestamp работает корректно)
### Technical Details
- `core/resolvers/notifier.py`: расширена функция `notifications_seen_thread` для поддержки всех типов уведомлений
- Добавлена обработка `thread == "followers"` для уведомлений о подписках
- Добавлена обработка `NotificationEntity.SHOUT` для уведомлений о новых публикациях
- Улучшена обработка ошибок с `logger.warning()` вместо исключений
## [0.9.30] - 2025-10-02
### 🔧 Fixed
- **Ревалидация кеша featured материалов**: Критическое исправление инвалидации кеша при изменении featured статуса
- Добавлены ключи кеша для featured материалов в `invalidate_shout_related_cache`
- Исправлена функция `set_featured`: добавлена инвалидация кеша лент
- Исправлена функция `set_unfeatured`: добавлена инвалидация кеша лент
- Теперь материалы корректно появляются/исчезают с главной страницы при фичеринге/расфичеринге
- Улучшена производительность через асинхронную инвалидацию кеша
### ✅ Code Quality
- **Python Standards Compliance**: Код соответствует стандартам 003-python-standards.mdc
- Пройдены проверки Ruff (linting & formatting)
- Пройдены проверки MyPy (type checking)
- Все функции имеют типы и докстринги
- Тесты проходят успешно
## [0.9.29] - 2025-10-01
### 🔧 Fixed
- **Фичерение публикаций**: Исправлена логика автоматического фичерения/расфичерения
- Теперь учитываются все положительные реакции (LIKE, ACCEPT, PROOF), а не только LIKE
- Исправлен подсчет реакций в `check_to_unfeature`: используется POSITIVE + NEGATIVE вместо только RATING_REACTIONS
- Добавлена явная проверка `reply_to.is_(None)` для исключения комментариев
- **Ревалидация кеша**: Добавлена ревалидация кеша публикаций, авторов и тем при изменении `featured_at`
- Улучшено логирование для отладки процесса фичерения
## [0.9.28] - 2025-09-28 ## [0.9.28] - 2025-09-28
### 🍪 CRITICAL Cross-Origin Auth ### 🍪 CRITICAL Cross-Origin Auth

View File

@@ -160,7 +160,7 @@ core/
### Environment Variables ### Environment Variables
- `DATABASE_URL` - Database connection string - `DATABASE_URL` - Database connection string
- `REDIS_URL` - Redis connection string - `REDIS_URL` - Redis connection string
- `JWT_SECRET` - JWT signing secret - `JWT_SECRET_KEY` - JWT signing secret
- `OAUTH_*` - OAuth provider credentials - `OAUTH_*` - OAuth provider credentials
### Database ### Database

View File

@@ -819,16 +819,16 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon
token_data["client_secret"] = client.client_secret token_data["client_secret"] = client.client_secret
async with httpx.AsyncClient() as http_client: async with httpx.AsyncClient() as http_client:
response = await http_client.post( token_response = await http_client.post(
token_endpoint, data=token_data, headers={"Accept": "application/json"} token_endpoint, data=token_data, headers={"Accept": "application/json"}
) )
if response.status_code != 200: if token_response.status_code != 200:
error_msg = f"Token request failed: {response.status_code} - {response.text}" error_msg = f"Token request failed: {token_response.status_code} - {token_response.text}"
logger.error(f"{error_msg}") logger.error(f"{error_msg}")
raise ValueError(error_msg) raise ValueError(error_msg)
token = response.json() token = token_response.json()
else: else:
# Провайдеры с PKCE поддержкой # Провайдеры с PKCE поддержкой
code_verifier = oauth_data.get("code_verifier") code_verifier = oauth_data.get("code_verifier")

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"files": { "files": {
"includes": [ "includes": [
"**/*.tsx", "**/*.tsx",

10
cache/cache.py vendored
View File

@@ -513,6 +513,10 @@ async def invalidate_shout_related_cache(shout: Shout, author_id: int) -> None:
"unrated", # неоцененные "unrated", # неоцененные
"recent", # последние "recent", # последние
"coauthored", # совместные "coauthored", # совместные
# 🔧 Добавляем ключи с featured материалами
"featured", # featured публикации
"featured:recent", # недавние featured
"featured:top", # топ featured
} }
# Добавляем ключи авторов # Добавляем ключи авторов
@@ -523,6 +527,12 @@ async def invalidate_shout_related_cache(shout: Shout, author_id: int) -> None:
cache_keys.update(f"topic_{t.id}" for t in shout.topics) cache_keys.update(f"topic_{t.id}" for t in shout.topics)
cache_keys.update(f"topic_shouts_{t.id}" for t in shout.topics) cache_keys.update(f"topic_shouts_{t.id}" for t in shout.topics)
# 🔧 Добавляем ключи featured материалов для каждой темы
for topic in shout.topics:
cache_keys.update(
[f"topic_{topic.id}:featured", f"topic_{topic.id}:featured:recent", f"topic_{topic.id}:featured:top"]
)
await invalidate_shouts_cache(list(cache_keys)) await invalidate_shouts_cache(list(cache_keys))

View File

@@ -242,7 +242,7 @@ SESSION_COOKIE_SAMESITE=lax
SESSION_COOKIE_MAX_AGE=2592000 # 30 дней SESSION_COOKIE_MAX_AGE=2592000 # 30 дней
# JWT # JWT
JWT_SECRET=your_jwt_secret_key JWT_SECRET_KEY=your_jwt_secret_key
JWT_EXPIRATION_HOURS=720 # 30 дней JWT_EXPIRATION_HOURS=720 # 30 дней
# Redis # Redis

View File

@@ -520,7 +520,7 @@ async def test_redis_integration():
### Подготовка ### Подготовка
- [ ] Настроен Redis connection pool с теми же параметрами - [ ] Настроен Redis connection pool с теми же параметрами
- [ ] Установлены зависимости: `auth.tokens.*`, `auth.utils` - [ ] Установлены зависимости: `auth.tokens.*`, `auth.utils`
- [ ] Настроены environment variables (JWT_SECRET, REDIS_URL) - [ ] Настроены environment variables (JWT_SECRET_KEY, REDIS_URL)
### Реализация ### Реализация
- [ ] Реализована функция извлечения токенов из запросов - [ ] Реализована функция извлечения токенов из запросов

View File

@@ -22,7 +22,7 @@
```python ```python
# settings.py # settings.py
JWT_ALGORITHM = "HS256" # HMAC with SHA-256 JWT_ALGORITHM = "HS256" # HMAC with SHA-256
JWT_SECRET = os.getenv("JWT_SECRET") # Минимум 256 бит JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") # Минимум 256 бит
JWT_EXPIRATION_DELTA = 30 * 24 * 60 * 60 # 30 дней JWT_EXPIRATION_DELTA = 30 * 24 * 60 * 60 # 30 дней
``` ```
@@ -439,7 +439,7 @@ async def detect_anomalies(user_id: str, event_type: str, ip_address: str):
### Environment Variables ### Environment Variables
```bash ```bash
# JWT Security # JWT Security
JWT_SECRET=your_super_secret_key_minimum_256_bits JWT_SECRET_KEY=your_super_secret_key_minimum_256_bits
JWT_ALGORITHM=HS256 JWT_ALGORITHM=HS256
JWT_EXPIRATION_HOURS=720 JWT_EXPIRATION_HOURS=720

View File

@@ -281,7 +281,7 @@ async def delete_session(token: str) -> bool:
### JWT токены ### JWT токены
- **Алгоритм**: HS256 - **Алгоритм**: HS256
- **Secret**: Из переменной окружения JWT_SECRET - **Secret**: Из переменной окружения JWT_SECRET_KEY
- **Payload**: `{user_id, username, iat, exp}` - **Payload**: `{user_id, username, iat, exp}`
- **Expiration**: 30 дней (настраивается) - **Expiration**: 30 дней (настраивается)

View File

@@ -6,7 +6,7 @@
```bash ```bash
# JWT настройки # JWT настройки
JWT_SECRET=your_super_secret_key_minimum_256_bits JWT_SECRET_KEY=your_super_secret_key_minimum_256_bits
JWT_ALGORITHM=HS256 JWT_ALGORITHM=HS256
JWT_EXPIRATION_HOURS=720 # 30 дней JWT_EXPIRATION_HOURS=720 # 30 дней
@@ -69,7 +69,7 @@ LOCKOUT_DURATION=1800 # 30 минут
# Проверка переменных окружения # Проверка переменных окружения
python -c " python -c "
import os import os
required = ['JWT_SECRET', 'REDIS_URL', 'GOOGLE_CLIENT_ID'] required = ['JWT_SECRET_KEY', 'REDIS_URL', 'GOOGLE_CLIENT_ID']
for var in required: for var in required:
print(f'{var}: {\"✅\" if os.getenv(var) else \"❌\"}')" print(f'{var}: {\"✅\" if os.getenv(var) else \"❌\"}')"
@@ -213,7 +213,7 @@ ab -n 1000 -c 10 -p login.json -T application/json http://localhost:8000/graphql
### Docker ### Docker
```dockerfile ```dockerfile
# Dockerfile # Dockerfile
ENV JWT_SECRET=your_secret_here ENV JWT_SECRET_KEY=your_secret_here
ENV REDIS_URL=redis://redis:6379/0 ENV REDIS_URL=redis://redis:6379/0
ENV SESSION_COOKIE_SECURE=true ENV SESSION_COOKIE_SECURE=true
``` ```
@@ -221,8 +221,8 @@ ENV SESSION_COOKIE_SECURE=true
### Dokku/Heroku ### Dokku/Heroku
```bash ```bash
# Установка переменных окружения # Установка переменных окружения
dokku config:set myapp JWT_SECRET=xxx REDIS_URL=yyy dokku config:set myapp JWT_SECRET_KEY=xxx REDIS_URL=yyy
heroku config:set JWT_SECRET=xxx REDIS_URL=yyy heroku config:set JWT_SECRET_KEY=xxx REDIS_URL=yyy
``` ```
### Nginx настройки ### Nginx настройки

View File

@@ -330,7 +330,7 @@ OAuth данные хранятся в JSON поле `oauth` модели `Autho
### Переменные окружения ### Переменные окружения
```bash ```bash
# JWT настройки # JWT настройки
JWT_SECRET=your_super_secret_key JWT_SECRET_KEY=your_super_secret_key
JWT_EXPIRATION_HOURS=720 # 30 дней JWT_EXPIRATION_HOURS=720 # 30 дней
# Redis подключение # Redis подключение

View File

@@ -819,7 +819,7 @@ jobs:
pytest tests/auth/e2e/ -m e2e pytest tests/auth/e2e/ -m e2e
env: env:
REDIS_URL: redis://localhost:6379/0 REDIS_URL: redis://localhost:6379/0
JWT_SECRET: test_secret_key_for_ci JWT_SECRET_KEY: test_secret_key_for_ci
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3

View File

@@ -127,7 +127,7 @@ env_vars:{variable_name} # STRING - значение перемен
### Примеры переменных ### Примеры переменных
```redis ```redis
GET env_vars:JWT_SECRET # Секретный ключ JWT GET env_vars:JWT_SECRET_KEY # Секретный ключ JWT
GET env_vars:REDIS_URL # URL Redis GET env_vars:REDIS_URL # URL Redis
GET env_vars:OAUTH_GOOGLE_CLIENT_ID # Google OAuth Client ID GET env_vars:OAUTH_GOOGLE_CLIENT_ID # Google OAuth Client ID
GET env_vars:FEATURE_REGISTRATION # Флаг функции регистрации GET env_vars:FEATURE_REGISTRATION # Флаг функции регистрации
@@ -135,7 +135,7 @@ GET env_vars:FEATURE_REGISTRATION # Флаг функции регистра
**Категории переменных**: **Категории переменных**:
- **database**: DB_URL, POSTGRES_* - **database**: DB_URL, POSTGRES_*
- **auth**: JWT_SECRET, OAUTH_* - **auth**: JWT_SECRET_KEY, OAUTH_*
- **redis**: REDIS_URL, REDIS_HOST, REDIS_PORT - **redis**: REDIS_URL, REDIS_HOST, REDIS_PORT
- **search**: SEARCH_* - **search**: SEARCH_*
- **integrations**: GOOGLE_ANALYTICS_ID, SENTRY_DSN, SMTP_* - **integrations**: GOOGLE_ANALYTICS_ID, SENTRY_DSN, SMTP_*

View File

@@ -99,6 +99,23 @@ class NotificationSeen(Base):
) )
class NotificationUnsubscribe(Base):
"""Модель для хранения отписок пользователей от уведомлений по определенным thread_id."""
__tablename__ = "notification_unsubscribe"
author_id: Mapped[int] = mapped_column(ForeignKey("author.id"), nullable=False)
thread_id: Mapped[str] = mapped_column(String, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
__table_args__ = (
PrimaryKeyConstraint(author_id, thread_id),
Index("idx_notification_unsubscribe_author", "author_id"),
Index("idx_notification_unsubscribe_thread", "thread_id"),
{"extend_existing": True},
)
class Notification(Base): class Notification(Base):
__tablename__ = "notification" __tablename__ = "notification"

518
package-lock.json generated
View File

@@ -1,32 +1,32 @@
{ {
"name": "publy-panel", "name": "publy-panel",
"version": "0.9.28", "version": "0.9.30",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "publy-panel", "name": "publy-panel",
"version": "0.9.28", "version": "0.9.30",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.2.4", "@biomejs/biome": "^2.2.5",
"@graphql-codegen/cli": "^6.0.0", "@graphql-codegen/cli": "^6.0.0",
"@graphql-codegen/client-preset": "^5.0.1", "@graphql-codegen/client-preset": "^5.0.2",
"@graphql-codegen/introspection": "^5.0.0", "@graphql-codegen/introspection": "^5.0.0",
"@graphql-codegen/typescript": "^5.0.0", "@graphql-codegen/typescript": "^5.0.1",
"@graphql-codegen/typescript-operations": "^5.0.0", "@graphql-codegen/typescript-operations": "^5.0.1",
"@graphql-codegen/typescript-resolvers": "^5.0.0", "@graphql-codegen/typescript-resolvers": "^5.0.1",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",
"@types/node": "^24.5.2", "@types/node": "^24.6.2",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"lightningcss": "^1.30.1", "lightningcss": "^1.30.2",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"solid-js": "^1.9.9", "solid-js": "^1.9.9",
"terser": "^5.44.0", "terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"vite": "^7.1.7", "vite": "^7.1.9",
"vite-plugin-solid": "^2.11.7" "vite-plugin-solid": "^2.11.9"
} }
}, },
"node_modules/@ardatan/relay-compiler": { "node_modules/@ardatan/relay-compiler": {
@@ -347,9 +347,9 @@
} }
}, },
"node_modules/@biomejs/biome": { "node_modules/@biomejs/biome": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.5.tgz",
"integrity": "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==", "integrity": "sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==",
"dev": true, "dev": true,
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"bin": { "bin": {
@@ -363,20 +363,20 @@
"url": "https://opencollective.com/biome" "url": "https://opencollective.com/biome"
}, },
"optionalDependencies": { "optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-arm64": "2.2.5",
"@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.5",
"@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.5",
"@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.5",
"@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64": "2.2.5",
"@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.5",
"@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.5",
"@biomejs/cli-win32-x64": "2.2.4" "@biomejs/cli-win32-x64": "2.2.5"
} }
}, },
"node_modules/@biomejs/cli-darwin-arm64": { "node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.5.tgz",
"integrity": "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==", "integrity": "sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -391,9 +391,9 @@
} }
}, },
"node_modules/@biomejs/cli-darwin-x64": { "node_modules/@biomejs/cli-darwin-x64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.5.tgz",
"integrity": "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==", "integrity": "sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -408,9 +408,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64": { "node_modules/@biomejs/cli-linux-arm64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.5.tgz",
"integrity": "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==", "integrity": "sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -425,9 +425,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64-musl": { "node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.5.tgz",
"integrity": "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==", "integrity": "sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -442,9 +442,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64": { "node_modules/@biomejs/cli-linux-x64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.5.tgz",
"integrity": "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==", "integrity": "sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -459,9 +459,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64-musl": { "node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.5.tgz",
"integrity": "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==", "integrity": "sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -476,9 +476,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-arm64": { "node_modules/@biomejs/cli-win32-arm64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.5.tgz",
"integrity": "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==", "integrity": "sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -493,9 +493,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-x64": { "node_modules/@biomejs/cli-win32-x64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.5.tgz",
"integrity": "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==", "integrity": "sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1088,21 +1088,21 @@
} }
}, },
"node_modules/@graphql-codegen/client-preset": { "node_modules/@graphql-codegen/client-preset": {
"version": "5.0.1", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.0.2.tgz",
"integrity": "sha512-3dXS7Sh/AkV+Ewq/HB1DSCb0tZBOIdTL8zkGQjRKWaf14x21h2f/xKl2zhRh6KlXjcCrIpX+AxHAhQxs6cXwVw==", "integrity": "sha512-lBkVMz7QA7FHWb71BcNB/tFFOh0LDNCPIBaJ70Lj1SIPjOfCEYmbkK6D5piPZu87m60hyWN3XDwNHEH8eGoXNA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-plugin-utils": "^7.20.2",
"@babel/template": "^7.20.7", "@babel/template": "^7.20.7",
"@graphql-codegen/add": "^6.0.0", "@graphql-codegen/add": "^6.0.0",
"@graphql-codegen/gql-tag-operations": "5.0.0", "@graphql-codegen/gql-tag-operations": "5.0.1",
"@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/plugin-helpers": "^6.0.0",
"@graphql-codegen/typed-document-node": "^6.0.0", "@graphql-codegen/typed-document-node": "^6.0.1",
"@graphql-codegen/typescript": "^5.0.0", "@graphql-codegen/typescript": "^5.0.1",
"@graphql-codegen/typescript-operations": "^5.0.0", "@graphql-codegen/typescript-operations": "^5.0.1",
"@graphql-codegen/visitor-plugin-common": "^6.0.0", "@graphql-codegen/visitor-plugin-common": "^6.0.1",
"@graphql-tools/documents": "^1.0.0", "@graphql-tools/documents": "^1.0.0",
"@graphql-tools/utils": "^10.0.0", "@graphql-tools/utils": "^10.0.0",
"@graphql-typed-document-node/core": "3.2.0", "@graphql-typed-document-node/core": "3.2.0",
@@ -1155,14 +1155,14 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@graphql-codegen/gql-tag-operations": { "node_modules/@graphql-codegen/gql-tag-operations": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.1.tgz",
"integrity": "sha512-kC2pc/tyzVc1laZtlfuQHqYxF4UqB4YXzAboFfeY1cxrxCh/+H70jHnfA1O4vhPndiRd+XZA8wxPv0hIqDXYaA==", "integrity": "sha512-GVd/B6mtRAXg6UxgeO805P7VDrCmVIb6qIMrE7O69j8e4EqIt/URdmJ7On+Bn8IIKp7TcpcLSo/VI28ptcssNw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/plugin-helpers": "^6.0.0",
"@graphql-codegen/visitor-plugin-common": "6.0.0", "@graphql-codegen/visitor-plugin-common": "6.0.1",
"@graphql-tools/utils": "^10.0.0", "@graphql-tools/utils": "^10.0.0",
"auto-bind": "~4.0.0", "auto-bind": "~4.0.0",
"tslib": "~2.6.0" "tslib": "~2.6.0"
@@ -1260,14 +1260,14 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@graphql-codegen/typed-document-node": { "node_modules/@graphql-codegen/typed-document-node": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.0.1.tgz",
"integrity": "sha512-OYmbadwvjq19yCZjioy901pLI9YV6i7A0fP3MpcJlo2uQVY27RJPcN2NeLfFzXdHr6f5bm9exqB6X1iKimfA2Q==", "integrity": "sha512-z0vvvmwfdozkY1AFqbNLeb/jAWyVwWJOIllZEEwPDKcVtCMPQZ1DRApPMRDRndRL6fOG4aXXnt7C5kgniC+qGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/plugin-helpers": "^6.0.0",
"@graphql-codegen/visitor-plugin-common": "6.0.0", "@graphql-codegen/visitor-plugin-common": "6.0.1",
"auto-bind": "~4.0.0", "auto-bind": "~4.0.0",
"change-case-all": "1.0.15", "change-case-all": "1.0.15",
"tslib": "~2.6.0" "tslib": "~2.6.0"
@@ -1287,15 +1287,15 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@graphql-codegen/typescript": { "node_modules/@graphql-codegen/typescript": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.1.tgz",
"integrity": "sha512-u90SGM6+Rdc3Je1EmVQOrGk5fl7hK1cLR4y5Q1MeUenj0aZFxKno65DCW7RcQpcfebvkPsVGA6y3oS02wPFj6Q==", "integrity": "sha512-GqAl4pxFdWTvW1h+Ume7djrucYwt03wiaS88m4ErG+tHsJaR2ZCtoHOo+B4bh7KIuBKap14/xOZG0qY/ThWAhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/plugin-helpers": "^6.0.0",
"@graphql-codegen/schema-ast": "^5.0.0", "@graphql-codegen/schema-ast": "^5.0.0",
"@graphql-codegen/visitor-plugin-common": "6.0.0", "@graphql-codegen/visitor-plugin-common": "6.0.1",
"auto-bind": "~4.0.0", "auto-bind": "~4.0.0",
"tslib": "~2.6.0" "tslib": "~2.6.0"
}, },
@@ -1307,15 +1307,15 @@
} }
}, },
"node_modules/@graphql-codegen/typescript-operations": { "node_modules/@graphql-codegen/typescript-operations": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.1.tgz",
"integrity": "sha512-mqgp/lp5v7w+RYj5AJ/BVquP+sgje3EAgg++62ciolOB5zzWT8en09cRdNq4UZfszCYTOtlhCG7NQAAcSae37A==", "integrity": "sha512-uJwsOIqvXyxlOI1Mnoy8Mn3TiOHTzVTGDwqL9gHnpKqQZdFfvMgfDf/HyT7Mw3XCOfhSS99fe9ATW0bkMExBZg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/plugin-helpers": "^6.0.0",
"@graphql-codegen/typescript": "^5.0.0", "@graphql-codegen/typescript": "^5.0.1",
"@graphql-codegen/visitor-plugin-common": "6.0.0", "@graphql-codegen/visitor-plugin-common": "6.0.1",
"auto-bind": "~4.0.0", "auto-bind": "~4.0.0",
"tslib": "~2.6.0" "tslib": "~2.6.0"
}, },
@@ -1340,15 +1340,15 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@graphql-codegen/typescript-resolvers": { "node_modules/@graphql-codegen/typescript-resolvers": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-5.0.1.tgz",
"integrity": "sha512-etUYZYwpBM+EmmcH/TtK9+dCzFMM36gI9aIc4/ckDnT34SLWnWVAkbfeNetwzhq98FD84SL5d+YqLGRFeEylJw==", "integrity": "sha512-E3Dyc2gaI4I79Wgwvwo5HP2MMQkUrcGA+3Lfx/ckDlE8zi3wwjWMhAjIhW54VQbi8q2/9h7ooRph3eat9VTscA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-codegen/plugin-helpers": "^6.0.0",
"@graphql-codegen/typescript": "^5.0.0", "@graphql-codegen/typescript": "^5.0.1",
"@graphql-codegen/visitor-plugin-common": "6.0.0", "@graphql-codegen/visitor-plugin-common": "6.0.1",
"@graphql-tools/utils": "^10.0.0", "@graphql-tools/utils": "^10.0.0",
"auto-bind": "~4.0.0", "auto-bind": "~4.0.0",
"tslib": "~2.6.0" "tslib": "~2.6.0"
@@ -1381,9 +1381,9 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@graphql-codegen/visitor-plugin-common": { "node_modules/@graphql-codegen/visitor-plugin-common": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.0.1.tgz",
"integrity": "sha512-K05Jv2elOeFstH3i+Ah0Pi9do6NYUvrbdhEkP+UvP9fmIro1hCKwcIEP7j4VFz8mt3gAC3dB5KVJDoyaPUgi4Q==", "integrity": "sha512-3gopoUYXn26PSj2UdCWmYj0QiRVD5qR3eDiXx72OQcN1Vb8qj6VfOWB+NDuD1Q1sgVYbCQVKgj92ERsSW1xH9Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2387,9 +2387,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
"integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2401,9 +2401,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
"integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2415,9 +2415,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
"integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2429,9 +2429,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
"integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2443,9 +2443,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
"integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2457,9 +2457,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
"integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2471,9 +2471,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
"integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2485,9 +2485,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
"integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2499,9 +2499,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
"integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2513,9 +2513,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
"integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2527,9 +2527,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
"integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -2541,9 +2541,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
"integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -2555,9 +2555,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
"integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -2569,9 +2569,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
"integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -2583,9 +2583,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
"integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -2597,9 +2597,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
"integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2611,9 +2611,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
"integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2625,9 +2625,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
"integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2639,9 +2639,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
"integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2653,9 +2653,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
"integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -2667,9 +2667,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
"integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2681,9 +2681,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
"integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2794,13 +2794,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.5.2", "version": "24.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz",
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.12.0" "undici-types": "~7.13.0"
} }
}, },
"node_modules/@types/prismjs": { "node_modules/@types/prismjs": {
@@ -3030,9 +3030,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.8", "version": "2.8.10",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.8.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz",
"integrity": "sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==", "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -3063,9 +3063,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.26.2", "version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3083,9 +3083,9 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.3", "baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001741", "caniuse-lite": "^1.0.30001746",
"electron-to-chromium": "^1.5.218", "electron-to-chromium": "^1.5.227",
"node-releases": "^2.0.21", "node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3" "update-browserslist-db": "^1.1.3"
}, },
@@ -3135,9 +3135,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001745", "version": "1.0.30001747",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz",
"integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3588,9 +3588,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.227", "version": "1.5.230",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz",
"integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", "integrity": "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -4292,9 +4292,9 @@
} }
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -4369,9 +4369,9 @@
} }
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"dev": true, "dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
@@ -4385,22 +4385,44 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"lightningcss-darwin-arm64": "1.30.1", "lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.1", "lightningcss-darwin-arm64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.1", "lightningcss-darwin-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.1" "lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/lightningcss-darwin-arm64": { "node_modules/lightningcss-darwin-arm64": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4419,9 +4441,9 @@
} }
}, },
"node_modules/lightningcss-darwin-x64": { "node_modules/lightningcss-darwin-x64": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4440,9 +4462,9 @@
} }
}, },
"node_modules/lightningcss-freebsd-x64": { "node_modules/lightningcss-freebsd-x64": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4461,9 +4483,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm-gnueabihf": { "node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -4482,9 +4504,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-gnu": { "node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4503,9 +4525,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-musl": { "node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4524,9 +4546,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-gnu": { "node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4545,9 +4567,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-musl": { "node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4566,9 +4588,9 @@
} }
}, },
"node_modules/lightningcss-win32-arm64-msvc": { "node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4587,9 +4609,9 @@
} }
}, },
"node_modules/lightningcss-win32-x64-msvc": { "node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -5408,9 +5430,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.52.3", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5424,28 +5446,28 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.3", "@rollup/rollup-android-arm-eabi": "4.52.4",
"@rollup/rollup-android-arm64": "4.52.3", "@rollup/rollup-android-arm64": "4.52.4",
"@rollup/rollup-darwin-arm64": "4.52.3", "@rollup/rollup-darwin-arm64": "4.52.4",
"@rollup/rollup-darwin-x64": "4.52.3", "@rollup/rollup-darwin-x64": "4.52.4",
"@rollup/rollup-freebsd-arm64": "4.52.3", "@rollup/rollup-freebsd-arm64": "4.52.4",
"@rollup/rollup-freebsd-x64": "4.52.3", "@rollup/rollup-freebsd-x64": "4.52.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.3", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
"@rollup/rollup-linux-arm-musleabihf": "4.52.3", "@rollup/rollup-linux-arm-musleabihf": "4.52.4",
"@rollup/rollup-linux-arm64-gnu": "4.52.3", "@rollup/rollup-linux-arm64-gnu": "4.52.4",
"@rollup/rollup-linux-arm64-musl": "4.52.3", "@rollup/rollup-linux-arm64-musl": "4.52.4",
"@rollup/rollup-linux-loong64-gnu": "4.52.3", "@rollup/rollup-linux-loong64-gnu": "4.52.4",
"@rollup/rollup-linux-ppc64-gnu": "4.52.3", "@rollup/rollup-linux-ppc64-gnu": "4.52.4",
"@rollup/rollup-linux-riscv64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-gnu": "4.52.4",
"@rollup/rollup-linux-riscv64-musl": "4.52.3", "@rollup/rollup-linux-riscv64-musl": "4.52.4",
"@rollup/rollup-linux-s390x-gnu": "4.52.3", "@rollup/rollup-linux-s390x-gnu": "4.52.4",
"@rollup/rollup-linux-x64-gnu": "4.52.3", "@rollup/rollup-linux-x64-gnu": "4.52.4",
"@rollup/rollup-linux-x64-musl": "4.52.3", "@rollup/rollup-linux-x64-musl": "4.52.4",
"@rollup/rollup-openharmony-arm64": "4.52.3", "@rollup/rollup-openharmony-arm64": "4.52.4",
"@rollup/rollup-win32-arm64-msvc": "4.52.3", "@rollup/rollup-win32-arm64-msvc": "4.52.4",
"@rollup/rollup-win32-ia32-msvc": "4.52.3", "@rollup/rollup-win32-ia32-msvc": "4.52.4",
"@rollup/rollup-win32-x64-gnu": "4.52.3", "@rollup/rollup-win32-x64-gnu": "4.52.4",
"@rollup/rollup-win32-x64-msvc": "4.52.3", "@rollup/rollup-win32-x64-msvc": "4.52.4",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -5903,9 +5925,9 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.2", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -5954,9 +5976,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.12.0", "version": "7.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -6039,9 +6061,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.7", "version": "7.1.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -6114,9 +6136,9 @@
} }
}, },
"node_modules/vite-plugin-solid": { "node_modules/vite-plugin-solid": {
"version": "2.11.8", "version": "2.11.9",
"resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.8.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.9.tgz",
"integrity": "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg==", "integrity": "sha512-bTA6p+bspXZsuulSd2y6aTzegF8xGaJYcq1Uyh/mv+W4DQtzCgL9nN6n2fsTaxp/dMk+ZHHKgGndlNeooqHLKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "publy-panel", "name": "publy-panel",
"version": "0.9.28", "version": "0.9.30",
"type": "module", "type": "module",
"description": "Publy, a modern platform for collaborative text creation, offers a user-friendly interface for authors, editors, and readers, supporting real-time collaboration and structured feedback.", "description": "Publy, a modern platform for collaborative text creation, offers a user-friendly interface for authors, editors, and readers, supporting real-time collaboration and structured feedback.",
"scripts": { "scripts": {
@@ -13,27 +13,27 @@
"codegen": "graphql-codegen --config codegen.ts" "codegen": "graphql-codegen --config codegen.ts"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.2.4", "@biomejs/biome": "^2.2.5",
"@graphql-codegen/cli": "^6.0.0", "@graphql-codegen/cli": "^6.0.0",
"@graphql-codegen/client-preset": "^5.0.1", "@graphql-codegen/client-preset": "^5.0.2",
"@graphql-codegen/introspection": "^5.0.0", "@graphql-codegen/introspection": "^5.0.0",
"@graphql-codegen/typescript": "^5.0.0", "@graphql-codegen/typescript": "^5.0.1",
"@graphql-codegen/typescript-operations": "^5.0.0", "@graphql-codegen/typescript-operations": "^5.0.1",
"@graphql-codegen/typescript-resolvers": "^5.0.0", "@graphql-codegen/typescript-resolvers": "^5.0.1",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",
"@types/node": "^24.5.2", "@types/node": "^24.6.2",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"lightningcss": "^1.30.1", "lightningcss": "^1.30.2",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"solid-js": "^1.9.9", "solid-js": "^1.9.9",
"terser": "^5.44.0", "terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"vite": "^7.1.7", "vite": "^7.1.9",
"vite-plugin-solid": "^2.11.7" "vite-plugin-solid": "^2.11.9"
}, },
"overrides": { "overrides": {
"vite": "^7.1.7" "vite": "^7.1.9"
} }
} }

View File

@@ -1,6 +1,6 @@
import { Component, createSignal, For, Show } from 'solid-js' import { Component, createSignal, For, Show } from 'solid-js'
import { query } from '../graphql' import { query } from '../graphql'
import type { EnvSection, EnvVariable, Query } from '../graphql/generated/schema' import type { EnvSection, EnvVariable, Query } from '../graphql/generated/graphql'
import { ADMIN_UPDATE_ENV_VARIABLE_MUTATION } from '../graphql/mutations' import { ADMIN_UPDATE_ENV_VARIABLE_MUTATION } from '../graphql/mutations'
import { ADMIN_GET_ENV_VARIABLES_QUERY } from '../graphql/queries' import { ADMIN_GET_ENV_VARIABLES_QUERY } from '../graphql/queries'
import EnvVariableModal from '../modals/EnvVariableModal' import EnvVariableModal from '../modals/EnvVariableModal'

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "discours-core" name = "discours-core"
version = "0.9.28" version = "0.9.32"
description = "Core backend for Discours.io platform" description = "Core backend for Discours.io platform"
authors = [ authors = [
{name = "Tony Rewin", email = "tonyrewin@yandex.ru"} {name = "Tony Rewin", email = "tonyrewin@yandex.ru"}

View File

@@ -27,11 +27,15 @@ from utils.logger import root_logger as logger
def resolve_roles(obj: dict | Any, info: GraphQLResolveInfo) -> list[str]: def resolve_roles(obj: dict | Any, info: GraphQLResolveInfo) -> list[str]:
"""Резолвер для поля roles автора""" """Резолвер для поля roles автора"""
try: try:
# Если это ORM объект с методом get_roles
if hasattr(obj, "get_roles"): if hasattr(obj, "get_roles"):
return obj.get_roles() return obj.get_roles()
# Если это словарь
if isinstance(obj, dict): if isinstance(obj, dict):
roles_data = obj.get("roles_data", {}) roles_data = obj.get("roles_data")
if roles_data is None:
return []
if isinstance(roles_data, list): if isinstance(roles_data, list):
return roles_data return roles_data
if isinstance(roles_data, dict): if isinstance(roles_data, dict):
@@ -122,9 +126,12 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A
domain=SESSION_COOKIE_DOMAIN, domain=SESSION_COOKIE_DOMAIN,
) )
logger.info( author_id = (
f"✅ Admin login: httpOnly cookie установлен для пользователя {result.get('author', {}).get('id')}" result.get("author", {}).get("id")
if isinstance(result.get("author"), dict)
else getattr(result.get("author"), "id", "unknown")
) )
logger.info(f"✅ Admin login: httpOnly cookie установлен для пользователя {author_id}")
# Для админки НЕ возвращаем токен клиенту - он в httpOnly cookie # Для админки НЕ возвращаем токен клиенту - он в httpOnly cookie
result_without_token = result.copy() result_without_token = result.copy()
@@ -136,9 +143,12 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A
# Для основного сайта возвращаем токен как обычно (Bearer в localStorage) # Для основного сайта возвращаем токен как обычно (Bearer в localStorage)
if not is_admin_request: if not is_admin_request:
logger.info( author_id = (
f"✅ Main site login: токен возвращен для localStorage пользователя {result.get('author', {}).get('id')}" result.get("author", {}).get("id")
if isinstance(result.get("author"), dict)
else getattr(result.get("author"), "id", "unknown")
) )
logger.info(f"✅ Main site login: токен возвращен для localStorage пользователя {author_id}")
return result return result
except Exception as e: except Exception as e:

View File

@@ -18,7 +18,7 @@ from cache.cache import (
from orm.author import Author, AuthorFollower from orm.author import Author, AuthorFollower
from orm.community import Community, CommunityAuthor, CommunityFollower from orm.community import Community, CommunityAuthor, CommunityFollower
from orm.reaction import Reaction from orm.reaction import Reaction
from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic
from orm.topic import Topic from orm.topic import Topic
from resolvers.stat import get_with_stat from resolvers.stat import get_with_stat
from services.auth import login_required from services.auth import login_required
@@ -974,12 +974,23 @@ async def get_author_follows(
has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id)) has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id))
followed_authors.append(temp_author.dict(has_access)) followed_authors.append(temp_author.dict(has_access))
# Получаем подписанные шауты
followed_shouts = []
with local_session() as session:
shout_followers = (
session.query(ShoutReactionsFollower).filter(ShoutReactionsFollower.follower == author_id).all()
)
for sf in shout_followers:
shout = session.query(Shout).filter(Shout.id == sf.shout).first()
if shout:
followed_shouts.append(shout.dict())
followed_communities = DEFAULT_COMMUNITIES # TODO: get followed communities followed_communities = DEFAULT_COMMUNITIES # TODO: get followed communities
return { return {
"authors": followed_authors, "authors": followed_authors,
"topics": followed_topics, "topics": followed_topics,
"communities": followed_communities, "communities": followed_communities,
"shouts": [], "shouts": followed_shouts,
"error": None, "error": None,
} }

View File

@@ -274,6 +274,108 @@ async def create_draft(_: None, info: GraphQLResolveInfo, draft_input: dict[str,
return {"error": f"Failed to create draft: {e!s}"} return {"error": f"Failed to create draft: {e!s}"}
@mutation.field("create_draft_from_shout")
@login_required
async def create_draft_from_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> dict[str, Any]:
"""
Создаёт черновик из существующего опубликованного шаута для редактирования.
Args:
info: GraphQL context
shout_id (int): ID публикации (shout)
Returns:
dict: Contains either:
- draft: The created draft object with shout reference
- error: Error message if creation failed
Example:
>>> async def test_create_from_shout():
... context = {'user_id': '123', 'author': {'id': 1}}
... info = type('Info', (), {'context': context})()
... result = await create_draft_from_shout(None, info, 42)
... assert result.get('error') is None
... assert result['draft'].shout == 42
... return result
"""
author_dict = info.context.get("author") or {}
author_id = author_dict.get("id")
if not author_id or not isinstance(author_id, int):
return {"error": "Author ID is required"}
try:
with local_session() as session:
# Загружаем шаут с авторами и темами
shout = (
session.query(Shout)
.options(joinedload(Shout.authors), joinedload(Shout.topics))
.where(Shout.id == shout_id)
.first()
)
if not shout:
return {"error": f"Shout with id={shout_id} not found"}
# Проверяем, что пользователь является автором шаута
author_ids = [a.id for a in shout.authors]
if author_id not in author_ids:
return {"error": "You are not authorized to edit this shout"}
# Проверяем, нет ли уже черновика для этого шаута
existing_draft = session.query(Draft).where(Draft.shout == shout_id).first()
if existing_draft:
logger.info(f"Draft already exists for shout {shout_id}: draft_id={existing_draft.id}")
return {"draft": create_draft_dict(existing_draft)}
# Создаём новый черновик из шаута
now = int(time.time())
draft = Draft(
created_at=now,
created_by=author_id,
community=shout.community,
layout=shout.layout or "article",
title=shout.title or "",
subtitle=shout.subtitle,
body=shout.body or "",
lead=shout.lead,
slug=shout.slug,
cover=shout.cover,
cover_caption=shout.cover_caption,
seo=shout.seo,
media=shout.media,
lang=shout.lang or "ru",
shout=shout_id, # Связываем с существующим шаутом
)
session.add(draft)
session.flush()
# Копируем авторов из шаута
for author in shout.authors:
da = DraftAuthor(draft=draft.id, author=author.id)
session.add(da)
# Копируем темы из шаута
shout_topics = session.query(ShoutTopic).where(ShoutTopic.shout == shout_id).all()
for st in shout_topics:
dt = DraftTopic(draft=draft.id, topic=st.topic, main=st.main)
session.add(dt)
session.commit()
logger.info(f"Created draft {draft.id} from shout {shout_id}")
# Формируем результат
draft_dict = create_draft_dict(draft)
return {"draft": draft_dict}
except Exception as e:
logger.error(f"Failed to create draft from shout {shout_id}: {e}", exc_info=True)
return {"error": f"Failed to create draft from shout: {e!s}"}
def generate_teaser(body: str, limit: int = 300) -> str: def generate_teaser(body: str, limit: int = 300) -> str:
body_text = extract_text(body) body_text = extract_text(body)
return ". ".join(body_text[:limit].split(". ")[:-1]) return ". ".join(body_text[:limit].split(". ")[:-1])

View File

@@ -25,8 +25,36 @@ from utils.logger import root_logger as logger
def get_entity_field_name(entity_type: str) -> str: def get_entity_field_name(entity_type: str) -> str:
"""Возвращает имя поля для связи с сущностью в модели подписчика""" """
entity_field_mapping = {"author": "following", "topic": "topic", "community": "community", "shout": "shout"} Возвращает имя поля для связи с сущностью в модели подписчика.
Эта функция используется для определения правильного поля в моделях подписчиков
(AuthorFollower, TopicFollower, CommunityFollower, ShoutReactionsFollower) при создании
или проверке подписки.
Args:
entity_type: Тип сущности в нижнем регистре ('author', 'topic', 'community', 'shout')
Returns:
str: Имя поля в модели подписчика ('following', 'topic', 'community', 'shout')
Raises:
ValueError: Если передан неизвестный тип сущности
Examples:
>>> get_entity_field_name('author')
'following'
>>> get_entity_field_name('topic')
'topic'
>>> get_entity_field_name('invalid')
ValueError: Unknown entity_type: invalid
"""
entity_field_mapping = {
"author": "following", # AuthorFollower.following -> Author
"topic": "topic", # TopicFollower.topic -> Topic
"community": "community", # CommunityFollower.community -> Community
"shout": "shout", # ShoutReactionsFollower.shout -> Shout
}
if entity_type not in entity_field_mapping: if entity_type not in entity_field_mapping:
msg = f"Unknown entity_type: {entity_type}" msg = f"Unknown entity_type: {entity_type}"
raise ValueError(msg) raise ValueError(msg)
@@ -38,11 +66,54 @@ def get_entity_field_name(entity_type: str) -> str:
async def follow( async def follow(
_: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None _: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
"""
GraphQL мутация для создания подписки на автора, тему, сообщество или публикацию.
Эта функция обрабатывает все типы подписок в системе, включая:
- Подписку на автора (AUTHOR)
- Подписку на тему (TOPIC)
- Подписку на сообщество (COMMUNITY)
- Подписку на публикацию (SHOUT)
Args:
_: None - Стандартный параметр GraphQL (не используется)
info: GraphQLResolveInfo - Контекст GraphQL запроса, содержит информацию об авторизованном пользователе
what: str - Тип сущности для подписки ('AUTHOR', 'TOPIC', 'COMMUNITY', 'SHOUT')
slug: str - Slug сущности (например, 'author-slug' или 'topic-slug')
entity_id: int | None - ID сущности (альтернатива slug)
Returns:
dict[str, Any] - Результат операции:
{
"success": bool, # Успешность операции
"error": str | None, # Текст ошибки если есть
"authors": Author[], # Обновленные авторы (для кеширования)
"topics": Topic[], # Обновленные темы (для кеширования)
"entity_id": int | None # ID созданной подписки
}
Raises:
ValueError: При передаче некорректных параметров
DatabaseError: При проблемах с базой данных
"""
logger.debug("Начало выполнения функции 'follow'") logger.debug("Начало выполнения функции 'follow'")
viewer_id = info.context.get("author", {}).get("id") viewer_id = info.context.get("author", {}).get("id")
if not viewer_id:
return {"error": "Access denied"}
follower_dict = info.context.get("author") or {} follower_dict = info.context.get("author") or {}
# ✅ КРИТИЧНО: Инвалидируем кеш В САМОМ НАЧАЛЕ, если пользователь авторизован
# чтобы предотвратить чтение старых данных при последующей перезагрузке
if viewer_id:
entity_type = what.lower()
cache_key_pattern = f"author:follows-{entity_type}s:{viewer_id}"
await redis.execute("DEL", cache_key_pattern)
await redis.execute("DEL", f"author:id:{viewer_id}")
logger.debug(f"Инвалидирован кеш подписок follower'а: {cache_key_pattern}")
# Проверка авторизации пользователя
if not viewer_id:
logger.warning("Попытка подписаться без авторизации")
return {"error": "Access denied"}
logger.debug(f"follower: {follower_dict}") logger.debug(f"follower: {follower_dict}")
if not viewer_id or not follower_dict: if not viewer_id or not follower_dict:
@@ -52,6 +123,7 @@ async def follow(
follower_id = follower_dict.get("id") follower_id = follower_dict.get("id")
logger.debug(f"follower_id: {follower_id}") logger.debug(f"follower_id: {follower_id}")
# Маппинг типов сущностей на их классы и методы кеширования
entity_classes = { entity_classes = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),
@@ -68,6 +140,10 @@ async def follow(
follows: list[dict[str, Any]] = [] follows: list[dict[str, Any]] = []
error: str | None = None error: str | None = None
# ✅ Сохраняем entity_id и error вне сессии для использования после её закрытия
entity_id_result: int | None = None
error_result: str | None = None
try: try:
logger.debug("Попытка получить сущность из базы данных") logger.debug("Попытка получить сущность из базы данных")
with local_session() as session: with local_session() as session:
@@ -109,9 +185,11 @@ async def follow(
) )
.first() .first()
) )
if existing_sub: if existing_sub:
logger.info(f"Пользователь {follower_id} уже подписан на {what.lower()} с ID {entity_id}") logger.info(f"Пользователь {follower_id} уже подписан на {what.lower()} с ID {entity_id}")
error = "already following" error_result = "already following"
# ✅ КРИТИЧНО: Не делаем return - продолжаем для получения списка подписок
else: else:
logger.debug("Добавление новой записи в базу данных") logger.debug("Добавление новой записи в базу данных")
sub = follower_class(follower=follower_id, **{entity_field: entity_id}) sub = follower_class(follower=follower_id, **{entity_field: entity_id})
@@ -120,21 +198,11 @@ async def follow(
session.commit() session.commit()
logger.info(f"Пользователь {follower_id} подписался на {what.lower()} с ID {entity_id}") logger.info(f"Пользователь {follower_id} подписался на {what.lower()} с ID {entity_id}")
# Инвалидируем кэш подписок пользователя после любой операции
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
await redis.execute("DEL", cache_key_pattern)
logger.debug(f"Инвалидирован кэш подписок: {cache_key_pattern}")
if cache_method: if cache_method:
logger.debug("Обновление кэша сущности") logger.debug("Обновление кэша сущности")
await cache_method(entity_dict) await cache_method(entity_dict)
# Инвалидируем кэш подписок пользователя для обновления списка подписок if what == "AUTHOR":
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
await redis.execute("DEL", cache_key_pattern)
logger.debug(f"Инвалидирован кэш подписок: {cache_key_pattern}")
if what == "AUTHOR" and not existing_sub:
logger.debug("Отправка уведомления автору о подписке") logger.debug("Отправка уведомления автору о подписке")
if isinstance(follower_dict, dict) and isinstance(entity_id, int): if isinstance(follower_dict, dict) and isinstance(entity_id, int):
# Получаем ID созданной записи подписки # Получаем ID созданной записи подписки
@@ -146,16 +214,25 @@ async def follow(
subscription_id=subscription_id, subscription_id=subscription_id,
) )
# ✅ КРИТИЧНО: Инвалидируем кеш списка подписчиков автора
# чтобы новый подписчик сразу появился в списке
await redis.execute("DEL", f"author:followers:{entity_id}")
logger.debug(f"Инвалидирован кеш подписчиков автора: author:followers:{entity_id}")
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков # Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
logger.debug("Инвалидируем кеш статистики авторов") logger.debug("Инвалидируем кеш статистики авторов")
await invalidate_authors_cache(entity_id) await invalidate_authors_cache(entity_id)
# Всегда получаем актуальный список подписок для возврата клиенту entity_id_result = entity_id
# ✅ Получаем актуальный список подписок для возврата клиенту
# Кеш уже инвалидирован в начале функции, поэтому get_cached_follows_method
# вернет свежие данные из БД
if get_cached_follows_method and isinstance(follower_id, int): if get_cached_follows_method and isinstance(follower_id, int):
logger.debug("Получение актуального списка подписок из кэша") logger.debug("Получение актуального списка подписок после закрытия сессии")
existing_follows = await get_cached_follows_method(follower_id) existing_follows = await get_cached_follows_method(follower_id)
logger.debug( logger.debug(
f"Получено подписок: {len(existing_follows)}, содержит target={entity_id in [f.get('id') for f in existing_follows] if existing_follows else False}" f"Получено подписок: {len(existing_follows)}, содержит target={entity_id_result in [f.get('id') for f in existing_follows] if existing_follows else False}"
) )
# Если это авторы, получаем безопасную версию # Если это авторы, получаем безопасную версию
@@ -179,7 +256,7 @@ async def follow(
logger.debug(f"Актуальный список подписок получен: {len(follows)} элементов") logger.debug(f"Актуальный список подписок получен: {len(follows)} элементов")
return {f"{entity_type}s": follows, "error": error} return {f"{entity_type}s": follows, "error": error_result}
except Exception as exc: except Exception as exc:
logger.exception("Произошла ошибка в функции 'follow'") logger.exception("Произошла ошибка в функции 'follow'")
@@ -191,11 +268,93 @@ async def follow(
async def unfollow( async def unfollow(
_: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None _: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
"""
GraphQL мутация для отмены подписки на автора, тему, сообщество или публикацию.
Эта функция обрабатывает отмену всех типов подписок в системе, включая:
- Отписку от автора (AUTHOR)
- Отписку от темы (TOPIC)
- Отписку от сообщества (COMMUNITY)
- Отписку от публикации (SHOUT)
Процесс отмены подписки:
1. Проверка авторизации пользователя
2. Поиск существующей подписки в базе данных
3. Удаление подписки если она найдена
4. Инвалидация кеша для обновления данных
5. Отправка уведомлений об отписке
Args:
_: None - Стандартный параметр GraphQL (не используется)
info: GraphQLResolveInfo - Контекст GraphQL запроса, содержит информацию об авторизованном пользователе
what: str - Тип сущности для отписки ('AUTHOR', 'TOPIC', 'COMMUNITY', 'SHOUT')
slug: str - Slug сущности (например, 'author-slug' или 'topic-slug')
entity_id: int | None - ID сущности (альтернатива slug)
Returns:
dict[str, Any] - Результат операции:
{
"success": bool, # Успешность операции
"error": str | None, # Текст ошибки если есть
"authors": Author[], # Обновленные авторы (для кеширования)
"topics": Topic[], # Обновленные темы (для кеширования)
}
Raises:
ValueError: При передаче некорректных параметров
DatabaseError: При проблемах с базой данных
Examples:
# Отписка от автора
mutation {
unfollow(what: "AUTHOR", slug: "author-slug") {
success
error
}
}
# Отписка от темы
mutation {
unfollow(what: "TOPIC", slug: "topic-slug") {
success
error
}
}
# Отписка от сообщества
mutation {
unfollow(what: "COMMUNITY", slug: "community-slug") {
success
error
}
}
# Отписка от публикации
mutation {
unfollow(what: "SHOUT", entity_id: 123) {
success
error
}
}
"""
logger.debug("Начало выполнения функции 'unfollow'") logger.debug("Начало выполнения функции 'unfollow'")
viewer_id = info.context.get("author", {}).get("id") viewer_id = info.context.get("author", {}).get("id")
if not viewer_id:
return {"error": "Access denied"}
follower_dict = info.context.get("author") or {} follower_dict = info.context.get("author") or {}
# ✅ КРИТИЧНО: Инвалидируем кеш В САМОМ НАЧАЛЕ, если пользователь авторизован
# чтобы предотвратить чтение старых данных при последующей перезагрузке
if viewer_id:
entity_type = what.lower()
cache_key_pattern = f"author:follows-{entity_type}s:{viewer_id}"
await redis.execute("DEL", cache_key_pattern)
await redis.execute("DEL", f"author:id:{viewer_id}")
logger.debug(f"Инвалидирован кеш подписок В НАЧАЛЕ операции unfollow: {cache_key_pattern}")
# Проверка авторизации пользователя
if not viewer_id:
logger.warning("Попытка отписаться без авторизации")
return {"error": "Access denied"}
logger.debug(f"follower: {follower_dict}") logger.debug(f"follower: {follower_dict}")
if not viewer_id or not follower_dict: if not viewer_id or not follower_dict:
@@ -205,6 +364,7 @@ async def unfollow(
follower_id = follower_dict.get("id") follower_id = follower_dict.get("id")
logger.debug(f"follower_id: {follower_id}") logger.debug(f"follower_id: {follower_id}")
# Маппинг типов сущностей на их классы и методы кеширования
entity_classes = { entity_classes = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),
@@ -262,11 +422,7 @@ async def unfollow(
session.commit() session.commit()
logger.info(f"Пользователь {follower_id} отписался от {what.lower()} с ID {entity_id}") logger.info(f"Пользователь {follower_id} отписался от {what.lower()} с ID {entity_id}")
# Инвалидируем кэш подписок пользователя # Кеш подписок follower'а уже инвалидирован в начале функции
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
await redis.execute("DEL", cache_key_pattern)
logger.debug(f"Инвалидирован кэш подписок: {cache_key_pattern}")
if get_cached_follows_method and isinstance(follower_id, int): if get_cached_follows_method and isinstance(follower_id, int):
logger.debug("Получение актуального списка подписок из кэша") logger.debug("Получение актуального списка подписок из кэша")
follows = await get_cached_follows_method(follower_id) follows = await get_cached_follows_method(follower_id)
@@ -277,6 +433,11 @@ async def unfollow(
if what == "AUTHOR" and isinstance(follower_dict, dict): if what == "AUTHOR" and isinstance(follower_dict, dict):
await notify_follower(follower=follower_dict, author_id=entity_id, action="unfollow") await notify_follower(follower=follower_dict, author_id=entity_id, action="unfollow")
# ✅ КРИТИЧНО: Инвалидируем кеш списка подписчиков автора
# чтобы отписавшийся сразу исчез из списка
await redis.execute("DEL", f"author:followers:{entity_id}")
logger.debug(f"Инвалидирован кеш подписчиков автора после unfollow: author:followers:{entity_id}")
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков # Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
logger.debug("Инвалидируем кеш статистики авторов после отписки") logger.debug("Инвалидируем кеш статистики авторов после отписки")
await invalidate_authors_cache(entity_id) await invalidate_authors_cache(entity_id)

View File

@@ -16,7 +16,7 @@ from orm.notification import (
NotificationEntity, NotificationEntity,
NotificationSeen, NotificationSeen,
) )
from orm.shout import Shout from orm.shout import Shout, ShoutReactionsFollower
from services.auth import login_required from services.auth import login_required
from storage.db import local_session from storage.db import local_session
from storage.schema import mutation, query from storage.schema import mutation, query
@@ -57,6 +57,37 @@ def query_notifications(author_id: int, after: int = 0) -> tuple[int, int, list[
return total, unread, notifications return total, unread, notifications
def check_subscription(shout_id: int, current_author_id: int) -> bool:
"""
Проверяет подписку пользователя на уведомления о шауте.
Проверяет наличие записи в ShoutReactionsFollower:
- Запись есть → подписан
- Записи нет → не подписан (отписался или никогда не подписывался)
Автоматическая подписка (auto=True) создается при:
- Создании поста
- Первом комментарии/реакции
Отписка = удаление записи из таблицы
Returns:
bool: True если подписан на уведомления
"""
with local_session() as session:
# Проверяем наличие записи в ShoutReactionsFollower
follow = (
session.query(ShoutReactionsFollower)
.filter(
ShoutReactionsFollower.follower == current_author_id,
ShoutReactionsFollower.shout == shout_id,
)
.first()
)
return follow is not None
def group_notification( def group_notification(
thread: str, thread: str,
authors: list[Any] | None = None, authors: list[Any] | None = None,
@@ -105,7 +136,6 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o
authors: List[NotificationAuthor], # List of authors involved in the thread. authors: List[NotificationAuthor], # List of authors involved in the thread.
} }
""" """
# TODO: use all stats
_total, _unread, notifications = query_notifications(author_id, after) _total, _unread, notifications = query_notifications(author_id, after)
groups_by_thread = {} groups_by_thread = {}
groups_amount = 0 groups_amount = 0
@@ -119,14 +149,20 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o
if str(notification.entity) == NotificationEntity.SHOUT.value: if str(notification.entity) == NotificationEntity.SHOUT.value:
shout = payload shout = payload
shout_id = shout.get("id") shout_id = shout.get("id")
author_id = shout.get("created_by") shout_author_id = shout.get("created_by")
thread_id = f"shout-{shout_id}" thread_id = f"shout-{shout_id}"
with local_session() as session: with local_session() as session:
author = session.query(Author).where(Author.id == author_id).first() author = session.query(Author).where(Author.id == shout_author_id).first()
shout = session.query(Shout).where(Shout.id == shout_id).first() shout = session.query(Shout).where(Shout.id == shout_id).first()
if author and shout: if author and shout:
# Проверяем подписку - если не подписан, пропускаем это уведомление
if not check_subscription(shout_id, author_id):
continue
author_dict = author.dict() author_dict = author.dict()
shout_dict = shout.dict() shout_dict = shout.dict()
group = group_notification( group = group_notification(
thread_id, thread_id,
shout=shout_dict, shout=shout_dict,
@@ -154,7 +190,8 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o
reply_id = reaction.get("reply_to") reply_id = reaction.get("reply_to")
thread_id = f"shout-{shout_id}" thread_id = f"shout-{shout_id}"
if reply_id and reaction.get("kind", "").lower() == "comment": if reply_id and reaction.get("kind", "").lower() == "comment":
thread_id += f"{reply_id}" thread_id = f"shout-{shout_id}::{reply_id}"
existing_group = groups_by_thread.get(thread_id) existing_group = groups_by_thread.get(thread_id)
if existing_group: if existing_group:
existing_group["seen"] = False existing_group["seen"] = False
@@ -163,6 +200,10 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o
existing_group["reactions"].append(reaction) existing_group["reactions"].append(reaction)
groups_by_thread[thread_id] = existing_group groups_by_thread[thread_id] = existing_group
else: else:
# Проверяем подписку - если не подписан, пропускаем это уведомление
if not check_subscription(shout_id, author_id):
continue
group = group_notification( group = group_notification(
thread_id, thread_id,
authors=[author_dict], authors=[author_dict],
@@ -214,6 +255,10 @@ async def load_notifications(_: None, info: GraphQLResolveInfo, after: int, limi
if author_id: if author_id:
groups_list = get_notifications_grouped(author_id, after, limit) groups_list = get_notifications_grouped(author_id, after, limit)
notifications = sorted(groups_list, key=lambda group: group.get("updated_at", 0), reverse=True) notifications = sorted(groups_list, key=lambda group: group.get("updated_at", 0), reverse=True)
# Считаем реальное количество сгруппированных уведомлений
total = len(notifications)
unread = sum(1 for n in notifications if not n.get("seen", False))
except Exception as e: except Exception as e:
error = str(e) error = str(e)
logger.error(e) logger.error(e)
@@ -245,7 +290,7 @@ async def notification_mark_seen(_: None, info: GraphQLResolveInfo, notification
@mutation.field("notifications_seen_after") @mutation.field("notifications_seen_after")
@login_required @login_required
async def notifications_seen_after(_: None, info: GraphQLResolveInfo, after: int) -> dict: async def notifications_seen_after(_: None, info: GraphQLResolveInfo, after: int) -> dict:
# TODO: use latest loaded notification_id as input offset parameter """Mark all notifications after given timestamp as seen."""
error = None error = None
try: try:
author_id = info.context.get("author", {}).get("id") author_id = info.context.get("author", {}).get("id")
@@ -273,18 +318,64 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
error = None error = None
author_id = info.context.get("author", {}).get("id") author_id = info.context.get("author", {}).get("id")
if author_id: if author_id:
[shout_id, reply_to_id] = thread.split(":")
with local_session() as session: with local_session() as session:
# Convert Unix timestamp to datetime for PostgreSQL compatibility # Convert Unix timestamp to datetime for PostgreSQL compatibility
after_datetime = datetime.fromtimestamp(after, tz=UTC) if after else None after_datetime = datetime.fromtimestamp(after, tz=UTC) if after else None
# TODO: handle new follower and new shout notifications # Handle different thread types: shout reactions, followers, or new shouts
if thread == "followers":
# Mark follower notifications as seen
query_conditions = [
Notification.entity == NotificationEntity.AUTHOR.value,
]
if after_datetime:
query_conditions.append(Notification.created_at > after_datetime)
follower_notifications = session.query(Notification).where(and_(*query_conditions)).all()
for n in follower_notifications:
try:
ns = NotificationSeen(notification=n.id, viewer=author_id)
session.add(ns)
except Exception as e:
logger.warning(f"Failed to mark follower notification as seen: {e}")
session.commit()
return {"error": None}
# Handle shout and reaction notifications
thread_parts = thread.split(":")
if len(thread_parts) < 2:
return {"error": "Invalid thread format"}
shout_id = thread_parts[0]
reply_to_id = thread_parts[1] if len(thread_parts) > 1 else None
# Query for new shout notifications in this thread
shout_query_conditions = [
Notification.entity == NotificationEntity.SHOUT.value,
Notification.action == NotificationAction.CREATE.value,
]
if after_datetime:
shout_query_conditions.append(Notification.created_at > after_datetime)
shout_notifications = session.query(Notification).where(and_(*shout_query_conditions)).all()
# Mark relevant shout notifications as seen
for n in shout_notifications:
payload = orjson.loads(str(n.payload))
if str(payload.get("id")) == shout_id:
try:
ns = NotificationSeen(notification=n.id, viewer=author_id)
session.add(ns)
except Exception as e:
logger.warning(f"Failed to mark shout notification as seen: {e}")
# Query for reaction notifications
if after_datetime: if after_datetime:
new_reaction_notifications = ( new_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "create", Notification.action == NotificationAction.CREATE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
Notification.created_at > after_datetime, Notification.created_at > after_datetime,
) )
.all() .all()
@@ -292,8 +383,8 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
removed_reaction_notifications = ( removed_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "delete", Notification.action == NotificationAction.DELETE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
Notification.created_at > after_datetime, Notification.created_at > after_datetime,
) )
.all() .all()
@@ -302,16 +393,16 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
new_reaction_notifications = ( new_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "create", Notification.action == NotificationAction.CREATE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
) )
.all() .all()
) )
removed_reaction_notifications = ( removed_reaction_notifications = (
session.query(Notification) session.query(Notification)
.where( .where(
Notification.action == "delete", Notification.action == NotificationAction.DELETE.value,
Notification.entity == "reaction", Notification.entity == NotificationEntity.REACTION.value,
) )
.all() .all()
) )

View File

@@ -1,3 +1,4 @@
import asyncio
import time import time
import traceback import traceback
from typing import Any from typing import Any
@@ -143,27 +144,29 @@ def is_featured_author(session: Session, author_id: int) -> bool:
def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool: def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool:
""" """
Make a shout featured if it receives more than 4 votes from authors. Make a shout featured if it receives more than 4 votes from featured authors.
:param session: Database session. :param session: Database session.
:param approver_id: Approver author ID. :param approver_id: Approver author ID.
:param reaction: Reaction object. :param reaction: Reaction object.
:return: True if shout should be featured, else False. :return: True if shout should be featured, else False.
""" """
is_positive_kind = reaction.get("kind") == ReactionKind.LIKE.value # 🔧 Проверяем любую положительную реакцию (LIKE, ACCEPT, PROOF), не только LIKE
is_positive_kind = reaction.get("kind") in POSITIVE_REACTIONS
if not reaction.get("reply_to") and is_positive_kind: if not reaction.get("reply_to") and is_positive_kind:
# Проверяем, не содержит ли пост более 20% дизлайков # Проверяем, не содержит ли пост более 20% дизлайков
# Если да, то не должен быть featured независимо от количества лайков # Если да, то не должен быть featured независимо от количества лайков
if check_to_unfeature(session, reaction): if check_to_unfeature(session, reaction):
return False return False
# Собираем всех авторов, поставивших лайк # Собираем всех авторов, поставивших положительную реакцию
author_approvers = set() author_approvers = set()
reacted_readers = ( reacted_readers = (
session.query(Reaction.created_by) session.query(Reaction.created_by)
.where( .where(
Reaction.shout == reaction.get("shout"), Reaction.shout == reaction.get("shout"),
Reaction.kind.in_(POSITIVE_REACTIONS), Reaction.kind.in_(POSITIVE_REACTIONS),
Reaction.reply_to.is_(None), # не реакция на комментарий
# Рейтинги (LIKE, DISLIKE) физически удаляются, поэтому фильтр deleted_at не нужен # Рейтинги (LIKE, DISLIKE) физически удаляются, поэтому фильтр deleted_at не нужен
) )
.distinct() .distinct()
@@ -189,7 +192,7 @@ def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool
def check_to_unfeature(session: Session, reaction: dict) -> bool: def check_to_unfeature(session: Session, reaction: dict) -> bool:
""" """
Unfeature a shout if: Unfeature a shout if:
1. Less than 5 positive votes, OR 1. Less than 5 positive votes from featured authors, OR
2. 20% or more of reactions are negative. 2. 20% or more of reactions are negative.
:param session: Database session. :param session: Database session.
@@ -199,18 +202,8 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool:
if not reaction.get("reply_to"): if not reaction.get("reply_to"):
shout_id = reaction.get("shout") shout_id = reaction.get("shout")
# Проверяем соотношение дизлайков, даже если текущая реакция не дизлайк # 🔧 Считаем все рейтинговые реакции (положительные + отрицательные)
total_reactions = ( # Используем POSITIVE_REACTIONS + NEGATIVE_REACTIONS вместо только RATING_REACTIONS
session.query(Reaction)
.where(
Reaction.shout == shout_id,
Reaction.reply_to.is_(None),
Reaction.kind.in_(RATING_REACTIONS),
# Рейтинги физически удаляются при удалении, поэтому фильтр deleted_at не нужен
)
.count()
)
positive_reactions = ( positive_reactions = (
session.query(Reaction) session.query(Reaction)
.where( .where(
@@ -233,9 +226,13 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool:
.count() .count()
) )
total_reactions = positive_reactions + negative_reactions
# Условие 1: Меньше 5 голосов "за" # Условие 1: Меньше 5 голосов "за"
if positive_reactions < 5: if positive_reactions < 5:
logger.debug(f"Публикация {shout_id}: {positive_reactions} лайков (меньше 5) - должна быть unfeatured") logger.debug(
f"Публикация {shout_id}: {positive_reactions} положительных реакций (меньше 5) - должна быть unfeatured"
)
return True return True
# Условие 2: Проверяем, составляют ли отрицательные реакции 20% или более от всех реакций # Условие 2: Проверяем, составляют ли отрицательные реакции 20% или более от всех реакций
@@ -256,6 +253,8 @@ async def set_featured(session: Session, shout_id: int) -> None:
:param session: Database session. :param session: Database session.
:param shout_id: Shout ID. :param shout_id: Shout ID.
""" """
from cache.revalidator import revalidation_manager
s = session.query(Shout).where(Shout.id == shout_id).first() s = session.query(Shout).where(Shout.id == shout_id).first()
if s: if s:
current_time = int(time.time()) current_time = int(time.time())
@@ -267,6 +266,22 @@ async def set_featured(session: Session, shout_id: int) -> None:
session.add(s) session.add(s)
session.commit() session.commit()
# 🔧 Ревалидация кеша публикации и связанных сущностей
revalidation_manager.mark_for_revalidation(shout_id, "shouts")
# Ревалидируем авторов публикации
for author in s.authors:
revalidation_manager.mark_for_revalidation(author.id, "authors")
# Ревалидируем темы публикации
for topic in s.topics:
revalidation_manager.mark_for_revalidation(topic.id, "topics")
# 🔧 Инвалидируем ключи кеша лент для обновления featured статусов
from cache.cache import invalidate_shout_related_cache
await invalidate_shout_related_cache(s, s.created_by)
logger.info(f"Публикация {shout_id} получила статус featured, кеш помечен для ревалидации")
def set_unfeatured(session: Session, shout_id: int) -> None: def set_unfeatured(session: Session, shout_id: int) -> None:
""" """
@@ -275,9 +290,33 @@ def set_unfeatured(session: Session, shout_id: int) -> None:
:param session: Database session. :param session: Database session.
:param shout_id: Shout ID. :param shout_id: Shout ID.
""" """
from cache.revalidator import revalidation_manager
# Получаем публикацию для доступа к авторам и темам
shout = session.query(Shout).where(Shout.id == shout_id).first()
if not shout:
return
session.query(Shout).where(Shout.id == shout_id).update({"featured_at": None}) session.query(Shout).where(Shout.id == shout_id).update({"featured_at": None})
session.commit() session.commit()
# 🔧 Ревалидация кеша публикации и связанных сущностей
revalidation_manager.mark_for_revalidation(shout_id, "shouts")
# Ревалидируем авторов публикации
for author in shout.authors:
revalidation_manager.mark_for_revalidation(author.id, "authors")
# Ревалидируем темы публикации
for topic in shout.topics:
revalidation_manager.mark_for_revalidation(topic.id, "topics")
# 🔧 Инвалидируем ключи кеша лент для обновления featured статусов
from cache.cache import invalidate_shout_related_cache
# Используем asyncio.create_task для асинхронного вызова
asyncio.create_task(invalidate_shout_related_cache(shout, shout.created_by))
logger.info(f"Публикация {shout_id} потеряла статус featured, кеш помечен для ревалидации")
async def _create_reaction(session: Session, shout_id: int, is_author: bool, author_id: int, reaction: dict) -> dict: async def _create_reaction(session: Session, shout_id: int, is_author: bool, author_id: int, reaction: dict) -> dict:
""" """

View File

@@ -24,6 +24,7 @@ type Mutation {
# draft # draft
create_draft(draft_input: DraftInput!): CommonResult! create_draft(draft_input: DraftInput!): CommonResult!
create_draft_from_shout(shout_id: Int!): CommonResult!
update_draft(draft_id: Int!, draft_input: DraftInput!): CommonResult! update_draft(draft_id: Int!, draft_input: DraftInput!): CommonResult!
delete_draft(draft_id: Int!): CommonResult! delete_draft(draft_id: Int!): CommonResult!
# publication # publication

View File

@@ -245,6 +245,7 @@ type AuthorFollowsResult {
topics: [Topic] topics: [Topic]
authors: [Author] authors: [Author]
communities: [Community] communities: [Community]
shouts: [Shout]
error: String error: String
} }

View File

@@ -76,7 +76,7 @@ OAUTH_CLIENTS = {
} }
# Настройки JWT # Настройки JWT
JWT_SECRET = os.getenv("JWT_SECRET", "your-secret-key") JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key")
JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 30 JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 30
JWT_REFRESH_TOKEN_EXPIRE_DAYS = int(environ.get("JWT_REFRESH_TOKEN_EXPIRE_DAYS", "30")) JWT_REFRESH_TOKEN_EXPIRE_DAYS = int(environ.get("JWT_REFRESH_TOKEN_EXPIRE_DAYS", "30"))

View File

@@ -55,7 +55,7 @@ class EnvService:
"POSTGRES_HOST": "database", "POSTGRES_HOST": "database",
"POSTGRES_PORT": "database", "POSTGRES_PORT": "database",
# Auth # Auth
"JWT_SECRET": "auth", "JWT_SECRET_KEY": "auth",
"JWT_ALGORITHM": "auth", "JWT_ALGORITHM": "auth",
"JWT_EXPIRATION": "auth", "JWT_EXPIRATION": "auth",
"SECRET_KEY": "auth", "SECRET_KEY": "auth",
@@ -103,7 +103,7 @@ class EnvService:
# Секретные переменные (не показываем их значения в UI) # Секретные переменные (не показываем их значения в UI)
SECRET_VARIABLES: ClassVar[set[str]] = { SECRET_VARIABLES: ClassVar[set[str]] = {
"JWT_SECRET", "JWT_SECRET_KEY",
"SECRET_KEY", "SECRET_KEY",
"AUTH_SECRET", "AUTH_SECRET",
"OAUTH_GOOGLE_CLIENT_SECRET", "OAUTH_GOOGLE_CLIENT_SECRET",
@@ -127,7 +127,7 @@ class EnvService:
"POSTGRES_DB": "Имя базы данных PostgreSQL", "POSTGRES_DB": "Имя базы данных PostgreSQL",
"POSTGRES_HOST": "Хост PostgreSQL", "POSTGRES_HOST": "Хост PostgreSQL",
"POSTGRES_PORT": "Порт PostgreSQL", "POSTGRES_PORT": "Порт PostgreSQL",
"JWT_SECRET": "Секретный ключ для JWT токенов", "JWT_SECRET_KEY": "Секретный ключ для JWT токенов",
"JWT_ALGORITHM": "Алгоритм подписи JWT", "JWT_ALGORITHM": "Алгоритм подписи JWT",
"JWT_EXPIRATION": "Время жизни JWT токенов", "JWT_EXPIRATION": "Время жизни JWT токенов",
"SECRET_KEY": "Секретный ключ приложения", "SECRET_KEY": "Секретный ключ приложения",

View File

@@ -64,12 +64,5 @@ def start_sentry() -> None:
) )
logger.info("[utils.sentry] Sentry initialized successfully.") logger.info("[utils.sentry] Sentry initialized successfully.")
# 🧪 Отправляем тестовое событие для проверки работы GlitchTip
try:
sentry_sdk.capture_message("🧪 GlitchTip test message - система инициализирована", level="info")
logger.info("[utils.sentry] Тестовое сообщение отправлено в GlitchTip")
except Exception as test_e:
logger.warning(f"[utils.sentry] Не удалось отправить тестовое сообщение: {test_e}")
except (sentry_sdk.utils.BadDsn, ImportError, ValueError, TypeError) as _e: except (sentry_sdk.utils.BadDsn, ImportError, ValueError, TypeError) as _e:
logger.warning("[utils.sentry] Failed to initialize Sentry", exc_info=True) logger.warning("[utils.sentry] Failed to initialize Sentry", exc_info=True)

2
uv.lock generated
View File

@@ -443,7 +443,7 @@ wheels = [
[[package]] [[package]]
name = "discours-core" name = "discours-core"
version = "0.9.28" version = "0.9.32"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "ariadne" }, { name = "ariadne" },