@@ -0,0 +1,26 @@
|
|||||||
|
APP__ORIGINS=["http://locahost:3000"]
|
||||||
|
DB__URL=postgres://user:password@localhost:5432/tgex
|
||||||
|
|
||||||
|
LOGGER__APP_NAME=tgex-backend
|
||||||
|
LOGGER__APP_VERSION=0.0.0
|
||||||
|
LOGGER__PRETTY_CONSOLE=true
|
||||||
|
LOGGER__LEVEL=info
|
||||||
|
|
||||||
|
JWT__SECRET_KEY=your_secret_key_here_please_generate_a_strong_random_string
|
||||||
|
|
||||||
|
PARSER__URL=http://localhost:8080
|
||||||
|
|
||||||
|
S3__ENDPOINT_URL=http://localhost:9000
|
||||||
|
S3__ACCESS_KEY_ID=user
|
||||||
|
S3__SECRET_ACCESS_KEY=password
|
||||||
|
S3__BUCKET_NAME=tgex-files
|
||||||
|
|
||||||
|
TELEGRAM__TOKEN=your_bot_token_here
|
||||||
|
BACKEND__BASE_URL=http://localhost:8000
|
||||||
|
LOGIN_URL=https://app.example.com/api/v1/auth/complete?token=
|
||||||
|
|
||||||
|
TELEGRAM__API_ID=123456
|
||||||
|
TELEGRAM__API_HASH=your_api_hash_here
|
||||||
|
TELEGRAM__SESSION_FILE=/data/tg_parser.json
|
||||||
|
TELEGRAM__PHONE=+79999999999
|
||||||
|
TELEGRAM__PASSWORD=
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Build & Push
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
run: |
|
||||||
|
docker build -t ghcr.io/${{ github.repository }}/app:latest .
|
||||||
|
docker push ghcr.io/${{ github.repository }}/app:latest
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
docker-compose.yml
|
||||||
|
.uv_cache/
|
||||||
|
|
||||||
|
|
||||||
|
session.json
|
||||||
|
tg_parser.json
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a FastAPI + Telegram Bot backend application for managing Telegram channel integrations. The project uses:
|
||||||
|
- **FastAPI** for HTTP API endpoints
|
||||||
|
- **aiogram** for Telegram bot functionality
|
||||||
|
- **Tortoise ORM** (async) for database operations
|
||||||
|
- **PostgreSQL** with asyncpg driver
|
||||||
|
- **aerich** for database migrations
|
||||||
|
- **uv** for Python package management
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
```bash
|
||||||
|
# Install dependencies (using uv)
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Start PostgreSQL (via Docker Compose)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Run database migrations
|
||||||
|
make migrate-up # or: export $(cat .env | xargs) && aerich upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
```bash
|
||||||
|
# Start the FastAPI server
|
||||||
|
uv run uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
```bash
|
||||||
|
# Run linter (Ruff)
|
||||||
|
uv run ruff check .
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
uv run ruff format .
|
||||||
|
|
||||||
|
# Type checking (mypy with strict mode)
|
||||||
|
uv run mypy .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
uv run pytest tests/test_file.py
|
||||||
|
|
||||||
|
# Run a specific test
|
||||||
|
uv run pytest tests/test_file.py::test_function_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
```bash
|
||||||
|
# Create a new migration (auto-generated based on model changes)
|
||||||
|
make migrate-create # or: source .env && aerich migrate
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
make migrate-up # or: source .env && aerich upgrade
|
||||||
|
|
||||||
|
# Rollback one migration
|
||||||
|
make migrate-down # or: source .env && aerich downgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Migration Workflow:**
|
||||||
|
|
||||||
|
1. **For simple model changes**: Let aerich auto-generate the migration:
|
||||||
|
```bash
|
||||||
|
source .env && aerich migrate --name "descriptive_name"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **For complex refactorings** (table renames, data migration, etc.):
|
||||||
|
- **FIRST**: Update domain models to reflect the new structure
|
||||||
|
- **THEN**: Run `aerich migrate` to auto-generate the migration file
|
||||||
|
- **FINALLY**: Edit the generated file to add data migration logic
|
||||||
|
|
||||||
|
Example workflow:
|
||||||
|
```bash
|
||||||
|
# 1. Update models in src/domain/
|
||||||
|
# 2. Generate migration (will create base ALTER TABLE statements)
|
||||||
|
source .env && aerich migrate --name "refactor_tables"
|
||||||
|
# 3. Edit the generated file in migrations/models/ to add:
|
||||||
|
# - Data migration SQL (INSERT ... SELECT)
|
||||||
|
# - Proper ordering of operations
|
||||||
|
# - Comments explaining complex logic
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **For custom constraints** (CHECK, custom indexes, etc.):
|
||||||
|
```bash
|
||||||
|
source .env && aerich migrate --name "add_check_constraint" --empty
|
||||||
|
# Then edit and add: # ruff: noqa and # mypy: ignore-errors at the top
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration Best Practices:**
|
||||||
|
- Always test migrations on a copy of production data
|
||||||
|
- Use transactions (`RUN_IN_TRANSACTION = True`)
|
||||||
|
- Add comments explaining complex data transformations
|
||||||
|
- For table renames with data migration:
|
||||||
|
1. Rename old table to `old_*`
|
||||||
|
2. Create new table structure
|
||||||
|
3. Migrate data with `INSERT INTO new SELECT ... FROM old`
|
||||||
|
4. Drop old table last
|
||||||
|
- Include proper downgrade logic (even if it loses some data)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Layered Architecture
|
||||||
|
|
||||||
|
The codebase follows a clean architecture pattern with clear separation of concerns:
|
||||||
|
|
||||||
|
1. **Domain Layer** (`src/domain/`)
|
||||||
|
- Tortoise ORM models that represent database entities
|
||||||
|
- All models inherit from `domain.base.TimestampedModel` which provides:
|
||||||
|
- Auto-generated `id` (UUID primary key)
|
||||||
|
- Timestamps: `created_at`, `updated_at`, `deleted_at`
|
||||||
|
- Timezone-aware datetime fields
|
||||||
|
- Use `class Meta: table = 'table_name'` for explicit table naming
|
||||||
|
|
||||||
|
2. **Use Case Layer** (`src/usecase/`)
|
||||||
|
- Business logic functions organized by feature
|
||||||
|
- Each use case is a standalone function with signature: `async def use_case_name(self: 'Usecase', ...) -> ReturnType`
|
||||||
|
- Depends on Protocol interfaces (Database, TelegramBotWriter, JWTEncoder)
|
||||||
|
- Use cases are assembled in the `Usecase` dataclass for dependency injection
|
||||||
|
- **IMPORTANT RULES:**
|
||||||
|
- Один use case отвечает за полноценный сценарий «от триггера до завершения»
|
||||||
|
- Use case НЕ должен вызывать другие use cases (общую логику выносить в хелперы/сервисы)
|
||||||
|
- Use case содержит ВСЮ бизнес-логику, включая управление состояниями диалога
|
||||||
|
- Use case может отправлять сообщения пользователям через `self.telegram_bot` protocol
|
||||||
|
- Всегда оборачивать операции БД в `async with self.database.transaction()`
|
||||||
|
|
||||||
|
3. **Adapter Layer** (`src/adapter/`)
|
||||||
|
- Concrete implementations of protocol interfaces:
|
||||||
|
- `Postgres`: Database operations (implements Database protocol)
|
||||||
|
- `Telegram`: Telegram bot integration (implements TelegramWriter protocol)
|
||||||
|
- `JWT`: Token encoding/decoding (implements JWTEncoder protocol)
|
||||||
|
|
||||||
|
4. **Controller Layer** (`src/controller/`)
|
||||||
|
- **HTTP controllers** (`src/controller/http/`): FastAPI route handlers
|
||||||
|
- **Telegram callbacks** (`src/controller/telegram_callback/`): Telegram event handlers
|
||||||
|
- **ВАЖНО: Controllers - это тонкий слой роутинга**
|
||||||
|
- Контроллер только извлекает данные из событий (HTTP request / Telegram update)
|
||||||
|
- Валидирует базовые параметры (user exists, message not empty)
|
||||||
|
- Вызывает соответствующий use case
|
||||||
|
- НЕ содержит бизнес-логику
|
||||||
|
- НЕ обращается к базе данных напрямую
|
||||||
|
- НЕ отправляет сообщения пользователям
|
||||||
|
- Все handlers регистрируются через декораторы на `telegram_callback_router`
|
||||||
|
- Handlers импортируются в `__init__.py` для auto-регистрации
|
||||||
|
|
||||||
|
5. **DTO Layer** (`src/dto/`)
|
||||||
|
- Pydantic models for request/response validation
|
||||||
|
- Separate from domain models to decouple API contracts from database schema
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
|
||||||
|
**Dependency Injection via Protocols:**
|
||||||
|
- Use cases depend on Protocol interfaces, not concrete implementations
|
||||||
|
- Allows for easy testing and swapping implementations
|
||||||
|
- See `src/usecase/__init__.py` for protocol definitions
|
||||||
|
|
||||||
|
**Transaction Management:**
|
||||||
|
- Database uses context-aware transactions via `DatabaseBase.transaction()`
|
||||||
|
- Session stored in ContextVar (`_session_ctx`) for implicit session access
|
||||||
|
- Always use `async with database.transaction()` for database operations
|
||||||
|
- The session is accessible via `database.session` within transaction context
|
||||||
|
|
||||||
|
**Shared Base Classes:**
|
||||||
|
- `shared/datebase_base.py`: Base database class with connection pooling and migration checking
|
||||||
|
- `shared/telegram_base.py`: Base Telegram bot class with polling lifecycle
|
||||||
|
- `shared/logger/`: Structured logging with JSON and console formatters
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- All config in `src/config.py` using Pydantic Settings
|
||||||
|
- Environment variables loaded from `.env` file
|
||||||
|
- Nested config via double underscore: `DB__URL`, `TELEGRAM__TOKEN`
|
||||||
|
|
||||||
|
### Application Lifecycle
|
||||||
|
|
||||||
|
1. **Startup** (in `src/main.py`):
|
||||||
|
- Logger initialized
|
||||||
|
- Adapters instantiated (Postgres, Telegram, JWT)
|
||||||
|
- Usecase dataclass created with adapter dependencies
|
||||||
|
- FastAPI lifespan context:
|
||||||
|
- Connect to database (includes migration check)
|
||||||
|
- Start Telegram bot polling
|
||||||
|
|
||||||
|
2. **Request Flow**:
|
||||||
|
- HTTP: FastAPI route → Controller → Usecase → Adapter → Database
|
||||||
|
- Telegram: Event → Telegram callback → Usecase → Adapter → Database
|
||||||
|
|
||||||
|
3. **Shutdown**:
|
||||||
|
- Stop Telegram bot polling
|
||||||
|
- Close database connections
|
||||||
|
|
||||||
|
### Important Implementation Details
|
||||||
|
|
||||||
|
**Database Session Management:**
|
||||||
|
- DO NOT create sessions manually
|
||||||
|
- Use `async with database.transaction()` which handles begin/commit/rollback
|
||||||
|
- Access session via `database.session` property within transaction context
|
||||||
|
- Session lifecycle is managed by ContextVars for thread-safety
|
||||||
|
|
||||||
|
**Telegram Bot Integration:**
|
||||||
|
- Bot runs in polling mode (not webhook)
|
||||||
|
- Routers registered in `src/controller/telegram_callback/`
|
||||||
|
- Base class handles graceful shutdown of polling task
|
||||||
|
|
||||||
|
**Authentication:**
|
||||||
|
- JWT-based authentication for HTTP endpoints
|
||||||
|
- Login flow: Telegram bot → Login token → JWT access token
|
||||||
|
- Use `get_current_user` dependency for protected routes
|
||||||
|
|
||||||
|
**Code Style:**
|
||||||
|
- Single quotes for strings (configured in Ruff)
|
||||||
|
- Line length: 120 characters
|
||||||
|
- Python 3.13+ syntax
|
||||||
|
- Strict mypy typing enabled
|
||||||
|
|
||||||
|
### Telegram Bot Patterns
|
||||||
|
|
||||||
|
**State Management:**
|
||||||
|
- Состояния хранятся в БД через модель `TelegramState`
|
||||||
|
- Используется `TelegramStateEnum` для типизации состояний (не строки!)
|
||||||
|
- Database protocol методы: `get_telegram_state()`, `set_telegram_state()`, `clear_telegram_state()`
|
||||||
|
|
||||||
|
**Controller Pattern (Telegram):**
|
||||||
|
```python
|
||||||
|
from aiogram import F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
@telegram_callback_router.message(Command('command_name'))
|
||||||
|
async def cmd_handler(message: Message) -> None:
|
||||||
|
if not message.from_user:
|
||||||
|
log.error('Failed to get user data')
|
||||||
|
return
|
||||||
|
|
||||||
|
usecase = dependencies.get_usecase()
|
||||||
|
await usecase.some_use_case(
|
||||||
|
telegram_id=message.from_user.id,
|
||||||
|
chat_id=message.chat.id
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case Pattern (Telegram):**
|
||||||
|
```python
|
||||||
|
from src import domain
|
||||||
|
|
||||||
|
async def some_use_case(self: 'Usecase', telegram_id: int, chat_id: int) -> None:
|
||||||
|
async with self.database.transaction():
|
||||||
|
# 1. Получить/проверить пользователя
|
||||||
|
user = await self.database.get_user(telegram_id=telegram_id)
|
||||||
|
if not user:
|
||||||
|
await self.telegram_bot.send_message('❌ Не авторизован', chat_id=chat_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Получить состояние (если нужно)
|
||||||
|
state = await self.database.get_telegram_state(telegram_id)
|
||||||
|
|
||||||
|
# 3. Бизнес-логика
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# 4. Обновить состояние
|
||||||
|
await self.database.set_telegram_state(
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
state=domain.TelegramStateEnum.SOME_STATE,
|
||||||
|
context={'key': 'value'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Отправить сообщение
|
||||||
|
await self.telegram_bot.send_message('✅ Успешно', chat_id=chat_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inline Keyboard Pattern:**
|
||||||
|
```python
|
||||||
|
from aiogram.types import InlineKeyboardButton
|
||||||
|
|
||||||
|
# В use case:
|
||||||
|
buttons = [
|
||||||
|
[InlineKeyboardButton(text='Кнопка', callback_data='callback_id:value')]
|
||||||
|
for item in items
|
||||||
|
]
|
||||||
|
await self.telegram_bot.send_message_with_inline_keyboard(
|
||||||
|
'Выберите опцию:', chat_id=chat_id, buttons=buttons
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Callback Query Handler:**
|
||||||
|
```python
|
||||||
|
@telegram_callback_router.callback_query(F.data.startswith('prefix:'))
|
||||||
|
async def callback_handler(callback: CallbackQuery) -> None:
|
||||||
|
if not callback.from_user or not callback.data:
|
||||||
|
return
|
||||||
|
|
||||||
|
usecase = dependencies.get_usecase()
|
||||||
|
await usecase.handle_callback(
|
||||||
|
telegram_id=callback.from_user.id,
|
||||||
|
chat_id=callback.message.chat.id,
|
||||||
|
callback_data=callback.data,
|
||||||
|
message_id=callback.message.message_id
|
||||||
|
)
|
||||||
|
await callback.answer() # Убрать "loading" на кнопке
|
||||||
|
```
|
||||||
|
|
||||||
|
**Text Message Filters:**
|
||||||
|
```python
|
||||||
|
# Обрабатывать только не-команды
|
||||||
|
@telegram_callback_router.message(F.text & ~F.text.startswith('/'))
|
||||||
|
async def text_handler(message: Message) -> None:
|
||||||
|
# Этот handler НЕ сработает для команд типа /start
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важные правила для Telegram handlers:**
|
||||||
|
- Специфичные фильтры (Command) регистрировать РАНЬШЕ общих (F.text)
|
||||||
|
- Использовать `~F.text.startswith('/')` для исключения команд в текстовых handlers
|
||||||
|
- Всегда проверять `message.from_user` перед использованием
|
||||||
|
- Callback handlers должны вызывать `await callback.answer()` в конце
|
||||||
|
- НЕ импортировать aiogram типы в usecase layer (только через TYPE_CHECKING в Protocol)
|
||||||
|
|
||||||
|
### Adding New Protocol Methods
|
||||||
|
|
||||||
|
When adding new functionality that requires protocol methods:
|
||||||
|
|
||||||
|
**1. Define Protocol in usecase layer (`src/usecase/__init__.py`):**
|
||||||
|
```python
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from aiogram.types import InlineKeyboardButton # Импорт только для типов
|
||||||
|
|
||||||
|
class TelegramBotWriter(typing.Protocol):
|
||||||
|
async def send_message_with_inline_keyboard(
|
||||||
|
self, text: str, chat_id: int, buttons: list[list['InlineKeyboardButton']]
|
||||||
|
) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Implement in adapter (`src/adapter/telegram_bot.py`):**
|
||||||
|
```python
|
||||||
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
|
||||||
|
class TelegramBot(TelegramBase):
|
||||||
|
async def send_message_with_inline_keyboard(
|
||||||
|
self, text: str, chat_id: int, buttons: list[list[InlineKeyboardButton]]
|
||||||
|
) -> None:
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
await self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard)
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Use in use case:**
|
||||||
|
```python
|
||||||
|
from aiogram.types import InlineKeyboardButton # Можно импортировать в use case
|
||||||
|
|
||||||
|
buttons = [[InlineKeyboardButton(text='Text', callback_data='data')]]
|
||||||
|
await self.telegram_bot.send_message_with_inline_keyboard('Message', chat_id, buttons)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Типизация Enum в Domain:**
|
||||||
|
- Используйте `enum.StrEnum` для строковых enum (Python 3.11+)
|
||||||
|
- Enum значения в lowercase с underscores
|
||||||
|
- Используйте `fields.CharEnumField()` в Tortoise ORM
|
||||||
|
|
||||||
|
```python
|
||||||
|
import enum
|
||||||
|
from tortoise import fields
|
||||||
|
from .base import TimestampedModel
|
||||||
|
|
||||||
|
class TelegramStateEnum(enum.StrEnum):
|
||||||
|
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
|
||||||
|
CREATIVE_WAITING_NAME = 'creative_waiting_name'
|
||||||
|
|
||||||
|
class TelegramState(TimestampedModel):
|
||||||
|
state = fields.CharEnumField(TelegramStateEnum, max_length=26)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'telegram_state'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Constraints:**
|
||||||
|
- Foreign keys: Use `fields.ForeignKeyField()` with `on_delete=fields.CASCADE`
|
||||||
|
- Unique constraints: Use `unique=True` or `unique_together` in Meta
|
||||||
|
- CHECK constraints: Cannot be defined in Tortoise models - create manual migrations
|
||||||
|
- Indexes: Use `index=True` on fields or define in Meta
|
||||||
|
|
||||||
|
Example with CHECK constraint:
|
||||||
|
```bash
|
||||||
|
# Create empty migration
|
||||||
|
export $(cat .env | xargs) && aerich migrate --name "add_check_constraint" --empty
|
||||||
|
```
|
||||||
|
|
||||||
|
Then edit the created file:
|
||||||
|
```python
|
||||||
|
# migrations/models/1_xxx_add_check_constraint.py
|
||||||
|
# ruff: noqa
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "table_name"
|
||||||
|
ADD CONSTRAINT "constraint_name"
|
||||||
|
CHECK (your_condition);
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "table_name"
|
||||||
|
DROP CONSTRAINT IF EXISTS "constraint_name";
|
||||||
|
"""
|
||||||
|
```
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
FROM python:3.13-slim-bookworm
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
RUN uv sync --frozen
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
COPY shared ./shared
|
||||||
|
COPY migrations ./migrations
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/shared:/src
|
||||||
|
ARG GIT_COMMIT=unknown
|
||||||
|
ENV GIT_COMMIT=${GIT_COMMIT}
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD [".venv/bin/uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
test:
|
||||||
|
pytest --no-header
|
||||||
|
|
||||||
|
build-parser:
|
||||||
|
docker build --platform linux/amd64 -f ./tg_parser/Dockerfile -t tg-parser:latest .
|
||||||
|
docker save tg-parser:latest -o tg-parser-x86.tar
|
||||||
|
|
||||||
|
env:
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
run:
|
||||||
|
uv run uvicorn src.main:app
|
||||||
|
|
||||||
|
up:
|
||||||
|
GIT_COMMIT_COUNT=$$(git rev-list --count HEAD 2>/dev/null) docker compose up --build -d --force-recreate
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
migrate-init:
|
||||||
|
export $$(cat .env | xargs) && aerich init-db
|
||||||
|
|
||||||
|
migrate-create:
|
||||||
|
export $$(cat .env | xargs) && aerich migrate
|
||||||
|
|
||||||
|
migrate-up:
|
||||||
|
export $$(cat .env | xargs) && aerich upgrade
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@docker rm -f $$(docker ps -aq) 2>/dev/null || true
|
||||||
|
@docker rmi -f $$(docker images --filter "dangling=true" -q) 2>/dev/null || true
|
||||||
|
@docker images --format "{{.Repository}}:{{.Tag}}" | grep -E "^(crm|local)" | xargs -r docker rmi -f 2>/dev/null || true
|
||||||
|
@docker volume rm $$(docker volume ls -q) 2>/dev/null || true
|
||||||
|
@docker network rm $$(docker network ls -q | grep -vE 'bridge|host|none') 2>/dev/null || true
|
||||||
|
@docker system prune --volumes -f
|
||||||
|
@echo "${GREEN}Docker очистка завершена. Базовые образы из registry сохранены.${NC}"
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# tgex-backend
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установить зависимости
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Скопировать примеры конфигов
|
||||||
|
cp docker-compose.yml.example docker-compose.yml
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Поднять Postgres и сервисы
|
||||||
|
make up
|
||||||
|
|
||||||
|
# (первый запуск) проинициализировать базу и таблицы aerich
|
||||||
|
make migrate-init
|
||||||
|
|
||||||
|
# Применить актуальные миграции
|
||||||
|
make migrate-up
|
||||||
|
```
|
||||||
|
|
||||||
|
Открыть:
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- Swagger: http://localhost:8000/docs
|
||||||
|
|
||||||
|
## Полезные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make up # поднять приложение и Postgres
|
||||||
|
make down # остановить контейнеры
|
||||||
|
make migrate-init # однократная инициализация базы (aerich init-db)
|
||||||
|
make migrate-up # применить миграции (aerich upgrade)
|
||||||
|
make migrate-create # сгенерировать новую миграцию (aerich migrate)
|
||||||
|
make test # pytest
|
||||||
|
uv run ruff check . # линтер
|
||||||
|
uv run mypy . # типизация
|
||||||
|
```
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
- FastAPI + PostgreSQL + Tortoise ORM, aiogram 3.x для бота
|
||||||
|
- Слои: `domain` (модели/правила), `usecase` (бизнес-логика), `adapter` (БД/бот/JWT), `controller` (HTTP + Telegram), `dto`
|
||||||
|
- Все use case оборачиваются в `self.database.transaction()` и проверяют права через `domain.WorkspacePermissions`
|
||||||
|
- Миграции и схема БД управляются aerich (`migrations/models`)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: "user"
|
||||||
|
POSTGRES_PASSWORD: "password"
|
||||||
|
POSTGRES_DB: "tgex"
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: "user"
|
||||||
|
MINIO_ROOT_PASSWORD: "password"
|
||||||
|
ports:
|
||||||
|
- "9001:9001" # Web Console
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- minio
|
||||||
|
environment:
|
||||||
|
DB__URL: "postgres://user:password@postgres:5432/tgex"
|
||||||
|
S3__ENDPOINT_URL: "http://minio:9000"
|
||||||
|
PARSER__URL: "http://tg_parser:8080"
|
||||||
|
GIT_COMMIT_COUNT: "${GIT_COMMIT_COUNT:-}"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
tg_bot:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: tg_bot/Dockerfile
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
environment:
|
||||||
|
BACKEND__BASE_URL: "http://backend:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
tg_parser:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: tg_parser/Dockerfile
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
DB__URL: "postgres://user:password@postgres:5432/tgex"
|
||||||
|
LOGGER__LEVEL: "info"
|
||||||
|
LOGGER__PRETTY_CONSOLE: "true"
|
||||||
|
expose:
|
||||||
|
- 8080
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./tg_parser.json:/data/tg_parser.json
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "channel" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"telegram_id" BIGINT NOT NULL UNIQUE,
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"username" VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
"access_hash" BIGINT,
|
||||||
|
"pts" INT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_channel_telegra_414c3b" ON "channel" ("telegram_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_channel_usernam_7fd87a" ON "channel" ("username");
|
||||||
|
CREATE TABLE IF NOT EXISTS "post" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"message_id" INT NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"deleted_from_channel_at" TIMESTAMPTZ,
|
||||||
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_post_channel_0a2cba" UNIQUE ("channel_id", "message_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_post_channel_197bed" ON "post" ("channel_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "post_views_history" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"views_count" INT NOT NULL,
|
||||||
|
"fetched_at" TIMESTAMPTZ NOT NULL,
|
||||||
|
"post_id" UUID NOT NULL REFERENCES "post" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_post_views__post_id_c6cc9f" UNIQUE ("post_id", "fetched_at")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_post_views__fetched_103eb5" ON "post_views_history" ("fetched_at");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_post_views__post_id_dfb1ca" ON "post_views_history" ("post_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "telegram_user" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"telegram_id" BIGINT NOT NULL UNIQUE,
|
||||||
|
"username" VARCHAR(255),
|
||||||
|
"first_name" VARCHAR(255),
|
||||||
|
"last_name" VARCHAR(255)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_telegram_us_telegra_3a3100" ON "telegram_user" ("telegram_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"telegram_user_id" UUID NOT NULL UNIQUE REFERENCES "telegram_user" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_user_telegra_444a03" ON "user" ("telegram_user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "login_token" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"token" VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
"expires_at" TIMESTAMPTZ NOT NULL,
|
||||||
|
"used_at" TIMESTAMPTZ,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_login_token_user_id_c85d7c" ON "login_token" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"name" VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "project" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"purchase_invite_type_default" VARCHAR(10) NOT NULL DEFAULT 'approval',
|
||||||
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_project_workspa_2f80f2" UNIQUE ("workspace_id", "channel_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_project_channel_ce2466" ON "project" ("channel_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_project_workspa_2354fd" ON "project" ("workspace_id");
|
||||||
|
COMMENT ON COLUMN "project"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived';
|
||||||
|
COMMENT ON COLUMN "project"."purchase_invite_type_default" IS 'PUBLIC: public\nAPPROVAL: approval';
|
||||||
|
CREATE TABLE IF NOT EXISTS "creative" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"name" VARCHAR(255) NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"media_type" VARCHAR(32),
|
||||||
|
"media_file_id" VARCHAR(512),
|
||||||
|
"media_s3_key" VARCHAR(512),
|
||||||
|
"buttons" JSONB NOT NULL,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_creative_project_0766a6" ON "creative" ("project_id");
|
||||||
|
COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
||||||
|
CREATE TABLE IF NOT EXISTS "purchase" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"placement_at" TIMESTAMPTZ,
|
||||||
|
"payment_at" TIMESTAMPTZ,
|
||||||
|
"cost_type" VARCHAR(8),
|
||||||
|
"cost_value" DOUBLE PRECISION,
|
||||||
|
"cost_before_bargain" DOUBLE PRECISION,
|
||||||
|
"purchase_type" VARCHAR(16),
|
||||||
|
"format" TEXT,
|
||||||
|
"comment" TEXT,
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_creativ_6829cd" ON "purchase" ("creative_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_project_95b1c2" ON "purchase" ("project_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_project_dee224" ON "purchase" ("project_id", "status");
|
||||||
|
COMMENT ON COLUMN "purchase"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
||||||
|
COMMENT ON COLUMN "purchase"."cost_type" IS 'FIXED: fixed\nCPM: cpm';
|
||||||
|
COMMENT ON COLUMN "purchase"."purchase_type" IS 'SELF_PROMO: self_promo\nSTANDARD: standard';
|
||||||
|
CREATE TABLE IF NOT EXISTS "purchase_channel" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
|
||||||
|
"placement_at" TIMESTAMPTZ,
|
||||||
|
"cost_type" VARCHAR(8),
|
||||||
|
"cost_value" DOUBLE PRECISION,
|
||||||
|
"cost_before_bargain" DOUBLE PRECISION,
|
||||||
|
"format" TEXT,
|
||||||
|
"comment" TEXT,
|
||||||
|
"invite_link" VARCHAR(512) NOT NULL,
|
||||||
|
"invite_link_type" VARCHAR(8) NOT NULL,
|
||||||
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
|
"purchase_id" UUID NOT NULL REFERENCES "purchase" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_purchase_ch_purchas_f7c86e" UNIQUE ("purchase_id", "channel_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_ch_channel_2b1264" ON "purchase_channel" ("channel_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_ch_purchas_1fe84a" ON "purchase_channel" ("purchase_id");
|
||||||
|
COMMENT ON COLUMN "purchase_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed';
|
||||||
|
COMMENT ON COLUMN "purchase_channel"."cost_type" IS 'FIXED: fixed\nCPM: cpm';
|
||||||
|
COMMENT ON COLUMN "purchase_channel"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
|
||||||
|
CREATE TABLE IF NOT EXISTS "placement" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"wanted_placement_date" TIMESTAMPTZ NOT NULL,
|
||||||
|
"cost" DOUBLE PRECISION,
|
||||||
|
"comment" TEXT,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
|
"post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL,
|
||||||
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
|
||||||
|
"purchase_channel_id" UUID NOT NULL REFERENCES "purchase_channel" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_post_id_e4a16e" ON "placement" ("post_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_project_2c6aa4" ON "placement" ("project_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_purchas_61f9e6" ON "placement" ("purchase_channel_id");
|
||||||
|
COMMENT ON COLUMN "placement"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
||||||
|
CREATE TABLE IF NOT EXISTS "subscription" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"invite_link" VARCHAR(512) NOT NULL,
|
||||||
|
"status" VARCHAR(12) NOT NULL DEFAULT 'active',
|
||||||
|
"unsubscribed_at" TIMESTAMPTZ,
|
||||||
|
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE,
|
||||||
|
"telegram_user_id" UUID NOT NULL REFERENCES "telegram_user" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_subscriptio_placeme_75b40b" UNIQUE ("placement_id", "telegram_user_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_subscriptio_invite__f2058d" ON "subscription" ("invite_link");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_subscriptio_placeme_5decb2" ON "subscription" ("placement_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_subscriptio_telegra_8be9bb" ON "subscription" ("telegram_user_id");
|
||||||
|
COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed';
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_invite" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'pending',
|
||||||
|
"invited_by_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_workspace_i_workspa_abd9c4" UNIQUE ("workspace_id", "user_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_i_invited_3ed9f8" ON "workspace_invite" ("invited_by_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_i_user_id_a96e1e" ON "workspace_invite" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_i_workspa_71e9d2" ON "workspace_invite" ("workspace_id");
|
||||||
|
COMMENT ON COLUMN "workspace_invite"."status" IS 'PENDING: pending\nACCEPTED: accepted\nREVOKED: revoked';
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_user" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(7) NOT NULL DEFAULT 'active',
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_workspace_u_workspa_13c46c" UNIQUE ("workspace_id", "user_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_user_id_246318" ON "workspace_user" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_a403b0" ON "workspace_user" ("workspace_id");
|
||||||
|
COMMENT ON COLUMN "workspace_user"."status" IS 'ACTIVE: active\nINVITED: invited\nBLOCKED: blocked';
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_user_permission" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"permission" VARCHAR(24) NOT NULL,
|
||||||
|
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_workspace_u_workspa_86f574" UNIQUE ("workspace_user_id", "permission")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_b9b7b0" ON "workspace_user_permission" ("workspace_user_id");
|
||||||
|
COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"permission" VARCHAR(24) NOT NULL,
|
||||||
|
"channel_id" UUID REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
|
"creative_id" UUID REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
|
"placement_id" UUID REFERENCES "placement" ("id") ON DELETE CASCADE,
|
||||||
|
"project_id" UUID REFERENCES "project" ("id") ON DELETE CASCADE,
|
||||||
|
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_channel_d94b48" ON "workspace_user_permission_scope" ("channel_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_creativ_8da562" ON "workspace_user_permission_scope" ("creative_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_placeme_ea1846" ON "workspace_user_permission_scope" ("placement_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_project_bd7fea" ON "workspace_user_permission_scope" ("project_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_82a35a" ON "workspace_user_permission_scope" ("workspace_user_id");
|
||||||
|
COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
|
||||||
|
CREATE TABLE IF NOT EXISTS "aerich" (
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"version" VARCHAR(255) NOT NULL,
|
||||||
|
"app" VARCHAR(100) NOT NULL,
|
||||||
|
"content" JSONB NOT NULL
|
||||||
|
);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
'v0'
|
||||||
|
)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP INDEX IF EXISTS "idx_workspace_u_channel_d94b48";
|
||||||
|
ALTER TABLE "workspace_user_permission_scope" DROP CONSTRAINT IF EXISTS "fk_workspac_channel_e039fdfd";
|
||||||
|
ALTER TABLE "creative" ADD "created_by_user_id" UUID;
|
||||||
|
COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full
|
||||||
|
PROJECTS_READ: projects_read
|
||||||
|
PROJECTS_WRITE: projects_write
|
||||||
|
CREATIVES_READ: creatives_read
|
||||||
|
CREATIVES_WRITE: creatives_write
|
||||||
|
PLACEMENTS_READ: placements_read
|
||||||
|
PLACEMENTS_WRITE: placements_write
|
||||||
|
ANALYTICS_READ: analytics_read
|
||||||
|
ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks
|
||||||
|
ANALYTICS_OWN_CREATIVES: analytics_own_creatives';
|
||||||
|
ALTER TABLE "workspace_user_permission_scope" DROP COLUMN "channel_id";
|
||||||
|
COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full
|
||||||
|
PROJECTS_READ: projects_read
|
||||||
|
PROJECTS_WRITE: projects_write
|
||||||
|
CREATIVES_READ: creatives_read
|
||||||
|
CREATIVES_WRITE: creatives_write
|
||||||
|
PLACEMENTS_READ: placements_read
|
||||||
|
PLACEMENTS_WRITE: placements_write
|
||||||
|
ANALYTICS_READ: analytics_read
|
||||||
|
ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks
|
||||||
|
ANALYTICS_OWN_CREATIVES: analytics_own_creatives';
|
||||||
|
ALTER TABLE "creative" ADD CONSTRAINT "fk_creative_user_2ed7d825" FOREIGN KEY ("created_by_user_id") REFERENCES "user" ("id") ON DELETE SET NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_creative_created_2d9564" ON "creative" ("created_by_user_id");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "uid_creative_me_creativ_b07496" ON "creative_media" ("creative_id", "position");"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP INDEX IF EXISTS "uid_creative_me_creativ_b07496";
|
||||||
|
DROP INDEX IF EXISTS "idx_creative_created_2d9564";
|
||||||
|
ALTER TABLE "creative" DROP CONSTRAINT IF EXISTS "fk_creative_user_2ed7d825";
|
||||||
|
ALTER TABLE "creative" DROP COLUMN "created_by_user_id";
|
||||||
|
COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full
|
||||||
|
PROJECTS_READ: projects_read
|
||||||
|
PROJECTS_WRITE: projects_write
|
||||||
|
CREATIVES_READ: creatives_read
|
||||||
|
CREATIVES_WRITE: creatives_write
|
||||||
|
PLACEMENTS_READ: placements_read
|
||||||
|
PLACEMENTS_WRITE: placements_write
|
||||||
|
ANALYTICS_READ: analytics_read
|
||||||
|
ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks
|
||||||
|
CHANNELS_READ: channels_read
|
||||||
|
CHANNELS_WRITE: channels_write';
|
||||||
|
ALTER TABLE "workspace_user_permission_scope" ADD "channel_id" UUID;
|
||||||
|
COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full
|
||||||
|
PROJECTS_READ: projects_read
|
||||||
|
PROJECTS_WRITE: projects_write
|
||||||
|
CREATIVES_READ: creatives_read
|
||||||
|
CREATIVES_WRITE: creatives_write
|
||||||
|
PLACEMENTS_READ: placements_read
|
||||||
|
PLACEMENTS_WRITE: placements_write
|
||||||
|
ANALYTICS_READ: analytics_read
|
||||||
|
ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks
|
||||||
|
CHANNELS_READ: channels_read
|
||||||
|
CHANNELS_WRITE: channels_write';
|
||||||
|
ALTER TABLE "workspace_user_permission_scope" ADD CONSTRAINT "fk_workspac_channel_e039fdfd" FOREIGN KEY ("channel_id") REFERENCES "channel" ("id") ON DELETE CASCADE;
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_channel_d94b48" ON "workspace_user_permission_scope" ("channel_id");"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"eJztXWtz2rga/isMn7ozOZ0ESMhmzpwZQmjXWwIMkLS7TcdjjEJ8Ymxqm6SZnfz3lXzBkm"
|
||||||
|
"yDb4AN7xeaSnpl+dH1efRK/qc616dINT+2nyRNQ2r1qvJPVZPmCP/BR51UqtJi4UeQAEua"
|
||||||
|
"qHZamUo0MS1Dki0c/CipJsJBU2TKhrKwFF3DodpSVUmgLuOEijbzg5aa8nOJREufIesJGT"
|
||||||
|
"ji+w8crGhT9AuZ3n8Xz+KjgtQpU1hlSp5th4vW28IOu7sTbj7ZKcnjJqKsq8u55qdevFlP"
|
||||||
|
"urZKvlwq04/EhsTNkIYMyUJT6jVIKd0X9oKcEuMAy1iiVVGnfsAUPUpLlYBR/e/jUpMJBh"
|
||||||
|
"X7SeSn8b9qAnhkXSPQKppFsPjn3Xkr/53t0Cp5VPuP1vBD/eI3+y1105oZdqSNSPXdNpQs"
|
||||||
|
"yTG1cfWBlA1EXluUrCCgNzjGUuYoHFTWkgN36pp+9P5IA7IX4KPstzAPZg++dJhW8TtM+5"
|
||||||
|
"r65tbgGozHwm1nNG7dDsibzE3zp2pD1Bp3SEzNDn3jQj84VaLj/uH0m1Umla/C+I8K+W/l"
|
||||||
|
"736vw1fcKt347yopk7S0dFHTX0VpSjU2L9QDBqf0K3a5mKasWNYSKnavFesW3q9XPBajdP"
|
||||||
|
"XKWuZQr25pd1itJalG77XXdlALV8fMkOZi2Fx2rcwEzQqvR86Qq0gM1XYmtIwdckae8p/f"
|
||||||
|
"a7V6vVk7rV9cnjeazfPL00uc1i5SMKq5prqvhc9Cb8zWHgl4ZzFWLBUF0cVrHCMCW8+AQx"
|
||||||
|
"W/SkEHurn0S1SRNrOe8H9r5+drMLtvDe2VAk7FtfueG1Vz4lgQlyYy7L8T4EjbpIKSH1u2"
|
||||||
|
"3j63j6Mky8g0xSfJfErW4TnDVB1+92P1fnr8wjKD4EYi66YuF6K1s0azcVm/aKyAXIWswy"
|
||||||
|
"+IlaK9KBYSVUV7TtK3ObNcuveuB8qz01ojRg8nySK7uBP5/k5I6uMzxa5IwESSn18lYyoy"
|
||||||
|
"MVQ7VSUZzZEW1lyvXdtPX4ZIlewXDeLs0vWBl08xp6d3r/F4odWQJS1ZWWWFAWdRZgQM/f"
|
||||||
|
"9IzgyCk0vJcCC9Ra/pUf0nGDWvzfkQSZNmdqnJs8mTPDGLaBTKC6qGCV1e3MlapYtOBVIX"
|
||||||
|
"SF2giIDUBRULUhdIXfGlrqTqQSbl4DBFGAv9CukFYxwaJRL+Cmv7hYVwXXPvfBszLd0D6s"
|
||||||
|
"Nt69tvTGvv9nufveQUsO1u/5rDc7K0LFyCIKR/jvq9cEgpEw7VOw2/7fepIlsnFVUxrR/b"
|
||||||
|
"wphaaU2WimopmvmRPHBLiy2CxXrkeZC5wYNkwCNvWpK1DAGeDAcdbTm3wRdwESVNRoFK8K"
|
||||||
|
"1317KreM3vUgAW42qrPRbuO1cVJ8GDRtDBATc4xJCfcJDTihIOIJcxho/LyMHjkh86vNXr"
|
||||||
|
"5E0ksmzofkM0oQi3zkAwNk+lOUu7WdhEgKInRI+1ypOWFRi2gCAWQDEI4SfdQMpM+4LeAv"
|
||||||
|
"1/B2LHdqALaB042JBeVxyfax34HZ11qw1xa9Ru3XSq63pyDijeudkUtM9uRDB8dGKQHHXG"
|
||||||
|
"ld5dt1t9j6PLztFUkUTFQvOMYpwnL92SDAu95togTe5BqN4b29oABTLmimnirEVT1smAmA"
|
||||||
|
"mRr7rxbC4wKqQTDlZ5j0jW5QJpFxKu04/W6LirjrZZzBXnq7S5Srrf/Se4E75uKnZGP0Dt"
|
||||||
|
"3cr6DNTeAxcFQe090IoFtfdQ1V5nCW3jH6jIaM2XtSqLbMkKN/VaDOWmXouUbkjUewiWj4"
|
||||||
|
"qKQoWHTXBShuVE9PwsDqQ4VSSmdlwYqGZdfEZvyTH17Urp+bQVRFfr3ACa0a53lEk+Lszl"
|
||||||
|
"c8Dj2EIiSZYiGUevKtJOOhkFMdorqKjgxVPF/AYSLixGq2HbpPNdfaZoY/0ZadUQLk/Fnq"
|
||||||
|
"wj8ipJh6m3lxAcs4CqA6MDqg4VC1QdqHqCM4jeDBr7fJxnkA+dPIBTXejXQsE1kqIvsJbl"
|
||||||
|
"HOMOqTMszXRTlQnjWVGqcI5ME/OOUD4dqUSwRkd6FjCFZ1g+7mAHIkHs1h+nuNJDlBfOnm"
|
||||||
|
"UH3/skRHVgXFOiRYcFk2ybksN32k/O9Xj9AToE6BBAV0GHON6KBR3iUNftJTwT8rA8bZyd"
|
||||||
|
"kd/6uf3brJB/GnZQo2YHnfp/N+p+bP00OPVVe30Rozi+G11VMmX9oH0dCuOOm8vUT1x/tH"
|
||||||
|
"8vmcR+JjI2bAljofdZHHZGg35v5OVxYadtkN9zx062I+qIKk/NL+6qhNq4M7wdia3BYNi/"
|
||||||
|
"b3Wd7Gpnvm3dLnl9QhWGjqXetD71C18/rwReexIwc5L+jgvRFwetv9x3QRQS9GMpGx+PQU"
|
||||||
|
"u42WDXpGpp6pZBa7d67U63w9i6T5AD6TFMg6HQ7oi9/ljsf2HqzXlR97HIr4bGOR/iFd1B"
|
||||||
|
"5iLsKST/IS7Wfas3Zp9CwSzxzcoNn7iYMO+JmyzXVJhiRzWPJpWv22Cq8SYyVs07jSPmnU"
|
||||||
|
"ZreacBLwZvgZ9iQuFtYUrZ85SykN7SViVjCRW554qUcX5rvAk3Lw+YDPbrs1X9JHwj88Kj"
|
||||||
|
"gs3wRDG4varIi3ma4S/vQ6IEpBdJXYY50ai6FKGdsmYcuI/Ermz946Z/d93tVAbDTlsYCe"
|
||||||
|
"7551WHsCNtVeanqjjK1rDT6oaBOUGPuoHEiWTM8HOToxq0B3j5eTrLsBDMZc9jw6jT/STi"
|
||||||
|
"pfJt/6piIvVRXBj6XH/Q8ADau2kN8bBB3mQqGakOlZ9dxBgwzviBnrrt7oIfMnDznIfNrt"
|
||||||
|
"GXUvgWJXGd3fWtFLI+9wTmuJhSJgBqKKhHfM3lVpy9KWAyDcBh+ez5lEJ1gCcgoX1VWSwn"
|
||||||
|
"qiI/aL52IS3wcIxXOkVYqzlf30jqNc5YHcmObaG87eHmk0NvbnDzidcMtnHzyc5PeBT72p"
|
||||||
|
"NNBzxCpow84PNzKm3rY6fCpH4qYUQ0jyusvczKfpc13JKy+1tS2MazzsfJa10x/JzsVm2n"
|
||||||
|
"hfNV4NcE7i873RsBv6ajqVjwazrUvUt/Kk1Injm7I6HP/EdhkqLmmxyHSrNObqCd1bMKDv"
|
||||||
|
"l+Y2hfkgPXpzaTZm/5mxW+eHyusGID1a0yXKxqLierUmakhCMqq2IuTfbDAaOo32bGtxWe"
|
||||||
|
"953TWahzdnAdJnBAoArAAaFigQMeOgfc15n0ffTGnA+lw0eP8vWE8kaGR0Ofi97iLP0AE5"
|
||||||
|
"INjDb79pYH36BqdvUEtssPbru8QBvBdH99UdCrKT4ppqUbSta9cgLFPcnwDzu/t0LPersX"
|
||||||
|
"RxhoIoQSHr71oolIV9/bNiQUWgZHlvzkrOtBPwH9BGg26CdQsaCfHDqjcaZYWV+G7WVGCi"
|
||||||
|
"ic1bEqKNSaIWEvYC13M7rtjhqWrhfszR2iUNWWwR9ip3v5xeXyUZv5+76f0T3TEsbK/OMu"
|
||||||
|
"a8gYlShnBvbqOaC7sFFqCNAwoGGwWgcaBhULNOzQF6AlvKIRLwLcc57cKf9Weyzcd64qTo"
|
||||||
|
"IHTeh5IYrmhZHdVhx0g1MZ8hMOSnXrSs4n/xdLXBbJxEsR5+4EkqHovXHKqtmU5y4rjL5j"
|
||||||
|
"YRcXM5zFuUPwLPoOwbPAHYKw/5rK259fZMcFjrc7Euhg69prCDlsXYe2wxzw+0rnVVoE+R"
|
||||||
|
"6Wfvvfu7Qg4yZ3vl/o3Mum/8oRIi8fiDJjAdcF7N4bgjlBE6K58SdsooU3k0+Zt/8D4zPk"
|
||||||
|
"jkEWHoBmhjQXva/NgBAHQhzoNSDEQcWCEHfoQtwerzPdPUvZwXWmhyxs3vVGd9ej9lC4Jk"
|
||||||
|
"LmUnNXa5N0YmYs9NeAH8SeLlGaGSdoDsPTvh1VwhbssV1WwoyPRNNjD75xBCcBhmG2RwJh"
|
||||||
|
"nPtQxLw8gfK/rnHvF6NsdA6KbKM5ADp28yv9J3DDOmCRfK0YoEPEH74iosWfQBOAiyJB1M"
|
||||||
|
"lnEQbcH0QdqFgQdY6ENa1m0rC57FqZRZ5z4QzzOeeyeULL5ZTL77Vavd6sndYvLs8bzeb5"
|
||||||
|
"5enquEswat25l2vhMzn6wtRe8CwMWaTYfwcAjlbNaJtSfgGodn4eQ7TBqaK/k0riuFNFio"
|
||||||
|
"GJQlIsWStA00NTlVKAyRgdN5YB1h/HGQWuo0zsY8COpNGY9TU01vHPZuRiUv2COWUkoNpR"
|
||||||
|
"FHsztQZGvX4BAowaiBcwaqhYYNT5T3tFr8ZkjLqAW2jb5tc57qDFduzGLX/3Dt4FWhkyjE"
|
||||||
|
"6fKfi19GeUlVt0SU5jklExJ4JYcJj2Rxds/6O8vLsFO7cSY0IdqwBgwoEhg2+epwFKBkqQ"
|
||||||
|
"m8fee/cY+I523nc0nW1r4z3TZrp/yCuE5jMnwKK5PnPoDAg/EP58+APwQiD8ULFA+I+E8C"
|
||||||
|
"fdQcu0ebaPLrj9nUjpBU9rhmjWxWf0lgTLgGEpdyRzO16SSkVxr7DLeiTcvwiveE04FgEE"
|
||||||
|
"Pgx8eNfn4fnmso7L+S0qBqNz1Z38iV3wXko4Cw+cD6gBcD6oWOB8x8L5Snh2e4G0KameQA"
|
||||||
|
"VWB53ejdD7fFVxkzxorXa7Mxjbd1DKMlrgkjxow859/wsJMtCL/lyIWymdNc5UnLwl3GcP"
|
||||||
|
"GB7JOVXeKz0hbMd3sBeuikwN3Zoz0XDZ4Umqyw7Dx74cQCz90efAgL4ZwJxOj5ceuoIeFm"
|
||||||
|
"cVo3W6yGafdlbKAk0ENBHQRArFuUATOZqKBU0ENJHCaCIJPtRxL9h6iLvQfNCuu/22LYdM"
|
||||||
|
"VF1OKYesO1ruySHNSDmkGbjUDhg9MHpg9MWgVckZPRDSzIQ07Hb/7dzrX8xVYizHjmJ9+K"
|
||||||
|
"BIKO1Mz6Da0SZlg21ycTUOccGabU3uoBcQ/iNB9gDZA9gxyB5QsSB7HLrswc60aaQPNoc9"
|
||||||
|
"Hwuotm5uhZ746a5LPnE5nSua+IjTPWiDYf/PTns8Eoed1s1VxfMaF0kPpWK/DoVxh4p+NR"
|
||||||
|
"QLPWhtbEUkFc98dWTftffj3Qz8BG4Og26r3bnt9PwSrD5m5pXBT+GVwk/i5tLqtbp/jYW2"
|
||||||
|
"lwlerqlvliJ7efjxpOX078Ziuyu0v4zolK8KrqGlJcqqIj+btE3/a09cvQltor9q7C0FiY"
|
||||||
|
"9dNOKcumhEH7po8GpR6BouuQJyfApSHBkkr7vJt+A3v3c9pAwbzjyPjc/SVrw3BVVz+Hj+"
|
||||||
|
"hA3Y2BaGAWBjB79oBzZ2oBULbAzYGLAxYGPFYWNecRLyMM4sw2Js87BTGAIW/tW3tB98O1"
|
||||||
|
"rknDEhKW6M1fGhBqoJqCbFVE1C+nYOUMa/1mNfHXrzZwaZIWszeN6cmgN6ud4ouyf4uCVG"
|
||||||
|
"jMbnTa15ND86r7IiyK81iqR6tpChyE/VEH3TjTlZp2RKfprCCJaRX6yK+6EqtwqzCZUZhR"
|
||||||
|
"bnO1W1s0azcVm/aKw+T7UKWec67H2BKlqffEFGNEUOR48ygYvL/IvLcNdIAKKbvJwAnp2e"
|
||||||
|
"xgAQp4r+DjuJ43ivrlmhU8Wfo34vgvP6JhyQdxp+we9TRbZOKqpiWj+KCesaFMlbM1KbB9"
|
||||||
|
"6H29Y3Htd2t3/Nr7ZJBtfJboLLf3p5/xfKHDK4"
|
||||||
|
)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
-- 1. Drop old constraints
|
||||||
|
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "uid_subscriptio_placeme_aba67c";
|
||||||
|
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_placemen_f35fb6bb";
|
||||||
|
|
||||||
|
-- 2. Add new column for placement_id
|
||||||
|
ALTER TABLE "subscription" ADD COLUMN "placement_id" UUID;
|
||||||
|
|
||||||
|
-- 3. Populate placement_id from placement_post table
|
||||||
|
UPDATE "subscription" s
|
||||||
|
SET placement_id = pp.placement_id
|
||||||
|
FROM "placement_post" pp
|
||||||
|
WHERE s.placement_post_id = pp.id;
|
||||||
|
|
||||||
|
-- 4. Make NOT NULL (all subscriptions should have a placement via placement_post)
|
||||||
|
ALTER TABLE "subscription" ALTER COLUMN "placement_id" SET NOT NULL;
|
||||||
|
|
||||||
|
-- 5. Add FK constraint to placement table
|
||||||
|
ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_placemen_0b5d8f15"
|
||||||
|
FOREIGN KEY ("placement_id") REFERENCES "placement" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- 6. Create unique index
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_75b40b"
|
||||||
|
ON "subscription" ("placement_id", "telegram_user_id");
|
||||||
|
|
||||||
|
-- 7. Drop old column
|
||||||
|
ALTER TABLE "subscription" DROP COLUMN "placement_post_id";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
-- 1. Drop new constraints
|
||||||
|
DROP INDEX IF EXISTS "uid_subscriptio_placeme_75b40b";
|
||||||
|
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_placemen_0b5d8f15";
|
||||||
|
|
||||||
|
-- 2. Add back placement_post_id column
|
||||||
|
ALTER TABLE "subscription" ADD COLUMN "placement_post_id" UUID;
|
||||||
|
|
||||||
|
-- 3. Populate from placement_post (best effort - takes first placement_post for the placement)
|
||||||
|
UPDATE "subscription" s
|
||||||
|
SET placement_post_id = pp.id
|
||||||
|
FROM "placement_post" pp
|
||||||
|
WHERE s.placement_id = pp.placement_id;
|
||||||
|
|
||||||
|
-- 4. Make NOT NULL (may fail if no matching placement_post exists)
|
||||||
|
ALTER TABLE "subscription" ALTER COLUMN "placement_post_id" SET NOT NULL;
|
||||||
|
|
||||||
|
-- 5. Add FK constraint back to placement_post
|
||||||
|
ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_placemen_f35fb6bb"
|
||||||
|
FOREIGN KEY ("placement_post_id") REFERENCES "placement_post" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- 6. Restore unique index
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_aba67c"
|
||||||
|
ON "subscription" ("placement_post_id", "telegram_user_id");
|
||||||
|
|
||||||
|
-- 7. Drop new column
|
||||||
|
ALTER TABLE "subscription" DROP COLUMN "placement_id";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
"al"
|
||||||
|
)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "creative" ADD "tag" VARCHAR(10) NOT NULL DEFAULT 'testing';
|
||||||
|
ALTER TABLE "post" ADD "published_at" TIMESTAMPTZ;
|
||||||
|
COMMENT ON COLUMN "creative"."tag" IS 'TESTING: testing\nPRODUCTION: production';"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "post" DROP COLUMN "published_at";
|
||||||
|
ALTER TABLE "creative" DROP COLUMN "tag";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
"3tCrbZLBTbKb4PKfXt7/BaQvMYA="
|
||||||
|
)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" ADD "invite_link_created_at" TIMESTAMPTZ;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" DROP COLUMN "invite_link_created_at";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "channel" ADD "is_accessible" BOOL NOT NULL DEFAULT True;
|
||||||
|
ALTER TABLE "placement_post" ADD "status" VARCHAR(64) NOT NULL DEFAULT 'Без статуса';
|
||||||
|
COMMENT ON COLUMN "placement_post"."status" IS 'NO_STATUS: Без статуса\nSEND_POST: Отправить пост\nPOST_APPROVAL: Согласование поста\nWAITING_SCHEDULE: Ожидание отложки\nSCHEDULED: Запланирован\nPOST_PUBLISHED: Пост вышел\nCOMPLETED_DELETED: Размещение отработало - пост удалён\nCOMPLETED_NOT_DELETED: Размещение отработало - пост не удалён\nCHECK_DELETED_EARLY: Проверить - пост удалён раньше срока\nCHECK_NOT_PUBLISHED: Проверить - пост не вышел\nCHECK_COMPLETED: Размещение отработало';"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "channel" DROP COLUMN "is_accessible";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN "status";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" ADD "cost_before_bargain_type" VARCHAR(8);
|
||||||
|
COMMENT ON COLUMN "placement"."cost_before_bargain_type" IS 'FIXED: fixed\nCPM: cpm';"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" DROP COLUMN "cost_before_bargain_type";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" ADD "invite_link_name" VARCHAR(32);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" DROP COLUMN "invite_link_name";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
"cQREBB"
|
||||||
|
)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
-- Drop old constraint (replaced by stricter one)
|
||||||
|
ALTER TABLE "channel" DROP CONSTRAINT IF EXISTS "channel_identity_required";
|
||||||
|
|
||||||
|
-- Add CHECK constraint: channel must have exactly one of username or invite_link
|
||||||
|
ALTER TABLE "channel"
|
||||||
|
ADD CONSTRAINT "check_channel_username_xor_invite_link"
|
||||||
|
CHECK (
|
||||||
|
(username IS NOT NULL AND invite_link IS NULL) OR
|
||||||
|
(username IS NULL AND invite_link IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON CONSTRAINT "check_channel_username_xor_invite_link" ON "channel"
|
||||||
|
IS 'Ensures channel has exactly one identifier: username OR invite_link (not both, not neither)';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "channel"
|
||||||
|
DROP CONSTRAINT IF EXISTS "check_channel_username_xor_invite_link";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
"cQREBB"
|
||||||
|
)
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" ADD "feed_time_minutes" INT;
|
||||||
|
ALTER TABLE "placement" ADD "top_time_minutes" INT;
|
||||||
|
|
||||||
|
-- Migrate known patterns: "X / Y" where X is top hours, Y is feed hours
|
||||||
|
-- Pattern: "1 / 24", "1/24"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 60, feed_time_minutes = 1440
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^1\\s*/\\s*24$';
|
||||||
|
|
||||||
|
-- Pattern: "1 / 36", "1/36"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 60, feed_time_minutes = 2160
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^1\\s*/\\s*36$';
|
||||||
|
|
||||||
|
-- Pattern: "1 / 48", "1/48"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 60, feed_time_minutes = 2880
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^1\\s*/\\s*48$';
|
||||||
|
|
||||||
|
-- Pattern: "1 / 72", "1/72"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 60, feed_time_minutes = 4320
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^1\\s*/\\s*72$';
|
||||||
|
|
||||||
|
-- Pattern: "2 / 24", "2/24"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 120, feed_time_minutes = 1440
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^2\\s*/\\s*24$';
|
||||||
|
|
||||||
|
-- Pattern: "2 / 36", "2/36"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 120, feed_time_minutes = 2160
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^2\\s*/\\s*36$';
|
||||||
|
|
||||||
|
-- Pattern: "2 / 48", "2/48"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 120, feed_time_minutes = 2880
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^2\\s*/\\s*48$';
|
||||||
|
|
||||||
|
-- Pattern: "2 / 72", "2/72"
|
||||||
|
UPDATE "placement" SET top_time_minutes = 120, feed_time_minutes = 4320
|
||||||
|
WHERE format IS NOT NULL AND lower(trim(format)) ~ '^2\\s*/\\s*72$';
|
||||||
|
|
||||||
|
-- Pattern: "X / (N дней)" or "X / (N дн)"
|
||||||
|
UPDATE "placement" SET
|
||||||
|
top_time_minutes = CASE
|
||||||
|
WHEN substring(lower(trim(format)) from '^(\\d+)\\s*/') ~ '^\\d+$'
|
||||||
|
AND (substring(lower(trim(format)) from '^(\\d+)\\s*/'))::int <= 12
|
||||||
|
THEN (substring(lower(trim(format)) from '^(\\d+)\\s*/'))::int * 60
|
||||||
|
ELSE (substring(lower(trim(format)) from '^(\\d+)\\s*/'))::int
|
||||||
|
END,
|
||||||
|
feed_time_minutes = (substring(lower(trim(format)) from '(\\d+)\\s*дн'))::int * 24 * 60
|
||||||
|
WHERE format IS NOT NULL
|
||||||
|
AND top_time_minutes IS NULL
|
||||||
|
AND lower(trim(format)) ~ '^\\d+\\s*/.*\\d+\\s*дн';
|
||||||
|
|
||||||
|
-- Pattern: "X / (без удаления)" or "X / без удаления"
|
||||||
|
UPDATE "placement" SET
|
||||||
|
top_time_minutes = CASE
|
||||||
|
WHEN substring(lower(trim(format)) from '^(\\d+)\\s*/') ~ '^\\d+$'
|
||||||
|
AND (substring(lower(trim(format)) from '^(\\d+)\\s*/'))::int <= 12
|
||||||
|
THEN (substring(lower(trim(format)) from '^(\\d+)\\s*/'))::int * 60
|
||||||
|
ELSE (substring(lower(trim(format)) from '^(\\d+)\\s*/'))::int
|
||||||
|
END,
|
||||||
|
feed_time_minutes = 0
|
||||||
|
WHERE format IS NOT NULL
|
||||||
|
AND top_time_minutes IS NULL
|
||||||
|
AND lower(trim(format)) ~ 'без удаления';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" DROP COLUMN IF EXISTS "feed_time_minutes";
|
||||||
|
ALTER TABLE "placement" DROP COLUMN IF EXISTS "top_time_minutes";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
-- Add CHECK constraint: exactly one FK must be set
|
||||||
|
ALTER TABLE "workspace_user_permission_scope"
|
||||||
|
ADD CONSTRAINT "check_exactly_one_scope"
|
||||||
|
CHECK (
|
||||||
|
(("project_id" IS NOT NULL)::int +
|
||||||
|
("creative_id" IS NOT NULL)::int +
|
||||||
|
("placement_id" IS NOT NULL)::int +
|
||||||
|
("channel_id" IS NOT NULL)::int) = 1
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE "workspace_user_permission_scope"
|
||||||
|
IS 'Permission scoped to specific entity (project/creative/placement/channel)'; \
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "workspace_user_permission_scope"
|
||||||
|
DROP CONSTRAINT IF EXISTS "check_exactly_one_scope"; \
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
'v0'
|
||||||
|
)
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
-- Шаг 1: Переименовать старую таблицу placement в old_placement
|
||||||
|
ALTER TABLE "placement" RENAME TO "old_placement";
|
||||||
|
|
||||||
|
-- Шаг 2: Создать новую таблицу placement (объединение purchase + purchase_channel)
|
||||||
|
CREATE TABLE "placement" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
|
||||||
|
"placement_at" TIMESTAMPTZ,
|
||||||
|
"payment_at" TIMESTAMPTZ,
|
||||||
|
"cost_type" VARCHAR(8),
|
||||||
|
"cost_value" DOUBLE PRECISION,
|
||||||
|
"cost_before_bargain" DOUBLE PRECISION,
|
||||||
|
"placement_type" VARCHAR(16),
|
||||||
|
"format" TEXT,
|
||||||
|
"comment" TEXT,
|
||||||
|
"invite_link" VARCHAR(512) NOT NULL,
|
||||||
|
"invite_link_type" VARCHAR(8) NOT NULL,
|
||||||
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Шаг 3: Мигрировать данные из purchase_channel + purchase в новый placement
|
||||||
|
INSERT INTO "placement" (
|
||||||
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
|
"status", "placement_at", "payment_at",
|
||||||
|
"cost_type", "cost_value", "cost_before_bargain",
|
||||||
|
"placement_type", "format", "comment",
|
||||||
|
"invite_link", "invite_link_type",
|
||||||
|
"project_id", "creative_id", "channel_id"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
pc.id, pc.created_at, pc.updated_at, pc.deleted_at,
|
||||||
|
pc.status,
|
||||||
|
COALESCE(pc.placement_at, p.placement_at),
|
||||||
|
p.payment_at,
|
||||||
|
COALESCE(pc.cost_type, p.cost_type),
|
||||||
|
COALESCE(pc.cost_value, p.cost_value),
|
||||||
|
COALESCE(pc.cost_before_bargain, p.cost_before_bargain),
|
||||||
|
p.purchase_type,
|
||||||
|
COALESCE(pc.format, p.format),
|
||||||
|
COALESCE(pc.comment, p.comment),
|
||||||
|
pc.invite_link, pc.invite_link_type,
|
||||||
|
p.project_id, p.creative_id, pc.channel_id
|
||||||
|
FROM "purchase_channel" pc
|
||||||
|
JOIN "purchase" p ON pc.purchase_id = p.id;
|
||||||
|
|
||||||
|
-- Шаг 4: Создать таблицу placement_post (бывший placement)
|
||||||
|
CREATE TABLE "placement_post" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"wanted_placement_date" TIMESTAMPTZ NOT NULL,
|
||||||
|
"cost" DOUBLE PRECISION,
|
||||||
|
"comment" TEXT,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
|
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE,
|
||||||
|
"post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL,
|
||||||
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Шаг 5: Мигрировать данные из old_placement в placement_post
|
||||||
|
INSERT INTO "placement_post" (
|
||||||
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
|
"wanted_placement_date", "cost", "comment", "status",
|
||||||
|
"creative_id", "placement_id", "post_id", "project_id"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
op.id, op.created_at, op.updated_at, op.deleted_at,
|
||||||
|
op.wanted_placement_date, op.cost, op.comment, op.status,
|
||||||
|
op.creative_id, op.purchase_channel_id, op.post_id, op.project_id
|
||||||
|
FROM "old_placement" op;
|
||||||
|
|
||||||
|
-- Шаг 6: Обновить foreign keys для таблиц, ссылающихся на old_placement
|
||||||
|
|
||||||
|
-- 6.1: Обновить subscription (placement_id -> placement_post_id)
|
||||||
|
-- Удалить constraint ДО переименования колонки
|
||||||
|
ALTER TABLE "subscription"
|
||||||
|
DROP CONSTRAINT IF EXISTS "subscription_placement_id_fkey";
|
||||||
|
|
||||||
|
-- Переименовать колонку
|
||||||
|
ALTER TABLE "subscription" RENAME COLUMN "placement_id" TO "placement_post_id";
|
||||||
|
|
||||||
|
-- 6.2: Обновить workspace_user_permission_scope.placement_id
|
||||||
|
-- Удалить старый foreign key constraint ДО обновления данных
|
||||||
|
ALTER TABLE "workspace_user_permission_scope"
|
||||||
|
DROP CONSTRAINT IF EXISTS "workspace_user_permission_scope_placement_id_fkey";
|
||||||
|
|
||||||
|
-- Обновить данные: старые placement_id -> новые placement_id (через purchase_channel_id)
|
||||||
|
UPDATE "workspace_user_permission_scope" wups
|
||||||
|
SET placement_id = op.purchase_channel_id
|
||||||
|
FROM "old_placement" op
|
||||||
|
WHERE wups.placement_id = op.id;
|
||||||
|
|
||||||
|
-- Шаг 7: Создать индексы и constraints
|
||||||
|
CREATE INDEX "idx_placement_project_4155ce"
|
||||||
|
ON "placement" ("project_id", "status");
|
||||||
|
CREATE INDEX "idx_placement_channel_fb3968"
|
||||||
|
ON "placement" ("channel_id");
|
||||||
|
CREATE INDEX "idx_placement_post_creativ_1c6807"
|
||||||
|
ON "placement_post" ("creative_id");
|
||||||
|
CREATE INDEX "idx_placement_post_placeme_bd5bc9"
|
||||||
|
ON "placement_post" ("placement_id");
|
||||||
|
CREATE INDEX "idx_placement_post_post_id_998755"
|
||||||
|
ON "placement_post" ("post_id");
|
||||||
|
CREATE INDEX "idx_placement_post_project_94129d"
|
||||||
|
ON "placement_post" ("project_id");
|
||||||
|
|
||||||
|
ALTER TABLE "subscription"
|
||||||
|
ADD CONSTRAINT "fk_subscrip_placem_1959be9e"
|
||||||
|
FOREIGN KEY ("placement_post_id")
|
||||||
|
REFERENCES "placement_post" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "uid_subscriptio_placem_578bfb"
|
||||||
|
ON "subscription" ("placement_post_id", "telegram_user_id");
|
||||||
|
|
||||||
|
ALTER TABLE "workspace_user_permission_scope"
|
||||||
|
ADD CONSTRAINT "fk_workspac_placemen_8c5d3f2a"
|
||||||
|
FOREIGN KEY ("placement_id")
|
||||||
|
REFERENCES "placement" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Шаг 8: Добавить комментарии
|
||||||
|
COMMENT ON TABLE "placement"
|
||||||
|
IS 'Размещение, управляемое пользователем (бывший PurchaseChannel + Purchase)';
|
||||||
|
COMMENT ON COLUMN "placement"."status"
|
||||||
|
IS 'PLANNED: planned
|
||||||
|
APPROVED: approved
|
||||||
|
REJECTED: rejected
|
||||||
|
IN_PROGRESS: in_progress
|
||||||
|
COMPLETED: completed';
|
||||||
|
COMMENT ON COLUMN "placement"."invite_link_type"
|
||||||
|
IS 'PUBLIC: public
|
||||||
|
APPROVAL: approval';
|
||||||
|
COMMENT ON COLUMN "placement"."cost_type"
|
||||||
|
IS 'FIXED: fixed
|
||||||
|
CPM: cpm';
|
||||||
|
COMMENT ON COLUMN "placement"."placement_type"
|
||||||
|
IS 'SELF_PROMO: self_promo
|
||||||
|
STANDARD: standard';
|
||||||
|
|
||||||
|
COMMENT ON TABLE "placement_post"
|
||||||
|
IS 'Публикация, мониторимая системой (бывший Placement)';
|
||||||
|
COMMENT ON COLUMN "placement_post"."status"
|
||||||
|
IS 'ACTIVE: active
|
||||||
|
ARCHIVED: archived';
|
||||||
|
|
||||||
|
-- Шаг 9: Удалить старые таблицы
|
||||||
|
DROP TABLE IF EXISTS "old_placement";
|
||||||
|
DROP TABLE IF EXISTS "purchase_channel";
|
||||||
|
DROP TABLE IF EXISTS "purchase";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
-- ВНИМАНИЕ: Downgrade может привести к потере данных при объединении placement обратно
|
||||||
|
-- в purchase + purchase_channel (если были созданы placement без привязки к одному purchase)
|
||||||
|
|
||||||
|
-- Шаг 1: Переименовать новую таблицу placement в new_placement
|
||||||
|
ALTER TABLE "placement" RENAME TO "new_placement";
|
||||||
|
|
||||||
|
-- Шаг 2: Воссоздать таблицу purchase
|
||||||
|
CREATE TABLE "purchase" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"placement_at" TIMESTAMPTZ,
|
||||||
|
"payment_at" TIMESTAMPTZ,
|
||||||
|
"cost_type" VARCHAR(8),
|
||||||
|
"cost_value" DOUBLE PRECISION,
|
||||||
|
"cost_before_bargain" DOUBLE PRECISION,
|
||||||
|
"purchase_type" VARCHAR(16),
|
||||||
|
"format" TEXT,
|
||||||
|
"comment" TEXT,
|
||||||
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Шаг 3: Воссоздать таблицу purchase_channel
|
||||||
|
CREATE TABLE "purchase_channel" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
|
||||||
|
"placement_at" TIMESTAMPTZ,
|
||||||
|
"cost_type" VARCHAR(8),
|
||||||
|
"cost_value" DOUBLE PRECISION,
|
||||||
|
"cost_before_bargain" DOUBLE PRECISION,
|
||||||
|
"format" TEXT,
|
||||||
|
"comment" TEXT,
|
||||||
|
"invite_link" VARCHAR(512) NOT NULL,
|
||||||
|
"invite_link_type" VARCHAR(8) NOT NULL,
|
||||||
|
"purchase_id" UUID NOT NULL REFERENCES "purchase" ("id") ON DELETE CASCADE,
|
||||||
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Шаг 4: Мигрировать данные из new_placement в purchase (сгруппировать)
|
||||||
|
-- Создаем purchase для каждого уникального project_id + creative_id
|
||||||
|
INSERT INTO "purchase" (
|
||||||
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
|
"status", "placement_at", "payment_at",
|
||||||
|
"cost_type", "cost_value", "cost_before_bargain",
|
||||||
|
"purchase_type", "format", "comment",
|
||||||
|
"project_id", "creative_id"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
MIN(np.created_at),
|
||||||
|
MAX(np.updated_at),
|
||||||
|
NULL,
|
||||||
|
'active',
|
||||||
|
MIN(np.placement_at),
|
||||||
|
MIN(np.payment_at),
|
||||||
|
MIN(np.cost_type),
|
||||||
|
MIN(np.cost_value),
|
||||||
|
MIN(np.cost_before_bargain),
|
||||||
|
MIN(np.placement_type),
|
||||||
|
MIN(np.format),
|
||||||
|
MIN(np.comment),
|
||||||
|
np.project_id,
|
||||||
|
np.creative_id
|
||||||
|
FROM "new_placement" np
|
||||||
|
GROUP BY np.project_id, np.creative_id;
|
||||||
|
|
||||||
|
-- Шаг 5: Мигрировать данные из new_placement в purchase_channel
|
||||||
|
INSERT INTO "purchase_channel" (
|
||||||
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
|
"status", "placement_at", "cost_type", "cost_value",
|
||||||
|
"cost_before_bargain", "format", "comment",
|
||||||
|
"invite_link", "invite_link_type",
|
||||||
|
"purchase_id", "channel_id"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
np.id, np.created_at, np.updated_at, np.deleted_at,
|
||||||
|
np.status, np.placement_at, np.cost_type, np.cost_value,
|
||||||
|
np.cost_before_bargain, np.format, np.comment,
|
||||||
|
np.invite_link, np.invite_link_type,
|
||||||
|
p.id, np.channel_id
|
||||||
|
FROM "new_placement" np
|
||||||
|
JOIN "purchase" p
|
||||||
|
ON p.project_id = np.project_id
|
||||||
|
AND p.creative_id = np.creative_id;
|
||||||
|
|
||||||
|
-- Шаг 6: Воссоздать старую таблицу placement
|
||||||
|
CREATE TABLE "placement" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"wanted_placement_date" TIMESTAMPTZ NOT NULL,
|
||||||
|
"cost" DOUBLE PRECISION,
|
||||||
|
"comment" TEXT,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
|
"purchase_channel_id" UUID NOT NULL
|
||||||
|
REFERENCES "purchase_channel" ("id") ON DELETE CASCADE,
|
||||||
|
"post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL,
|
||||||
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Шаг 7: Мигрировать данные из placement_post в placement
|
||||||
|
INSERT INTO "placement" (
|
||||||
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
|
"wanted_placement_date", "cost", "comment", "status",
|
||||||
|
"creative_id", "purchase_channel_id", "post_id", "project_id"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
pub.id, pub.created_at, pub.updated_at, pub.deleted_at,
|
||||||
|
pub.wanted_placement_date, pub.cost, pub.comment, pub.status,
|
||||||
|
pub.creative_id, pub.placement_id, pub.post_id, pub.project_id
|
||||||
|
FROM "placement_post" pub;
|
||||||
|
|
||||||
|
-- Шаг 8: Обновить foreign keys обратно
|
||||||
|
|
||||||
|
-- 8.1: Обновить subscription (placement_post_id -> placement_id)
|
||||||
|
-- Удалить новый foreign key constraint
|
||||||
|
ALTER TABLE "subscription"
|
||||||
|
DROP CONSTRAINT IF EXISTS "fk_subscrip_placem_1959be9e";
|
||||||
|
|
||||||
|
-- Переименовать колонку обратно
|
||||||
|
ALTER TABLE "subscription" RENAME COLUMN "placement_post_id" TO "placement_id";
|
||||||
|
|
||||||
|
-- 8.2: Обновить workspace_user_permission_scope.placement_id обратно
|
||||||
|
-- Удалить constraint на новый placement
|
||||||
|
ALTER TABLE "workspace_user_permission_scope"
|
||||||
|
DROP CONSTRAINT IF EXISTS "fk_workspac_placemen_8c5d3f2a";
|
||||||
|
|
||||||
|
-- Обновить данные обратно: новые placement_id -> старые placement_id
|
||||||
|
UPDATE "workspace_user_permission_scope" wups
|
||||||
|
SET placement_id = pub.id
|
||||||
|
FROM "placement_post" pub
|
||||||
|
WHERE wups.placement_id = pub.placement_id;
|
||||||
|
|
||||||
|
-- Шаг 9: Создать индексы и constraints
|
||||||
|
CREATE INDEX "idx_purchase_project_status" ON "purchase" ("project_id", "status");
|
||||||
|
CREATE INDEX "idx_placement_purchas_61f9e6"
|
||||||
|
ON "placement" ("purchase_channel_id");
|
||||||
|
CREATE INDEX "idx_placement_post_id_e4a16e" ON "placement" ("post_id");
|
||||||
|
|
||||||
|
ALTER TABLE "subscription"
|
||||||
|
ADD CONSTRAINT "fk_subscrip_placemen_0b5d8f15"
|
||||||
|
FOREIGN KEY ("placement_id")
|
||||||
|
REFERENCES "placement" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "uid_subscriptio_placeme_75b40b"
|
||||||
|
ON "subscription" ("placement_id", "telegram_user_id");
|
||||||
|
|
||||||
|
ALTER TABLE "workspace_user_permission_scope"
|
||||||
|
ADD CONSTRAINT "fk_workspac_placemen_8c5d3f2a"
|
||||||
|
FOREIGN KEY ("placement_id")
|
||||||
|
REFERENCES "placement" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Шаг 10: Удалить новые таблицы
|
||||||
|
DROP TABLE IF EXISTS "placement_post";
|
||||||
|
DROP TABLE IF EXISTS "new_placement";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
'OfXn7/A5u7TFM='
|
||||||
|
)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "login_token" ADD "message_id" INT;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "login_token" DROP COLUMN "message_id";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
)
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
DROP INDEX IF EXISTS "idx_placement_p_creativ_a8f431";
|
||||||
|
DROP INDEX IF EXISTS "idx_placement_p_project_888982";
|
||||||
|
ALTER TABLE "placement_post" DROP CONSTRAINT IF EXISTS "fk_placemen_project_f98a29a5";
|
||||||
|
ALTER TABLE "placement_post" DROP CONSTRAINT IF EXISTS "fk_placemen_creative_db63ad95";
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "creative_id" DROP NOT NULL;
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN "comment";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN "project_id";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN "cost";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN "wanted_placement_date";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN "creative_id";"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "creative_id" SET NOT NULL;
|
||||||
|
ALTER TABLE "placement_post" ADD "comment" TEXT;
|
||||||
|
ALTER TABLE "placement_post" ADD "project_id" UUID NOT NULL;
|
||||||
|
ALTER TABLE "placement_post" ADD "cost" DOUBLE PRECISION;
|
||||||
|
ALTER TABLE "placement_post" ADD "wanted_placement_date" TIMESTAMPTZ NOT NULL;
|
||||||
|
ALTER TABLE "placement_post" ADD "creative_id" UUID NOT NULL;
|
||||||
|
ALTER TABLE "placement_post" ADD CONSTRAINT "fk_placemen_creative_db63ad95" FOREIGN KEY ("creative_id") REFERENCES "creative" ("id") ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "placement_post" ADD CONSTRAINT "fk_placemen_project_f98a29a5" FOREIGN KEY ("project_id") REFERENCES "project" ("id") ON DELETE CASCADE;
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_p_project_888982" ON "placement_post" ("project_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_p_creativ_a8f431" ON "placement_post" ("creative_id");"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
'f/C4NtvdBk9VSAWNZJcSZj+9vP8f6Gvh+w=='
|
||||||
|
)
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "status" SET DEFAULT 'Без статуса';
|
||||||
|
UPDATE "placement"
|
||||||
|
SET "status" = 'Без статуса'
|
||||||
|
WHERE "status" IN ('planned', 'approved', 'in_progress', 'completed', 'rejected');
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "status" TYPE VARCHAR(64) USING "status"::VARCHAR(64);
|
||||||
|
COMMENT ON COLUMN "placement"."status" IS 'NO_STATUS: Без статуса
|
||||||
|
WRITE: Написать
|
||||||
|
WAITING_RESPONSE: Ждём ответа
|
||||||
|
TERMS_APPROVAL: Согласование условий
|
||||||
|
TO_PAY: Оплатить
|
||||||
|
PAID: Оплачено
|
||||||
|
CANCELED: Отмена
|
||||||
|
PRICE_NOT_OK: Не подходит цена
|
||||||
|
NOT_RELEVANT: Неактуально
|
||||||
|
NO_RESPONSE: Не отвечает';
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "invite_link" DROP NOT NULL;
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN IF EXISTS "wanted_placement_date";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN IF EXISTS "cost";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN IF EXISTS "comment";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN IF EXISTS "creative_id";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN IF EXISTS "project_id";
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN IF EXISTS "status";"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "status" SET DEFAULT 'planned';
|
||||||
|
UPDATE "placement"
|
||||||
|
SET "status" = 'planned'
|
||||||
|
WHERE "status" IN (
|
||||||
|
'Без статуса',
|
||||||
|
'Написать',
|
||||||
|
'Ждём ответа',
|
||||||
|
'Согласование условий',
|
||||||
|
'Оплатить',
|
||||||
|
'Оплачено',
|
||||||
|
'Отмена',
|
||||||
|
'Не подходит цена',
|
||||||
|
'Неактуально',
|
||||||
|
'Не отвечает'
|
||||||
|
);
|
||||||
|
COMMENT ON COLUMN "placement"."status" IS 'PLANNED: planned
|
||||||
|
APPROVED: approved
|
||||||
|
REJECTED: rejected
|
||||||
|
IN_PROGRESS: in_progress
|
||||||
|
COMPLETED: completed';
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "status" TYPE VARCHAR(11) USING "status"::VARCHAR(11);
|
||||||
|
ALTER TABLE "placement" ALTER COLUMN "invite_link" SET NOT NULL;
|
||||||
|
ALTER TABLE "placement_post" ADD "wanted_placement_date" TIMESTAMPTZ;
|
||||||
|
ALTER TABLE "placement_post" ADD "cost" DOUBLE PRECISION;
|
||||||
|
ALTER TABLE "placement_post" ADD "comment" TEXT;
|
||||||
|
ALTER TABLE "placement_post" ADD "creative_id" UUID;
|
||||||
|
ALTER TABLE "placement_post" ADD "project_id" UUID;
|
||||||
|
ALTER TABLE "placement_post" ADD "status" VARCHAR(8) NOT NULL DEFAULT 'active';
|
||||||
|
COMMENT ON COLUMN "placement_post"."status" IS 'ACTIVE: active\nARCHIVED: archived';"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "workspace" ADD "avatar_s3_key" VARCHAR(512);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "workspace" DROP COLUMN "avatar_s3_key";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
'slSFZHCb7lLC/IeXX/8CYMWYuQ=='
|
||||||
|
)
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "channel" ADD "invite_link" VARCHAR(1024);
|
||||||
|
ALTER TABLE "channel" ALTER COLUMN "username" DROP NOT NULL;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "channel" DROP COLUMN "invite_link";
|
||||||
|
ALTER TABLE "channel" ALTER COLUMN "username" SET NOT NULL;"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "channel"
|
||||||
|
ADD CONSTRAINT "channel_identity_required"
|
||||||
|
CHECK (username IS NOT NULL OR invite_link IS NOT NULL); \
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "channel" DROP CONSTRAINT IF EXISTS "channel_identity_required"; \
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
)
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "creative_media" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"media_type" VARCHAR(32) NOT NULL,
|
||||||
|
"media_file_id" VARCHAR(512) NOT NULL,
|
||||||
|
"media_s3_key" VARCHAR(512),
|
||||||
|
"position" INT NOT NULL,
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_creative_me_creativ_dbd1c4" ON "creative_media" ("creative_id");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "uid_creative_media_creative_pos" ON "creative_media" ("creative_id", "position");
|
||||||
|
INSERT INTO "creative_media" (
|
||||||
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
|
"media_type", "media_file_id", "media_s3_key", "position", "creative_id"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"deleted_at",
|
||||||
|
"media_type",
|
||||||
|
"media_file_id",
|
||||||
|
"media_s3_key",
|
||||||
|
0,
|
||||||
|
"id"
|
||||||
|
FROM "creative"
|
||||||
|
WHERE "media_file_id" IS NOT NULL AND "media_type" IS NOT NULL;
|
||||||
|
ALTER TABLE "creative" DROP COLUMN "media_type";
|
||||||
|
ALTER TABLE "creative" DROP COLUMN "media_s3_key";
|
||||||
|
ALTER TABLE "creative" DROP COLUMN "media_file_id";"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "creative" ADD "media_type" VARCHAR(32);
|
||||||
|
ALTER TABLE "creative" ADD "media_s3_key" VARCHAR(512);
|
||||||
|
ALTER TABLE "creative" ADD "media_file_id" VARCHAR(512);
|
||||||
|
UPDATE "creative" AS c
|
||||||
|
SET
|
||||||
|
"media_type" = cm."media_type",
|
||||||
|
"media_s3_key" = cm."media_s3_key",
|
||||||
|
"media_file_id" = cm."media_file_id"
|
||||||
|
FROM "creative_media" AS cm
|
||||||
|
WHERE cm."creative_id" = c."id" AND cm."position" = 0;
|
||||||
|
DROP TABLE IF EXISTS "creative_media";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"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"
|
||||||
|
"3zVIVkcJ3uUsL8p5f3fwEyCCxF"
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
module github.com/TelegramExchange/pkg
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gotd/td v0.136.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gotd/td v0.136.0/go.mod h1:mStcqs/9FXhNhWnPTguptSwqkQbRIwXLw3SCSpzPJxM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
URL string `envconfig:"DB__URL" required:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pool struct {
|
||||||
|
*pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, c Config) (*Pool, error) {
|
||||||
|
cfg, err := pgxpool.ParseConfig(c.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgxpool.ParseConfig: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgxpool.NewWithConfig: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Pool{Pool: pool}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) Close() {
|
||||||
|
p.Pool.Close()
|
||||||
|
|
||||||
|
log.Info().Msg("Postgres closed")
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/gotd/td/session"
|
||||||
|
"github.com/gotd/td/telegram"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ApiID int `envconfig:"TELEGRAM__API_ID" required:"true"`
|
||||||
|
ApiHash string `envconfig:"TELEGRAM__API_HASH" required:"true"`
|
||||||
|
SessionFile string `envconfig:"TELEGRAM__SESSION_FILE" required:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
*telegram.Client
|
||||||
|
cancel context.CancelFunc
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg Config) (*Client, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tg := telegram.NewClient(cfg.ApiID, cfg.ApiHash, telegram.Options{
|
||||||
|
SessionStorage: &session.FileStorage{Path: cfg.SessionFile},
|
||||||
|
})
|
||||||
|
|
||||||
|
runCtx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
ready := make(chan struct{})
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
Client: tg,
|
||||||
|
cancel: cancel,
|
||||||
|
done: done,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := tg.Run(runCtx, func(ctx context.Context) error {
|
||||||
|
status, err := tg.Auth().Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
close(ready)
|
||||||
|
return fmt.Errorf("telegram.Auth error: %w", err)
|
||||||
|
}
|
||||||
|
if !status.Authorized {
|
||||||
|
close(ready)
|
||||||
|
return fmt.Errorf("session not authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о текущем пользователе
|
||||||
|
self, err := tg.Self(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to get user info")
|
||||||
|
} else {
|
||||||
|
log.Info().
|
||||||
|
Int64("id", self.ID).
|
||||||
|
Str("username", "@"+self.Username).
|
||||||
|
Str("first_name", self.FirstName).
|
||||||
|
Str("last_name", self.LastName).
|
||||||
|
Str("phone", self.Phone).
|
||||||
|
Msg("Authorized as user")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(ready)
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
return ctx.Err()
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("telegram client stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-ready
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.cancel()
|
||||||
|
<-c.done
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package transaction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/TelegramExchange/pkg/postgres"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingInit = errors.New("missing `transaction.Init' call before `transaction.Begin'")
|
||||||
|
errMissingBegin = errors.New("missing `transaction.Begin' call before 'transaction.Get'")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
IsUnitTest bool
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKey struct{}
|
||||||
|
|
||||||
|
func Init(p *postgres.Pool) {
|
||||||
|
pool = p.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
pgx.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func Begin(ctx context.Context) (context.Context, error) {
|
||||||
|
if IsUnitTest {
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if pool == nil {
|
||||||
|
return nil, errMissingInit
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pool.Begin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, ctxKey{}, &Transaction{tx})
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rollback(ctx context.Context) {
|
||||||
|
if IsUnitTest {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, ok := ctx.Value(ctxKey{}).(*Transaction)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tx.Rollback(ctx)
|
||||||
|
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||||
|
log.Error().Err(err).Msg("transaction: Rollback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Commit(ctx context.Context) error {
|
||||||
|
if IsUnitTest {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, ok := ctx.Value(ctxKey{}).(*Transaction)
|
||||||
|
if !ok {
|
||||||
|
return errMissingBegin
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("tx.Commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Executor interface {
|
||||||
|
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
||||||
|
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
|
||||||
|
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func TryExtractTX(ctx context.Context) Executor {
|
||||||
|
tx, ok := ctx.Value(ctxKey{}).(*Transaction)
|
||||||
|
if !ok {
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package transaction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Wrap(ctx context.Context, fn func(context.Context) error) error {
|
||||||
|
tx, err := pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pool.Begin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = tx.Rollback(ctx)
|
||||||
|
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||||
|
log.Error().Err(err).Msg("transaction: Rollback")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, ctxKey{}, &Transaction{tx})
|
||||||
|
|
||||||
|
err = fn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fn: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("tx.Commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
[project]
|
||||||
|
name = "tgex-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"aerich>=0.9.2",
|
||||||
|
"aioboto3>=13.3.0",
|
||||||
|
"aiogram>=3.16.0",
|
||||||
|
"aiolimiter>=1.2.1",
|
||||||
|
"asyncpg>=0.30.0",
|
||||||
|
"beautifulsoup4>=4.14.2",
|
||||||
|
"fastapi>=0.121.0",
|
||||||
|
"fastapi-pagination>=0.15.3",
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"lxml>=6.0.2",
|
||||||
|
"pydantic-settings>=2.11.0",
|
||||||
|
"pyjwt>=2.10.1",
|
||||||
|
"python-multipart>=0.0.20",
|
||||||
|
"tortoise-orm>=0.25.1",
|
||||||
|
"tortoise-orm-stubs>=1.0.2",
|
||||||
|
"types-aiobotocore-s3>=2.15.2",
|
||||||
|
"uvicorn>=0.38.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"mypy>=1.18.2",
|
||||||
|
"pytest>=8.4.2",
|
||||||
|
"pytest-asyncio>=1.2.0",
|
||||||
|
"ruff>=0.14.4",
|
||||||
|
"ty>=0.0.1a25",
|
||||||
|
"vulture>=2.14",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [".venv"]
|
||||||
|
target-version = "py313"
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"UP", # pyupgrade
|
||||||
|
]
|
||||||
|
ignore = ["B008"]
|
||||||
|
fixable = ["ALL"]
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "single"
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
plugins = [
|
||||||
|
"pydantic.mypy",
|
||||||
|
]
|
||||||
|
exclude = [
|
||||||
|
"tests/conftest.py"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["src", "."]
|
||||||
|
|
||||||
|
[tool.aerich]
|
||||||
|
tortoise_orm = "shared.datebase_base.TORTOISE_ORM"
|
||||||
|
location = "./migrations"
|
||||||
|
src_folder = "./."
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared utilities package."""
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
RED = '\033[91m'
|
||||||
|
YELLOW = '\033[33m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings[T: BaseSettings](settings_class: type[T]) -> T:
|
||||||
|
try:
|
||||||
|
return settings_class()
|
||||||
|
except ValidationError as e:
|
||||||
|
print(f'{RED}{BOLD}Missing environment variables:{RESET}')
|
||||||
|
|
||||||
|
missing: set[str] = set()
|
||||||
|
for err in e.errors():
|
||||||
|
if err['type'] == 'missing' and len(err['loc']) == 1:
|
||||||
|
field_info = settings_class.model_fields.get(str(err['loc'][0]))
|
||||||
|
if field_info and field_info.annotation is not None and hasattr(field_info.annotation, 'model_fields'):
|
||||||
|
annotation_fields = getattr(field_info.annotation, 'model_fields', {})
|
||||||
|
for name, info in annotation_fields.items():
|
||||||
|
if info.is_required():
|
||||||
|
missing.add(f'{err["loc"][0]}__{name}'.upper())
|
||||||
|
else:
|
||||||
|
missing.add('__'.join(str(loc).upper() for loc in err['loc']))
|
||||||
|
else:
|
||||||
|
missing.add('__'.join(str(loc).upper() for loc in err['loc']))
|
||||||
|
|
||||||
|
for field_name in sorted(missing):
|
||||||
|
print(f' {YELLOW}{field_name}{RESET}')
|
||||||
|
|
||||||
|
print(f'\n{RED}Check .env file or set required variables{RESET}')
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
from tortoise import Tortoise
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseConfig(pydantic.BaseModel):
|
||||||
|
URL: pydantic.PostgresDsn
|
||||||
|
ECHO: bool = False
|
||||||
|
ECHO_POOL: bool = False
|
||||||
|
POOL_SIZE: int = 50
|
||||||
|
MAX_OVERFLOW: int = 10
|
||||||
|
|
||||||
|
|
||||||
|
TORTOISE_ORM = {
|
||||||
|
'connections': {'default': os.getenv('DB__URL')},
|
||||||
|
'apps': {
|
||||||
|
'models': {
|
||||||
|
'models': ['src.domain', 'aerich.models'],
|
||||||
|
'default_connection': 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'use_tz': True,
|
||||||
|
'timezone': 'UTC',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseBase:
|
||||||
|
def __init__(self, config: DatabaseConfig) -> None:
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
async def connect(self) -> None:
|
||||||
|
if self.config.ECHO:
|
||||||
|
logging.getLogger('tortoise.db_client').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('tortoise').setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
await Tortoise.init(
|
||||||
|
db_url=str(self.config.URL),
|
||||||
|
modules={'models': ['src.domain', 'aerich.models']},
|
||||||
|
use_tz=True,
|
||||||
|
timezone='UTC',
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.ping()
|
||||||
|
await self._check_migrations()
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
await Tortoise.close_connections()
|
||||||
|
|
||||||
|
async def ping(self) -> None:
|
||||||
|
from tortoise import connections
|
||||||
|
|
||||||
|
conn = connections.get('default')
|
||||||
|
try:
|
||||||
|
await conn.execute_query('SELECT 1')
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.error(f'Database ping failed: {e}')
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _check_migrations(self) -> None:
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from tortoise import connections
|
||||||
|
|
||||||
|
conn = connections.get('default')
|
||||||
|
|
||||||
|
# Проверяем существование таблицы aerich
|
||||||
|
result = await conn.execute_query(
|
||||||
|
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'aerich')"
|
||||||
|
)
|
||||||
|
if not result[1][0]['exists']:
|
||||||
|
logging.warning('Migrations not applied. Run: aerich upgrade')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Получаем последнюю примененную версию
|
||||||
|
try:
|
||||||
|
result = await conn.execute_query('SELECT version, app FROM aerich ORDER BY id DESC LIMIT 1')
|
||||||
|
if result[1]:
|
||||||
|
current_version = result[1][0]['version']
|
||||||
|
logging.info(f'Current migration version: {current_version}')
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f'Could not check migration version: {e}')
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.19.x]
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: go build
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.19.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: go test -coverprofile=coverage.out -covermode=atomic
|
||||||
|
|
||||||
|
- uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
files: ./coverage.out
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
|
License, supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
|
other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
|
ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from
|
||||||
|
a header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
|
material is not limited to numerical parameters, data structure
|
||||||
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the object code that the
|
||||||
|
Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that,
|
||||||
|
taken together, effectively do not restrict modification of the
|
||||||
|
portions of the Library contained in the Combined Work and reverse
|
||||||
|
engineering for debugging such modifications, if you also do each of
|
||||||
|
the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
d) Do one of the following:
|
||||||
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
|
License, and the Corresponding Application Code in a form
|
||||||
|
suitable for, and under terms that permit, the user to
|
||||||
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.
|
||||||
|
|
||||||
|
1) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (a) uses at run time
|
||||||
|
a copy of the Library already present on the user's computer
|
||||||
|
system, and (b) will operate properly with a modified version
|
||||||
|
of the Library that is interface-compatible with the Linked
|
||||||
|
Version.
|
||||||
|
|
||||||
|
e) Provide Installation Information, but only if you would otherwise
|
||||||
|
be required to provide such information under section 6 of the
|
||||||
|
GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
|
you use option 4d0, the Installation Information must accompany
|
||||||
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
|
Code. If you use option 4d1, you must provide the Installation
|
||||||
|
Information in the manner specified by section 6 of the GNU GPL
|
||||||
|
for conveying Corresponding Source.)
|
||||||
|
|
||||||
|
5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the
|
||||||
|
Library side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work based
|
||||||
|
on the Library, uncombined with any other library facilities,
|
||||||
|
conveyed under the terms of this License.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Library as you received it specifies that a certain numbered version
|
||||||
|
of the GNU Lesser General Public License "or any later version"
|
||||||
|
applies to it, you have the option of following the terms and
|
||||||
|
conditions either of that published version or of any later version
|
||||||
|
published by the Free Software Foundation. If the Library as you
|
||||||
|
received it does not specify a version number of the GNU Lesser
|
||||||
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
|
General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
||||||
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
echotron
|
||||||
|
Copyright (C) 2019 Nicolò Santamaria
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
echotron Copyright (C) 2019 Nicolò Santamaria
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [](https://golang.org/) [](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [](https://codecov.io/gh/NicoNex/echotron) [](https://github.com/NicoNex/echotron/blob/master/LICENSE) [](https://github.com/avelino/awesome-go) [](https://t.me/echotronnews) |
|
||||||
|
| :------: |
|
||||||
|
|
||||||
|
**Echotron** is an elegant and concurrent library for the Telegram bot API in Go.
|
||||||
|
|
||||||
|
Fetch with
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/NicoNex/echotron/v3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
### Simplest implementations
|
||||||
|
#### Long polling
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
api := echotron.NewAPI(token)
|
||||||
|
|
||||||
|
for u := range echotron.PollingUpdates(token) {
|
||||||
|
if u.Message.Text == "/start" {
|
||||||
|
api.SendMessage("Hello world", u.ChatID(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
#### Webhook
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
api := echotron.NewAPI(token)
|
||||||
|
|
||||||
|
for u := range echotron.WebhookUpdates("https://example.com:443/my_token", token) {
|
||||||
|
if u.Message.Text == "/start" {
|
||||||
|
api.SendMessage("Hello world", u.ChatID(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
For more scalable and recommended implementations see the other examples.
|
||||||
|
|
||||||
|
### Long Polling
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Struct useful for managing internal states in your bot, but it could be of
|
||||||
|
// any type such as `type bot int64` if you only need to store the chatID.
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
// This function needs to be of type 'echotron.NewBotFn' and is called by
|
||||||
|
// the echotron dispatcher upon any new message from a chatID that has never
|
||||||
|
// interacted with the bot before.
|
||||||
|
// This means that echotron keeps one instance of the echotron.Bot implementation
|
||||||
|
// for each chat where the bot is used.
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
return &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is needed to implement the echotron.Bot interface.
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// This is the entry point of echotron library.
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
for {
|
||||||
|
log.Println(dsp.Poll())
|
||||||
|
// In case of connection issues wait 5 seconds before trying to reconnect.
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
**Echotron** makes a new instance of the struct bot for each open chat with a Telegram user, channel or group.
|
||||||
|
This allows to:
|
||||||
|
- safely call the `Update(*echotron.Update)` method concurrently
|
||||||
|
- give to the user a convenient way to manage the bot internal states across all the chats
|
||||||
|
- make sure that, even if one instance of the bot is deadlocked, the other ones keep running just fine, making the bot work for other users without any issues and/or slowdowns.
|
||||||
|
|
||||||
|
Please note that the the aforementioned behaviour is dictated by the `echotron.Dispatcher` object whose usage is not mandatory and for special needs can be ignored and implemented in different ways still keeping all the methods in the `echotron.API` object.
|
||||||
|
|
||||||
|
**Echotron** is designed to be as similar to the official [Telegram API](https://core.telegram.org/bots/api) as possible, but there are some things to take into account before starting to work with this library.
|
||||||
|
|
||||||
|
- The methods have the exact same name, but with a capital first letter, since in Go methods have to start with a capital letter to be exported.
|
||||||
|
_Example: `sendMessage` becomes `SendMessage`_
|
||||||
|
- The order of the parameters in some methods is different than in the official Telegram API, so refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for the correct one.
|
||||||
|
- The only `chat_id` (or, in this case, `chatID`) type supported is `int64`, instead of the "Integer or String" requirement of the official API. That's because numeric IDs can't change in any way, which isn't the case with text-based usernames.
|
||||||
|
- In some methods, you might find a `InputFile` type parameter. [`InputFile`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#InputFile) is a struct with unexported fields, since only three combination of fields are valid, which can be obtained through the methods [`NewInputFileID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileID), [`NewInputFilePath`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFilePath) and [`NewInputFileBytes`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileBytes).
|
||||||
|
- In some methods, you might find a `MessageIDOptions` type parameter. [`MessageIDOptions`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#MessageIDOptions) is another struct with unexported fields, since only two combination of field are valid, which can be obtained through the methods [`NewMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewMessageID) and [`NewInlineMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInlineMessageID).
|
||||||
|
- Optional parameters can be added by passing the correct struct to each method that might request optional parameters. If you don't want to pass any optional parameter, `nil` is more than enough. Refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) to check for each method's optional parameters struct: it's the type of the `opts` parameter.
|
||||||
|
- Some parameters are hardcoded to avoid putting random stuff which isn't recognized by the Telegram API. Some notable examples are [`ParseMode`](https://github.com/NicoNex/echotron/blob/master/options.go#L21), [`ChatAction`](https://github.com/NicoNex/echotron/blob/master/options.go#L54) and [`InlineQueryType`](https://github.com/NicoNex/echotron/blob/master/inline.go#L27). For a full list of custom hardcoded parameters, refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for each custom type: by clicking on the type's name, you'll get the source which contains the possible values for that type.
|
||||||
|
|
||||||
|
## Additional examples
|
||||||
|
### Functional approach to state management
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recursive type definition of the bot state function.
|
||||||
|
type stateFn func(*echotron.Update) stateFn
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
state stateFn
|
||||||
|
name string
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
bot := &bot{
|
||||||
|
chatID: chatID,
|
||||||
|
API: echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
// We set the default state to the bot.handleMessage method.
|
||||||
|
bot.state = bot.handleMessage
|
||||||
|
return bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
// Here we execute the current state and set the next one.
|
||||||
|
b.state = b.state(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) handleMessage(update *echotron.Update) stateFn {
|
||||||
|
if strings.HasPrefix(update.Message.Text, "/set_name") {
|
||||||
|
b.SendMessage("Send me my new name!", b.chatID, nil)
|
||||||
|
// Here we return b.handleName since next time we receive a message it
|
||||||
|
// will be the new name.
|
||||||
|
return b.handleName
|
||||||
|
}
|
||||||
|
return b.handleMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) handleName(update *echotron.Update) stateFn {
|
||||||
|
b.name = update.Message.Text
|
||||||
|
b.SendMessage(fmt.Sprintf("My new name is %q", b.name), b.chatID, nil)
|
||||||
|
// Here we return b.handleMessage since the next time we receive a message
|
||||||
|
// it will be handled in the default way.
|
||||||
|
return b.handleMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
log.Println(dsp.Poll())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Self destruction for lower memory footprint
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
var dsp *echotron.Dispatcher
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
bot := &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
go bot.selfDestruct(time.After(time.Hour))
|
||||||
|
return bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) selfDestruct(timech <-chan time.Time) {
|
||||||
|
<-timech
|
||||||
|
b.SendMessage("goodbye", b.chatID, nil)
|
||||||
|
dsp.DelSession(b.chatID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsp = echotron.NewDispatcher(token, newBot)
|
||||||
|
log.Println(dsp.Poll())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
return &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
dsp.ListenWebhook("https://example.com:443/my_bot_token")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Webhook with a custom http.Server
|
||||||
|
|
||||||
|
This is an example for a custom http.Server which handles your own specified routes
|
||||||
|
and also the webhook route which is specified by ListenWebhook.
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
return &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
termChan := make(chan os.Signal, 1) // Channel for terminating the app via os.Interrupt signal
|
||||||
|
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle user login
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle user logout
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Tell something about your awesome telegram bot
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set custom http.Server
|
||||||
|
server := &http.Server{Addr: ":8080", Handler: mux}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-termChan
|
||||||
|
// Perform some cleanup..
|
||||||
|
if err := server.Shutdown(context.Background()); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Capture the interrupt signal for app termination handling
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
dsp.SetHTTPServer(server)
|
||||||
|
// Start your custom http.Server with a registered /my_bot_token handler.
|
||||||
|
log.Println(dsp.ListenWebhook("https://example.com/my_bot_token"))
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// APIError represents an error returned by the Telegram API.
|
||||||
|
type APIError struct {
|
||||||
|
desc string
|
||||||
|
code int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorCode returns the error code received from the Telegram API.
|
||||||
|
func (a *APIError) ErrorCode() int {
|
||||||
|
return a.code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the error description received from the Telegram API.
|
||||||
|
func (a *APIError) Description() string {
|
||||||
|
return a.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the error string.
|
||||||
|
func (a *APIError) Error() string {
|
||||||
|
return fmt.Sprintf("API error: %d %s", a.code, a.desc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var a APIError
|
||||||
|
|
||||||
|
func TestErrorCode(_ *testing.T) {
|
||||||
|
a.ErrorCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDescription(_ *testing.T) {
|
||||||
|
a.Description()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(_ *testing.T) {
|
||||||
|
_ = a.Error()
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 580 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2022 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
// ChatAdministratorRights represents the rights of an administrator in a chat.
|
||||||
|
type ChatAdministratorRights struct {
|
||||||
|
IsAnonymous bool `json:"is_anonymous"`
|
||||||
|
CanManageChat bool `json:"can_manage_chat"`
|
||||||
|
CanDeleteMessages bool `json:"can_delete_messages"`
|
||||||
|
CanManageVideoChats bool `json:"can_manage_video_chats"`
|
||||||
|
CanRestrictMembers bool `json:"can_restrict_members"`
|
||||||
|
CanPromoteMembers bool `json:"can_promote_members"`
|
||||||
|
CanChangeInfo bool `json:"can_change_info"`
|
||||||
|
CanInviteUsers bool `json:"can_invite_users"`
|
||||||
|
CanPostStories bool `json:"can_post_stories"`
|
||||||
|
CanEditStories bool `json:"can_edit_stories"`
|
||||||
|
CanDeleteStories bool `json:"can_delete_stories"`
|
||||||
|
CanPostMessages bool `json:"can_post_messages,omitempty"`
|
||||||
|
CanEditMessages bool `json:"can_edit_messages,omitempty"`
|
||||||
|
CanPinMessages bool `json:"can_pin_messages,omitempty"`
|
||||||
|
CanManageTopics bool `json:"can_manage_topics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMyDefaultAdministratorRightsOptions contains the optional parameters used by
|
||||||
|
// the SetMyDefaultAdministratorRights method.
|
||||||
|
type SetMyDefaultAdministratorRightsOptions struct {
|
||||||
|
Rights ChatAdministratorRights `query:"rights"`
|
||||||
|
ForChannels bool `query:"for_channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyDefaultAdministratorRightsOptions contains the optional parameters used by
|
||||||
|
// the GetMyDefaultAdministratorRights method.
|
||||||
|
type GetMyDefaultAdministratorRightsOptions struct {
|
||||||
|
ForChannels bool `query:"for_channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMyDefaultAdministratorRights is used to change the default administrator rights
|
||||||
|
// requested by the bot when it's added as an administrator to groups or channels.
|
||||||
|
// These rights will be suggested to users, but they are are free to modify the list
|
||||||
|
// before adding the bot.
|
||||||
|
func (a API) SetMyDefaultAdministratorRights(opts *SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
|
||||||
|
return res, client.get(a.base, "setMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyDefaultAdministratorRights is used to get the current default administrator rights of the bot.
|
||||||
|
func (a API) GetMyDefaultAdministratorRights(opts *GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
|
||||||
|
return res, client.get(a.base, "getMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rights = ChatAdministratorRights{
|
||||||
|
IsAnonymous: true,
|
||||||
|
CanManageChat: true,
|
||||||
|
CanDeleteMessages: true,
|
||||||
|
CanManageVideoChats: true,
|
||||||
|
CanRestrictMembers: true,
|
||||||
|
CanPromoteMembers: true,
|
||||||
|
CanChangeInfo: true,
|
||||||
|
CanInviteUsers: true,
|
||||||
|
CanPostStories: true,
|
||||||
|
CanEditStories: true,
|
||||||
|
CanDeleteStories: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetMyDefaultAdministratorRights(t *testing.T) {
|
||||||
|
_, err := api.SetMyDefaultAdministratorRights(
|
||||||
|
&SetMyDefaultAdministratorRightsOptions{
|
||||||
|
Rights: rights,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMyDefaultAdministratorRights(t *testing.T) {
|
||||||
|
res, err := api.GetMyDefaultAdministratorRights(nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(*res.Result, rights) {
|
||||||
|
t.Logf("expected: %+v", rights)
|
||||||
|
t.Logf("got: %+v", res.Result)
|
||||||
|
t.Fatal("error: chat administrator rights mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type smap sync.Map
|
||||||
|
|
||||||
|
func (s *smap) load(id int64) (Bot, bool) {
|
||||||
|
bot, ok := (*sync.Map)(s).Load(id)
|
||||||
|
if ok {
|
||||||
|
return bot.(Bot), ok
|
||||||
|
}
|
||||||
|
return nil, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *smap) store(id int64, bot Bot) {
|
||||||
|
(*sync.Map)(s).Store(id, bot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *smap) delete(id int64) {
|
||||||
|
(*sync.Map)(s).Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bot is the interface that must be implemented by your definition of
|
||||||
|
// the struct thus it represent each open session with a user on Telegram.
|
||||||
|
type Bot interface {
|
||||||
|
// Update will be called upon receiving any update from Telegram.
|
||||||
|
Update(*Update)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBotFn is called every time echotron receives an update with a chat ID never
|
||||||
|
// encountered before.
|
||||||
|
type NewBotFn func(chatId int64) Bot
|
||||||
|
|
||||||
|
// The Dispatcher passes the updates from the Telegram Bot API to the Bot instance
|
||||||
|
// associated with each chatID. When a new chat ID is found, the provided function
|
||||||
|
// of type NewBotFn will be called.
|
||||||
|
type Dispatcher struct {
|
||||||
|
sessions smap
|
||||||
|
newBot NewBotFn
|
||||||
|
updates chan *Update
|
||||||
|
httpServer *http.Server
|
||||||
|
api API
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDispatcher returns a new instance of the Dispatcher object.
|
||||||
|
// Calls the Update function of the bot associated with each chat ID.
|
||||||
|
// If a new chat ID is found, newBotFn will be called first.
|
||||||
|
func NewDispatcher(token string, newBotFn NewBotFn) *Dispatcher {
|
||||||
|
d := &Dispatcher{
|
||||||
|
api: NewAPI(token),
|
||||||
|
newBot: newBotFn,
|
||||||
|
updates: make(chan *Update),
|
||||||
|
}
|
||||||
|
go d.listen()
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelSession deletes the Bot instance, seen as a session, from the
|
||||||
|
// map with all of them.
|
||||||
|
func (d *Dispatcher) DelSession(chatID int64) {
|
||||||
|
d.sessions.delete(chatID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSession allows to arbitrarily create a new Bot instance.
|
||||||
|
func (d *Dispatcher) AddSession(chatID int64) {
|
||||||
|
d.sessions.store(chatID, d.newBot(chatID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll is a wrapper function for PollOptions.
|
||||||
|
func (d *Dispatcher) Poll() error {
|
||||||
|
return d.PollOptions(true, UpdateOptions{Timeout: 120})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollOptions starts the polling loop so that the dispatcher calls the function Update
|
||||||
|
// upon receiving any update from Telegram.
|
||||||
|
func (d *Dispatcher) PollOptions(dropPendingUpdates bool, opts UpdateOptions) error {
|
||||||
|
var (
|
||||||
|
timeout = opts.Timeout
|
||||||
|
isFirstRun = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// deletes webhook if present to run in long polling mode
|
||||||
|
if _, err := d.api.DeleteWebhook(dropPendingUpdates); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if isFirstRun {
|
||||||
|
opts.Timeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := d.api.GetUpdates(&opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dropPendingUpdates || !isFirstRun {
|
||||||
|
for _, u := range response.Result {
|
||||||
|
d.updates <- u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(response.Result); l > 0 {
|
||||||
|
opts.Offset = response.Result[l-1].ID + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFirstRun {
|
||||||
|
isFirstRun = false
|
||||||
|
opts.Timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dispatcher) instance(chatID int64) Bot {
|
||||||
|
bot, ok := d.sessions.load(chatID)
|
||||||
|
if !ok {
|
||||||
|
bot = d.newBot(chatID)
|
||||||
|
d.sessions.store(chatID, bot)
|
||||||
|
}
|
||||||
|
return bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dispatcher) listen() {
|
||||||
|
for update := range d.updates {
|
||||||
|
bot := d.instance(update.ChatID())
|
||||||
|
go bot.Update(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenWebhook is a wrapper function for ListenWebhookOptions.
|
||||||
|
func (d *Dispatcher) ListenWebhook(webhookURL string) error {
|
||||||
|
return d.ListenWebhookOptions(webhookURL, false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenWebhookOptions sets a webhook and listens for incoming updates.
|
||||||
|
// The webhookUrl should be provided in the following format: '<hostname>:<port>/<path>',
|
||||||
|
// eg: 'https://example.com:443/bot_token'.
|
||||||
|
// ListenWebhook will then proceed to communicate the webhook url '<hostname>/<path>' to Telegram
|
||||||
|
// and run a webserver that listens to ':<port>' and handles the path.
|
||||||
|
func (d *Dispatcher) ListenWebhookOptions(webhookURL string, dropPendingUpdates bool, opts *WebhookOptions) error {
|
||||||
|
u, err := url.Parse(webhookURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
whURL := fmt.Sprintf("%s%s", u.Hostname(), u.EscapedPath())
|
||||||
|
if _, err = d.api.SetWebhook(whURL, dropPendingUpdates, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.httpServer != nil {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/", d.httpServer.Handler)
|
||||||
|
mux.HandleFunc(u.EscapedPath(), d.HandleWebhook)
|
||||||
|
d.httpServer.Handler = mux
|
||||||
|
return d.httpServer.ListenAndServe()
|
||||||
|
}
|
||||||
|
http.HandleFunc(u.EscapedPath(), d.HandleWebhook)
|
||||||
|
return http.ListenAndServe(fmt.Sprintf(":%s", u.Port()), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHTTPServer allows to set a custom http.Server for ListenWebhook and ListenWebhookOptions.
|
||||||
|
func (d *Dispatcher) SetHTTPServer(s *http.Server) {
|
||||||
|
d.httpServer = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleWebhook is the http.HandlerFunc for the webhook URL.
|
||||||
|
// Useful if you've already a http server running and want to handle the request yourself.
|
||||||
|
func (d *Dispatcher) HandleWebhook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var update Update
|
||||||
|
|
||||||
|
jsn, err := readRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("echotron.Dispatcher", "HandleWebhook", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(jsn, &update); err != nil {
|
||||||
|
log.Println("echotron.Dispatcher", "HandleWebhook", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.updates <- &update
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRequest(r *http.Request) ([]byte, error) {
|
||||||
|
switch r.Header.Get("Content-Encoding") {
|
||||||
|
case "gzip":
|
||||||
|
reader, err := gzip.NewReader(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
return io.ReadAll(reader)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return io.ReadAll(r.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type test struct{}
|
||||||
|
|
||||||
|
func (t test) Update(_ *Update) {}
|
||||||
|
|
||||||
|
var dsp *Dispatcher
|
||||||
|
|
||||||
|
func TestNewDispatcher(t *testing.T) {
|
||||||
|
if dsp = NewDispatcher("token", func(_ int64) Bot { return test{} }); dsp == nil {
|
||||||
|
t.Fatal("dispatcher is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddSession(t *testing.T) {
|
||||||
|
dsp.AddSession(0)
|
||||||
|
|
||||||
|
if _, ok := dsp.sessions.load(0); !ok {
|
||||||
|
t.Fatal("could not add session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelSession(t *testing.T) {
|
||||||
|
dsp.DelSession(0)
|
||||||
|
|
||||||
|
if _, ok := dsp.sessions.load(0); ok {
|
||||||
|
t.Fatal("could not delete session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenWebhook(_ *testing.T) {
|
||||||
|
dsp.ListenWebhook("http://example.com:8443/test")
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoll(_ *testing.T) {
|
||||||
|
dsp.Poll()
|
||||||
|
|
||||||
|
dsp.updates <- &Update{}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
ChatJoinRequest: &ChatJoinRequest{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
ChatBoost: &ChatBoostUpdated{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
RemovedChatBoost: &ChatBoostRemoved{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
Message: &Message{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
EditedMessage: &Message{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
ChannelPost: &Message{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
EditedChannelPost: &Message{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
BusinessConnection: &BusinessConnection{
|
||||||
|
User: User{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
BusinessMessage: &Message{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
EditedBusinessMessage: &Message{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
DeletedBusinessMessages: &BusinessMessagesDeleted{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
MessageReaction: &MessageReactionUpdated{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
MessageReactionCount: &MessageReactionCountUpdated{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
InlineQuery: &InlineQuery{
|
||||||
|
From: &User{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
ChosenInlineResult: &ChosenInlineResult{
|
||||||
|
From: &User{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
CallbackQuery: &CallbackQuery{
|
||||||
|
Message: &Message{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
ShippingQuery: &ShippingQuery{
|
||||||
|
From: User{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
PreCheckoutQuery: &PreCheckoutQuery{
|
||||||
|
From: User{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
PollAnswer: &PollAnswer{
|
||||||
|
User: &User{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
MyChatMember: &ChatMemberUpdated{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp.updates <- &Update{
|
||||||
|
ChatMember: &ChatMemberUpdated{
|
||||||
|
Chat: Chat{ID: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import "net/url"
|
||||||
|
|
||||||
|
// Game represents a game.
|
||||||
|
type Game struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Photo []PhotoSize `json:"photo"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
TextEntities []MessageEntity `json:"text_entities,omitempty"`
|
||||||
|
Animation Animation `json:"animation,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallbackGame is a placeholder, currently holds no information.
|
||||||
|
type CallbackGame struct{}
|
||||||
|
|
||||||
|
// GameHighScore represents one row of the high scores table for a game.
|
||||||
|
type GameHighScore struct {
|
||||||
|
User User `json:"user"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GameScoreOptions contains the optional parameters used in SetGameScore method.
|
||||||
|
type GameScoreOptions struct {
|
||||||
|
Force bool `query:"force"`
|
||||||
|
DisableEditMessage bool `query:"disable_edit_message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendGame is used to send a Game.
|
||||||
|
func (a API) SendGame(gameShortName string, chatID int64, opts *BaseOptions) (res APIResponseMessage, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("chat_id", itoa(chatID))
|
||||||
|
vals.Set("game_short_name", gameShortName)
|
||||||
|
return res, client.get(a.base, "sendGame", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGameScore is used to set the score of the specified user in a game.
|
||||||
|
func (a API) SetGameScore(userID int64, score int, msgID MessageIDOptions, opts *GameScoreOptions) (res APIResponseMessage, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("score", itoa(int64(score)))
|
||||||
|
return res, client.get(a.base, "setGameScore", addValues(addValues(vals, msgID), opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGameHighScores is used to get data for high score tables.
|
||||||
|
func (a API) GetGameHighScores(userID int64, opts MessageIDOptions) (res APIResponseGameHighScore, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
return res, client.get(a.base, "getGameHighScores", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var (
|
||||||
|
gameMsgTmp *Message
|
||||||
|
highScores []*GameHighScore
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSendGame(t *testing.T) {
|
||||||
|
resp, err := api.SendGame(
|
||||||
|
"echotron_coverage_game",
|
||||||
|
chatID,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gameMsgTmp = resp.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameHighScores(t *testing.T) {
|
||||||
|
resp, err := api.GetGameHighScores(
|
||||||
|
chatID,
|
||||||
|
NewMessageID(chatID, gameMsgTmp.ID),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
highScores = resp.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetGameScore(t *testing.T) {
|
||||||
|
var score int
|
||||||
|
|
||||||
|
if len(highScores) > 0 {
|
||||||
|
score = highScores[0].Score + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := api.SetGameScore(
|
||||||
|
chatID,
|
||||||
|
score,
|
||||||
|
NewMessageID(chatID, gameMsgTmp.ID),
|
||||||
|
&GameScoreOptions{
|
||||||
|
Force: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/NicoNex/echotron/v3
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require golang.org/x/time v0.5.0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// content contains a file's name, its type and its data.
|
||||||
|
type content struct {
|
||||||
|
fname string
|
||||||
|
ftype string
|
||||||
|
fdata []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(r APIResponse) error {
|
||||||
|
if b := r.Base(); !b.Ok {
|
||||||
|
return &APIError{code: b.ErrorCode, desc: b.Description}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processMedia(media, thumbnail InputFile) (im mediaEnvelope, cnt []content, err error) {
|
||||||
|
switch {
|
||||||
|
case media.id != "":
|
||||||
|
im = mediaEnvelope{
|
||||||
|
media: media.id,
|
||||||
|
thumbnail: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
case media.url != "":
|
||||||
|
im = mediaEnvelope{
|
||||||
|
media: media.url,
|
||||||
|
thumbnail: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
case media.path != "" && len(media.content) == 0:
|
||||||
|
if media.content, media.path, err = readFile(media); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case media.path != "" && len(media.content) > 0:
|
||||||
|
cnt = append(cnt, content{media.path, media.path, media.content})
|
||||||
|
im = mediaEnvelope{
|
||||||
|
media: fmt.Sprintf("attach://%s", media.path),
|
||||||
|
thumbnail: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case thumbnail.path != "" && len(thumbnail.content) == 0:
|
||||||
|
if thumbnail.content, thumbnail.path, err = readFile(thumbnail); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case thumbnail.path != "" && len(thumbnail.content) > 0:
|
||||||
|
cnt = append(cnt, content{thumbnail.path, thumbnail.path, thumbnail.content})
|
||||||
|
im.thumbnail = fmt.Sprintf("attach://%s", thumbnail.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func processSticker(sticker InputFile) (se stickerEnvelope, cnt []content, err error) {
|
||||||
|
switch {
|
||||||
|
case sticker.id != "":
|
||||||
|
se.Sticker = sticker.id
|
||||||
|
|
||||||
|
case sticker.url != "":
|
||||||
|
se.Sticker = sticker.url
|
||||||
|
|
||||||
|
case sticker.path != "" && len(sticker.content) == 0:
|
||||||
|
if sticker.content, sticker.path, err = readFile(sticker); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case sticker.path != "" && len(sticker.content) > 0:
|
||||||
|
cnt = append(cnt, content{sticker.path, sticker.path, sticker.content})
|
||||||
|
se.Sticker = fmt.Sprintf("attach://%s", sticker.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(im InputFile) (content []byte, path string, err error) {
|
||||||
|
content, err = os.ReadFile(im.path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path = filepath.Base(im.path)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func toContent(ftype string, f InputFile) (content, error) {
|
||||||
|
if f.path != "" && len(f.content) == 0 {
|
||||||
|
var err error
|
||||||
|
if f.content, f.path, err = readFile(f); err != nil {
|
||||||
|
return content{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content{f.path, ftype, f.content}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInputMedia(media []GroupableInputMedia) (ret []InputMedia) {
|
||||||
|
ret = make([]InputMedia, len(media))
|
||||||
|
|
||||||
|
for i, v := range media {
|
||||||
|
ret[i] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinURL(base, endpoint string, vals url.Values) (addr string, err error) {
|
||||||
|
addr, err = url.JoinPath(base, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if vals != nil {
|
||||||
|
if queries := vals.Encode(); queries != "" {
|
||||||
|
addr = fmt.Sprintf("%s?%s", addr, queries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func itoa(i int64) string {
|
||||||
|
return strconv.FormatInt(i, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ftoa(f float64) string {
|
||||||
|
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func btoa(b bool) string {
|
||||||
|
return strconv.FormatBool(b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InlineQueryType is a custom type for the various InlineQueryResult*'s Type field.
|
||||||
|
type InlineQueryType string
|
||||||
|
|
||||||
|
// These are all the possible types for the various InlineQueryResult*'s Type field.
|
||||||
|
const (
|
||||||
|
InlineArticle InlineQueryType = "article"
|
||||||
|
InlinePhoto = "photo"
|
||||||
|
InlineGIF = "gif"
|
||||||
|
InlineMPEG4GIF = "mpeg4_gif"
|
||||||
|
InlineVideo = "video"
|
||||||
|
InlineAudio = "audio"
|
||||||
|
InlineVoice = "voice"
|
||||||
|
InlineDocument = "document"
|
||||||
|
InlineLocation = "location"
|
||||||
|
InlineVenue = "venue"
|
||||||
|
InlineContact = "contact"
|
||||||
|
InlineGame = "game"
|
||||||
|
InlineSticker = "sticker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InlineQuery represents an incoming inline query.
|
||||||
|
// When the user sends an empty query, your bot could return some default or trending results.
|
||||||
|
type InlineQuery struct {
|
||||||
|
From *User `json:"from"`
|
||||||
|
Location *Location `json:"location,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
Offset string `json:"offset"`
|
||||||
|
ChatType string `json:"chat_type,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChosenInlineResult represents a result of an inline query that was chosen by the user and sent to their chat partner.
|
||||||
|
type ChosenInlineResult struct {
|
||||||
|
ResultID string `json:"result_id"`
|
||||||
|
From *User `json:"from"`
|
||||||
|
Location *Location `json:"location,omitempty"`
|
||||||
|
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlineQueryResult represents an interface that implements all the various InlineQueryResult* types.
|
||||||
|
type InlineQueryResult interface {
|
||||||
|
ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlineQueryResultArticle represents a link to an article or web page.
|
||||||
|
type InlineQueryResultArticle struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||||
|
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultArticle) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultPhoto represents a link to a photo.
|
||||||
|
// By default, this photo will be sent by the user with optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
|
||||||
|
type InlineQueryResultPhoto struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url"`
|
||||||
|
PhotoURL string `json:"photo_url"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
PhotoHeight int `json:"photo_height,omitempty"`
|
||||||
|
PhotoWidth int `json:"photo_width,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultPhoto) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultGif represents a link to an animated GIF file.
|
||||||
|
// By default, this animated GIF file will be sent by the user with optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
|
||||||
|
type InlineQueryResultGif struct {
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
GifURL string `json:"gif_url"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
GifDuration int `json:"gif_duration,omitempty"`
|
||||||
|
GifHeight int `json:"gif_height,omitempty"`
|
||||||
|
GifWidth int `json:"gif_width,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultGif) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultMpeg4Gif represents a link to a video animation (H.264/MPEG-4 AVC video without sound).
|
||||||
|
// By default, this animated MPEG-4 file will be sent by the user with optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
|
||||||
|
type InlineQueryResultMpeg4Gif struct {
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Mpeg4URL string `json:"mpeg4_url"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
Mpeg4Duration int `json:"mpeg4_duration,omitempty"`
|
||||||
|
Mpeg4Height int `json:"mpeg4_height,omitempty"`
|
||||||
|
Mpeg4Width int `json:"mpeg4_width,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultMpeg4Gif) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultVideo represents a link to a page containing an embedded video player or a video file.
|
||||||
|
// By default, this video file will be sent by the user with an optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
|
||||||
|
type InlineQueryResultVideo struct {
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
VideoURL string `json:"video_url"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
VideoHeight int `json:"video_height,omitempty"`
|
||||||
|
VideoDuration int `json:"video_duration,omitempty"`
|
||||||
|
VideoWidth int `json:"video_width,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultVideo) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultAudio represents a link to an MP3 audio file.
|
||||||
|
// By default, this audio file will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the audio.
|
||||||
|
type InlineQueryResultAudio struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
AudioURL string `json:"audio_url"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
Performer string `json:"performer,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
AudioDuration int `json:"audio_duration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultAudio) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultVoice represents a link to a voice recording in an .OGG container encoded with OPUS.
|
||||||
|
// By default, this voice recording will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the the voice message.
|
||||||
|
type InlineQueryResultVoice struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
VoiceURL string `json:"voice_url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
VoiceDuration int `json:"voice_duration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultVoice) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultDocument represents a link to a file.
|
||||||
|
// By default, this file will be sent by the user with an optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the file.
|
||||||
|
// Currently, only .PDF and .ZIP files can be sent using this method.
|
||||||
|
type InlineQueryResultDocument struct {
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||||
|
DocumentURL string `json:"document_url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||||
|
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultDocument) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultLocation represents a location on a map.
|
||||||
|
// By default, the location will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the location.
|
||||||
|
type InlineQueryResultLocation struct {
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
LivePeriod int `json:"live_period,omitempty"`
|
||||||
|
HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"`
|
||||||
|
ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||||
|
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||||
|
Heading int `json:"heading,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultLocation) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultVenue represents a venue.
|
||||||
|
// By default, the venue will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the venue.
|
||||||
|
type InlineQueryResultVenue struct {
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
GooglePlaceType string `json:"google_place_type,omitempty"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
FoursquareID string `json:"foursquare_id,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
GooglePlaceID string `json:"google_place_id,omitempty"`
|
||||||
|
FoursquareType string `json:"foursquare_type,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||||
|
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultVenue) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultContact represents a contact with a phone number.
|
||||||
|
// By default, this contact will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the contact.
|
||||||
|
type InlineQueryResultContact struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
VCard string `json:"vcard,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||||
|
LastName string `json:"last_name,omitempty"`
|
||||||
|
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||||
|
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultContact) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultGame represents a Game.
|
||||||
|
type InlineQueryResultGame struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
GameShortName string `json:"game_short_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultGame) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedPhoto represents a link to a photo stored on the Telegram servers.
|
||||||
|
// By default, this photo will be sent by the user with an optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
|
||||||
|
type InlineQueryResultCachedPhoto struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
PhotoFileID string `json:"photo_file_id"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedPhoto) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedGif represents a link to an animated GIF file stored on the Telegram servers.
|
||||||
|
// By default, this animated GIF file will be sent by the user with an optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with specified content instead of the animation.
|
||||||
|
type InlineQueryResultCachedGif struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
GifFileID string `json:"gif_file_id"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedGif) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedMpeg4Gif represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers.
|
||||||
|
// By default, this animated MPEG-4 file will be sent by the user with an optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
|
||||||
|
type InlineQueryResultCachedMpeg4Gif struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Mpeg4FileID string `json:"mpeg4_file_id"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedMpeg4Gif) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedSticker represents a link to a sticker stored on the Telegram servers.
|
||||||
|
// By default, this sticker will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the sticker.
|
||||||
|
type InlineQueryResultCachedSticker struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
StickerFileID string `json:"sticker_file_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedSticker) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedDocument represents a link to a file stored on the Telegram servers.
|
||||||
|
// By default, this file will be sent by the user with an optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the file.
|
||||||
|
type InlineQueryResultCachedDocument struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
DocumentFileID string `json:"document_file_id"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedDocument) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedVideo represents a link to a video file stored on the Telegram servers.
|
||||||
|
// By default, this video file will be sent by the user with an optional caption.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
|
||||||
|
type InlineQueryResultCachedVideo struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
VideoFileID string `json:"video_file_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedVideo) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedVoice represents a link to a voice message stored on the Telegram servers.
|
||||||
|
// By default, this voice message will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the voice message.
|
||||||
|
type InlineQueryResultCachedVoice struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
VoiceFileID string `json:"voice_file_id"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedVoice) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InlineQueryResultCachedAudio represents a link to an MP3 audio file stored on the Telegram servers.
|
||||||
|
// By default, this audio file will be sent by the user.
|
||||||
|
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the audio.
|
||||||
|
type InlineQueryResultCachedAudio struct {
|
||||||
|
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
|
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
|
||||||
|
AudioFileID string `json:"audio_file_id"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
Type InlineQueryType `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||||
|
func (i InlineQueryResultCachedAudio) ImplementsInlineQueryResult() {}
|
||||||
|
|
||||||
|
// InputMessageContent represents an interface that implements all the various Input*MessageContent types.
|
||||||
|
type InputMessageContent interface {
|
||||||
|
ImplementsInputMessageContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputTextMessageContent represents the content of a text message to be sent as the result of an inline query.
|
||||||
|
type InputTextMessageContent struct {
|
||||||
|
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||||
|
MessageText string `json:"message_text"`
|
||||||
|
ParseMode string `json:"parse_mode,omitempty"`
|
||||||
|
Entities []*MessageEntity `json:"entities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||||
|
func (i InputTextMessageContent) ImplementsInputMessageContent() {}
|
||||||
|
|
||||||
|
// InputLocationMessageContent represents the content of a location message to be sent as the result of an inline query.
|
||||||
|
type InputLocationMessageContent struct {
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"`
|
||||||
|
LivePeriod int `json:"live_period,omitempty"`
|
||||||
|
Heading int `json:"heading,omitempty"`
|
||||||
|
ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||||
|
func (i InputLocationMessageContent) ImplementsInputMessageContent() {}
|
||||||
|
|
||||||
|
// InputVenueMessageContent represents the content of a venue message to be sent as the result of an inline query.
|
||||||
|
type InputVenueMessageContent struct {
|
||||||
|
GooglePlaceID string `json:"google_place_id,omitempty"`
|
||||||
|
GooglePlaceType string `json:"google_place_type,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
FoursquareID string `json:"foursquare_id,omitempty"`
|
||||||
|
FoursquareType string `json:"foursquare_type,omitempty"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||||
|
func (i InputVenueMessageContent) ImplementsInputMessageContent() {}
|
||||||
|
|
||||||
|
// InputContactMessageContent represents the content of a contact message to be sent as the result of an inline query.
|
||||||
|
type InputContactMessageContent struct {
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name,omitempty"`
|
||||||
|
VCard string `json:"vcard,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||||
|
func (i InputContactMessageContent) ImplementsInputMessageContent() {}
|
||||||
|
|
||||||
|
// InputInvoiceMessageContent represents the content of an invoice message to be sent as the result of an inline query.
|
||||||
|
type InputInvoiceMessageContent struct {
|
||||||
|
SuggestedTipAmounts *[]int `json:"suggested_tip_amounts,omitempty"`
|
||||||
|
PhotoURL string `json:"photo_url,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Payload string `json:"string"`
|
||||||
|
ProviderToken string `json:"provider_token,omitempty"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
ProviderData string `json:"provider_data,omitempty"`
|
||||||
|
Prices []LabeledPrice `json:"prices"`
|
||||||
|
PhotoSize int `json:"photo_size,omitempty"`
|
||||||
|
MaxTipAmount int `json:"max_tip_amount,omitempty"`
|
||||||
|
PhotoWidth int `json:"photo_width,omitempty"`
|
||||||
|
PhotoHeight int `json:"photo_height,omitempty"`
|
||||||
|
NeedName bool `json:"need_name,omitempty"`
|
||||||
|
NeedPhoneNumber bool `json:"need_phone_number,omitempty"`
|
||||||
|
NeedEmail bool `json:"need_email,omitempty"`
|
||||||
|
NeedShippingAddress bool `json:"need_shipping_address,omitempty"`
|
||||||
|
SendPhoneNumberToProvider bool `json:"send_phone_number_to_provider,omitempty"`
|
||||||
|
SendEmailToProvider bool `json:"send_email_to_provider,omitempty"`
|
||||||
|
IsFlexible bool `json:"is_flexible,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||||
|
func (i InputInvoiceMessageContent) ImplementsInputMessageContent() {}
|
||||||
|
|
||||||
|
// InlineQueryResultsButton represents a button to be shown above inline query results.
|
||||||
|
// You MUST use exactly one of the fields.
|
||||||
|
type InlineQueryResultsButton struct {
|
||||||
|
WebApp WebAppInfo `json:"web_app,omitempty"`
|
||||||
|
StartParameter string `json:"start_parameter,omitempty"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreparedInlineMessage describes an inline message to be sent by a user of a Mini App.
|
||||||
|
type PreparedInlineMessage struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ExpirationDate int `json:"expiration_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlineQueryOptions is a custom type which contains the various options required by the AnswerInlineQuery method.
|
||||||
|
type InlineQueryOptions struct {
|
||||||
|
Button InlineQueryResultsButton `query:"button"`
|
||||||
|
NextOffset string `query:"next_offset"`
|
||||||
|
CacheTime int `query:"cache_time"`
|
||||||
|
IsPersonal bool `query:"is_personal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreparedInlineMessageOptions is a custom type which contains the various options required by the SavePreparedInlineMessage method.
|
||||||
|
type PreparedInlineMessageOptions struct {
|
||||||
|
AllowUserChats bool `query:"allow_user_chats"`
|
||||||
|
AllowBotChats bool `query:"allow_bot_chats"`
|
||||||
|
AllowGroupChats bool `query:"allow_group_chats"`
|
||||||
|
AllowChannelChats bool `query:"allow_channel_chats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnswerInlineQuery is used to send answers to an inline query.
|
||||||
|
func (a API) AnswerInlineQuery(inlineQueryID string, results []InlineQueryResult, opts *InlineQueryOptions) (res APIResponseBase, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
jsn, _ := json.Marshal(results)
|
||||||
|
vals.Set("inline_query_id", inlineQueryID)
|
||||||
|
vals.Set("results", string(jsn))
|
||||||
|
return res, client.get(a.base, "answerInlineQuery", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePreparedInlineMessage stores a message that can be sent by a user of a Mini App.
|
||||||
|
func (a API) SavePreparedInlineMessage(userID int64, result InlineQueryResult, opts *PreparedInlineMessageOptions) (res APIResponsePreparedInlineMessage, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
jsn, _ := json.Marshal(result)
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("result", string(jsn))
|
||||||
|
return res, client.get(a.base, "savePreparedInlineMessage", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestInlineQueryResultArticle(_ *testing.T) {
|
||||||
|
i := InlineQueryResultArticle{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultPhoto(_ *testing.T) {
|
||||||
|
i := InlineQueryResultPhoto{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultGif(_ *testing.T) {
|
||||||
|
i := InlineQueryResultGif{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultMpeg4Gif(_ *testing.T) {
|
||||||
|
i := InlineQueryResultMpeg4Gif{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultVideo(_ *testing.T) {
|
||||||
|
i := InlineQueryResultVideo{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultAudio(_ *testing.T) {
|
||||||
|
i := InlineQueryResultAudio{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultVoice(_ *testing.T) {
|
||||||
|
i := InlineQueryResultVoice{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultDocument(_ *testing.T) {
|
||||||
|
i := InlineQueryResultDocument{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultLocation(_ *testing.T) {
|
||||||
|
i := InlineQueryResultLocation{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultVenue(_ *testing.T) {
|
||||||
|
i := InlineQueryResultVenue{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultContact(_ *testing.T) {
|
||||||
|
i := InlineQueryResultContact{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultGame(_ *testing.T) {
|
||||||
|
i := InlineQueryResultGame{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedPhoto(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedPhoto{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedGif(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedGif{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedMpeg4Gif(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedMpeg4Gif{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedSticker(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedSticker{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedDocument(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedDocument{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedVideo(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedVideo{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedVoice(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedVoice{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineQueryResultCachedAudio(_ *testing.T) {
|
||||||
|
i := InlineQueryResultCachedAudio{}
|
||||||
|
i.ImplementsInlineQueryResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputTextMessageContent(_ *testing.T) {
|
||||||
|
i := InputTextMessageContent{}
|
||||||
|
i.ImplementsInputMessageContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputLocationMessageContent(_ *testing.T) {
|
||||||
|
i := InputLocationMessageContent{}
|
||||||
|
i.ImplementsInputMessageContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputVenueMessageContent(_ *testing.T) {
|
||||||
|
i := InputVenueMessageContent{}
|
||||||
|
i.ImplementsInputMessageContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputContactMessageContent(_ *testing.T) {
|
||||||
|
i := InputContactMessageContent{}
|
||||||
|
i.ImplementsInputMessageContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputInvoiceMessageContent(_ *testing.T) {
|
||||||
|
i := InputInvoiceMessageContent{}
|
||||||
|
i.ImplementsInputMessageContent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2022 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
// MenuButtonType is a custom type for the various MenuButton*'s Type field.
|
||||||
|
type MenuButtonType string
|
||||||
|
|
||||||
|
// These are all the possible types for the various MenuButton*'s Type field.
|
||||||
|
const (
|
||||||
|
MenuButtonTypeCommands MenuButtonType = "commands"
|
||||||
|
MenuButtonTypeWebApp = "web_app"
|
||||||
|
MenuButtonTypeDefault = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MenuButton is a unique type for MenuButtonCommands, MenuButtonWebApp and MenuButtonDefault
|
||||||
|
type MenuButton struct {
|
||||||
|
WebApp *WebAppInfo `json:"web_app,omitempty"`
|
||||||
|
Type MenuButtonType `json:"type"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChatMenuButtonOptions contains the optional parameters used by the SetChatMenuButton method.
|
||||||
|
type SetChatMenuButtonOptions struct {
|
||||||
|
MenuButton MenuButton `query:"menu_button"`
|
||||||
|
ChatID int64 `query:"chat_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatMenuButtonOptions contains the optional parameters used by the GetChatMenuButton method.
|
||||||
|
type GetChatMenuButtonOptions struct {
|
||||||
|
ChatID int64 `query:"chat_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChatMenuButton is used to change the bot's menu button in a private chat, or the default menu button.
|
||||||
|
func (a API) SetChatMenuButton(opts *SetChatMenuButtonOptions) (res APIResponseBool, err error) {
|
||||||
|
return res, client.get(a.base, "setChatMenuButton", urlValues(opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatMenuButton is used to get the current value of the bot's menu button in a private chat, or the default menu button.
|
||||||
|
func (a API) GetChatMenuButton(opts *GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
|
||||||
|
return res, client.get(a.base, "getChatMenuButton", urlValues(opts), &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
menuBtn = MenuButton{
|
||||||
|
Type: MenuButtonTypeCommands,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetChatMenuButton(t *testing.T) {
|
||||||
|
_, err := api.SetChatMenuButton(
|
||||||
|
&SetChatMenuButtonOptions{
|
||||||
|
MenuButton: menuBtn,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetChatMenuButton(t *testing.T) {
|
||||||
|
res, err := api.GetChatMenuButton(nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(*res.Result, menuBtn) {
|
||||||
|
t.Logf("expected menu button: %+v", menuBtn)
|
||||||
|
t.Logf("got menu button: %+v", res.Result)
|
||||||
|
t.Fatal("error: menu buttons mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type lclient struct {
|
||||||
|
*http.Client
|
||||||
|
*sync.RWMutex
|
||||||
|
cl map[string]*rate.Limiter // chat based limiter
|
||||||
|
gl *rate.Limiter // global limiter
|
||||||
|
climiter func() *rate.Limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = &lclient{
|
||||||
|
Client: new(http.Client),
|
||||||
|
RWMutex: new(sync.RWMutex),
|
||||||
|
cl: make(map[string]*rate.Limiter),
|
||||||
|
gl: rate.NewLimiter(rate.Every(time.Second/30), 30),
|
||||||
|
climiter: func() *rate.Limiter {
|
||||||
|
return rate.NewLimiter(rate.Every(time.Minute/20), 20)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGlobalRequestLimit sets the global rate limit for requests to the Telegram API.
|
||||||
|
// An interval of 0 disables the rate limiter, allowing unlimited requests.
|
||||||
|
// By default the interval of this limiter is set to time.Second/30 and the
|
||||||
|
// burstSize is set to 30.
|
||||||
|
func SetGlobalRequestLimit(interval time.Duration, burstSize int) {
|
||||||
|
client.Lock()
|
||||||
|
client.gl = rate.NewLimiter(rate.Every(interval), burstSize)
|
||||||
|
client.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChatRequestLimit sets the per-chat rate limit for requests to the Telegram API.
|
||||||
|
// An interval of 0 disables the rate limiter, allowing unlimited requests.
|
||||||
|
// By default the interval of this limiter is set to time.Minute/20 and the
|
||||||
|
// burstSize is set to 20.
|
||||||
|
func SetChatRequestLimit(interval time.Duration, burstSize int) {
|
||||||
|
client.Lock()
|
||||||
|
client.cl = make(map[string]*rate.Limiter)
|
||||||
|
client.climiter = func() *rate.Limiter {
|
||||||
|
return rate.NewLimiter(rate.Every(interval), burstSize)
|
||||||
|
}
|
||||||
|
client.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) wait(chatID string) error {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
// If the chatID is empty, it's a general API call like GetUpdates, GetMe
|
||||||
|
// and similar, so skip the per-chat request limit wait.
|
||||||
|
if chatID != "" {
|
||||||
|
// If no limiter exists for a chat, create one.
|
||||||
|
l, ok := c.cl[chatID]
|
||||||
|
if !ok {
|
||||||
|
l = c.climiter()
|
||||||
|
c.cl[chatID] = l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to respect the single chat limit of requests.
|
||||||
|
if err := l.Wait(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to respect the global limit of requests.
|
||||||
|
return c.gl.Wait(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) doGet(reqURL string) ([]byte, error) {
|
||||||
|
resp, err := c.Get(reqURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) doPost(reqURL string, files ...content) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
buf = new(bytes.Buffer)
|
||||||
|
w = multipart.NewWriter(buf)
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
part, err := w.CreateFormFile(f.ftype, filepath.Base(f.fname))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
part.Write(f.fdata)
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, reqURL, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", w.FormDataContentType())
|
||||||
|
|
||||||
|
res, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) doPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
|
||||||
|
var form = make(url.Values)
|
||||||
|
|
||||||
|
for k, v := range keyVals {
|
||||||
|
form.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, reqURL, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.PostForm = form
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
res, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
|
||||||
|
var cnt []content
|
||||||
|
|
||||||
|
if file.id != "" {
|
||||||
|
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.id)
|
||||||
|
} else if file.url != "" {
|
||||||
|
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.url)
|
||||||
|
} else if c, e := toContent(fileType, file); e == nil {
|
||||||
|
cnt = append(cnt, c)
|
||||||
|
} else {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, e := toContent("thumbnail", thumbnail); e == nil {
|
||||||
|
cnt = append(cnt, c)
|
||||||
|
} else {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cnt) > 0 {
|
||||||
|
res, err = c.doPost(url, cnt...)
|
||||||
|
} else {
|
||||||
|
res, err = c.doGet(url)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) get(base, endpoint string, vals url.Values, v APIResponse) error {
|
||||||
|
url, err := url.JoinPath(base, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if vals != nil {
|
||||||
|
if queries := vals.Encode(); queries != "" {
|
||||||
|
url = fmt.Sprintf("%s?%s", url, queries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt, err := c.doGet(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(cnt, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return check(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) postFile(base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values, v APIResponse) error {
|
||||||
|
url, err := joinURL(base, endpoint, vals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt, err := c.sendFile(file, thumbnail, url, fileType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(cnt, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return check(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) postMedia(base, endpoint string, editSingle bool, vals url.Values, v APIResponse, files ...InputMedia) error {
|
||||||
|
url, err := joinURL(base, endpoint, vals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt, err := c.sendMediaFiles(url, editSingle, files...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(cnt, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return check(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) postStickers(base, endpoint string, vals url.Values, v APIResponse, stickers ...InputSticker) error {
|
||||||
|
url, err := joinURL(base, endpoint, vals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt, err := c.sendStickers(url, stickers...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(cnt, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return check(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) postProfilePhoto(base, endpoint string, vals url.Values, v APIResponse, profilePhoto InputProfilePhoto) error {
|
||||||
|
url, err := joinURL(base, endpoint, vals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.wait(""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt, err := c.sendProfilePhoto(url, profilePhoto)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(cnt, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return check(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
|
||||||
|
var (
|
||||||
|
med []mediaEnvelope
|
||||||
|
cnt []content
|
||||||
|
jsn []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
var im mediaEnvelope
|
||||||
|
var cntArr []content
|
||||||
|
|
||||||
|
media := file.media()
|
||||||
|
thumbnail := file.thumbnail()
|
||||||
|
|
||||||
|
im, cntArr, err = processMedia(media, thumbnail)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
im.InputMedia = file
|
||||||
|
|
||||||
|
med = append(med, im)
|
||||||
|
cnt = append(cnt, cntArr...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if editSingle {
|
||||||
|
jsn, err = json.Marshal(med[0])
|
||||||
|
} else {
|
||||||
|
jsn, err = json.Marshal(med)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url = fmt.Sprintf("%s&media=%s", url, jsn)
|
||||||
|
|
||||||
|
if len(cnt) > 0 {
|
||||||
|
return c.doPost(url, cnt...)
|
||||||
|
}
|
||||||
|
return c.doGet(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
|
||||||
|
var (
|
||||||
|
sti []stickerEnvelope
|
||||||
|
cnt []content
|
||||||
|
jsn []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, s := range stickers {
|
||||||
|
var se stickerEnvelope
|
||||||
|
var cntArr []content
|
||||||
|
|
||||||
|
se, cntArr, err = processSticker(s.Sticker)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
se.InputSticker = s
|
||||||
|
|
||||||
|
sti = append(sti, se)
|
||||||
|
cnt = append(cnt, cntArr...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sti) == 1 {
|
||||||
|
jsn, _ = json.Marshal(sti[0])
|
||||||
|
url = fmt.Sprintf("%s&sticker=%s", url, jsn)
|
||||||
|
} else {
|
||||||
|
jsn, _ = json.Marshal(sti)
|
||||||
|
url = fmt.Sprintf("%s&stickers=%s", url, jsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cnt) > 0 {
|
||||||
|
return c.doPost(url, cnt...)
|
||||||
|
}
|
||||||
|
return c.doGet(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c lclient) sendProfilePhoto(url string, profilePhoto InputProfilePhoto) (res []byte, err error) {
|
||||||
|
var (
|
||||||
|
pp profilePhotoEnvelope
|
||||||
|
cnt []content
|
||||||
|
jsn []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
file := profilePhoto.file()
|
||||||
|
|
||||||
|
switch file := file; {
|
||||||
|
case file.id != "":
|
||||||
|
pp.ProfilePhoto = file.id
|
||||||
|
pp.Animation = file.id
|
||||||
|
|
||||||
|
case file.url != "":
|
||||||
|
pp.ProfilePhoto = file.url
|
||||||
|
pp.Animation = file.url
|
||||||
|
|
||||||
|
case file.path != "" && len(file.content) == 0:
|
||||||
|
if file.content, file.path, err = readFile(file); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case file.path != "" && len(file.content) > 0:
|
||||||
|
cnt = append(cnt, content{file.path, file.path, file.content})
|
||||||
|
pp.ProfilePhoto = fmt.Sprintf("attach://%s", file.path)
|
||||||
|
pp.Animation = fmt.Sprintf("attach://%s", file.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch profilePhoto.(type) {
|
||||||
|
case InputProfilePhotoStatic:
|
||||||
|
pp.Animation = ""
|
||||||
|
case InputProfilePhotoAnimated:
|
||||||
|
pp.ProfilePhoto = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
pp.InputProfilePhoto = profilePhoto
|
||||||
|
jsn, err = json.Marshal(pp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := "?"
|
||||||
|
if strings.Contains(url, "?") {
|
||||||
|
sep = "&"
|
||||||
|
}
|
||||||
|
url = fmt.Sprintf("%s%sprofile_photo=%s", url, sep, jsn)
|
||||||
|
|
||||||
|
if len(cnt) > 0 {
|
||||||
|
return c.doPost(url, cnt...)
|
||||||
|
}
|
||||||
|
return c.doGet(url)
|
||||||
|
}
|
||||||
@@ -0,0 +1,861 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
// ParseMode is a custom type for the various frequent options used by some methods of the API.
|
||||||
|
type ParseMode string
|
||||||
|
|
||||||
|
// These are all the possible options that can be used by some methods.
|
||||||
|
const (
|
||||||
|
Markdown ParseMode = "Markdown"
|
||||||
|
MarkdownV2 = "MarkdownV2"
|
||||||
|
HTML = "HTML"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PollType is a custom type for the various types of poll that can be sent.
|
||||||
|
type PollType string
|
||||||
|
|
||||||
|
// These are all the possible poll types.
|
||||||
|
const (
|
||||||
|
Quiz PollType = "quiz"
|
||||||
|
Regular = "regular"
|
||||||
|
Any = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// DiceEmoji is a custom type for the various emojis that can be sent through the SendDice method.
|
||||||
|
type DiceEmoji string
|
||||||
|
|
||||||
|
// These are all the possible emojis that can be sent through the SendDice method.
|
||||||
|
const (
|
||||||
|
Die DiceEmoji = "🎲"
|
||||||
|
Darts = "🎯"
|
||||||
|
Basket = "🏀"
|
||||||
|
Goal = "⚽️"
|
||||||
|
Bowling = "🎳"
|
||||||
|
Slot = "🎰"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChatAction is a custom type for the various actions that can be sent through the SendChatAction method.
|
||||||
|
type ChatAction string
|
||||||
|
|
||||||
|
// These are all the possible actions that can be sent through the SendChatAction method.
|
||||||
|
const (
|
||||||
|
Typing ChatAction = "typing"
|
||||||
|
UploadPhoto = "upload_photo"
|
||||||
|
RecordVideo = "record_video"
|
||||||
|
UploadVideo = "upload_video"
|
||||||
|
RecordAudio = "record_audio"
|
||||||
|
UploadAudio = "upload_audio"
|
||||||
|
UploadDocument = "upload_document"
|
||||||
|
FindLocation = "find_location"
|
||||||
|
RecordVideoNote = "record_video_note"
|
||||||
|
UploadVideoNote = "upload_video_note"
|
||||||
|
ChooseSticker = "choose_sticker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageEntityType is a custom type for the various MessageEntity types used in various methods.
|
||||||
|
type MessageEntityType string
|
||||||
|
|
||||||
|
// These are all the possible types for MessageEntityType.
|
||||||
|
const (
|
||||||
|
MentionEntity MessageEntityType = "mention"
|
||||||
|
HashtagEntity = "hashtag"
|
||||||
|
CashtagEntity = "cashtag"
|
||||||
|
BotCommandEntity = "bot_command"
|
||||||
|
UrlEntity = "url"
|
||||||
|
EmailEntity = "email"
|
||||||
|
PhoneNumberEntity = "phone_number"
|
||||||
|
BoldEntity = "bold"
|
||||||
|
ItalicEntity = "italic"
|
||||||
|
UnderlineEntity = "underline"
|
||||||
|
StrikethroughEntity = "strikethrough"
|
||||||
|
SpoilerEntity = "spoiler"
|
||||||
|
BlockQuoteEntity = "blockquote"
|
||||||
|
ExpandableBlockQuoteEntity = "expandable_blockquote"
|
||||||
|
CodeEntity = "code"
|
||||||
|
PreEntity = "pre"
|
||||||
|
TextLinkEntity = "text_link"
|
||||||
|
TextMentionEntity = "text_mention"
|
||||||
|
CustomEmojiEntity = "custom_emoji"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateType is a custom type for the various update types that a bot can be subscribed to.
|
||||||
|
type UpdateType string
|
||||||
|
|
||||||
|
// These are all the possible types that a bot can be subscribed to.
|
||||||
|
const (
|
||||||
|
MessageUpdate UpdateType = "message"
|
||||||
|
EditedMessageUpdate = "edited_message"
|
||||||
|
ChannelPostUpdate = "channel_post"
|
||||||
|
EditedChannelPostUpdate = "edited_channel_post"
|
||||||
|
InlineQueryUpdate = "inline_query"
|
||||||
|
ChosenInlineResultUpdate = "chosen_inline_result"
|
||||||
|
CallbackQueryUpdate = "callback_query"
|
||||||
|
ShippingQueryUpdate = "shipping_query"
|
||||||
|
PreCheckoutQueryUpdate = "pre_checkout_query"
|
||||||
|
PollUpdate = "poll"
|
||||||
|
PollAnswerUpdate = "poll_answer"
|
||||||
|
MyChatMemberUpdate = "my_chat_member"
|
||||||
|
ChatMemberUpdate = "chat_member"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplyMarkup is an interface for the various keyboard types.
|
||||||
|
type ReplyMarkup interface {
|
||||||
|
ImplementsReplyMarkup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardButton represents a button in a keyboard.
|
||||||
|
type KeyboardButton struct {
|
||||||
|
RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"`
|
||||||
|
WebApp *WebAppInfo `json:"web_app,omitempty"`
|
||||||
|
RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"`
|
||||||
|
RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"`
|
||||||
|
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
|
||||||
|
Style ButtonStyle `json:"style,omitempty"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
RequestContact bool `json:"request_contact,omitempty"`
|
||||||
|
RequestLocation bool `json:"request_location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ButtonStyle represents supported button style values.
|
||||||
|
type ButtonStyle string
|
||||||
|
|
||||||
|
// These are all the possible values for ButtonStyle.
|
||||||
|
const (
|
||||||
|
PrimaryButtonStyle ButtonStyle = "primary"
|
||||||
|
DangerButtonStyle = "danger"
|
||||||
|
SuccessButtonStyle = "success"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyboardButtonPollType represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed.
|
||||||
|
type KeyboardButtonPollType struct {
|
||||||
|
Type PollType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardButtonRequestUsers defines the criteria used to request suitable users.
|
||||||
|
// The identifiers of the selected users will be shared with the bot when the corresponding button is pressed.
|
||||||
|
type KeyboardButtonRequestUsers struct {
|
||||||
|
RequestID int `json:"request_id"`
|
||||||
|
MaxQuantity int `json:"max_quantity,omitempty"`
|
||||||
|
UserIsBot bool `json:"user_is_bot,omitempty"`
|
||||||
|
UserIsPremium bool `json:"user_is_premium,omitempty"`
|
||||||
|
RequestName bool `json:"request_name,omitempty"`
|
||||||
|
RequestUsername bool `json:"request_username,omitempty"`
|
||||||
|
RequestPhoto bool `json:"request_photo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardButtonRequestChat defines the criteria used to request a suitable chat.
|
||||||
|
// The identifier of the selected chat will be shared with the bot when the corresponding button is pressed.
|
||||||
|
type KeyboardButtonRequestChat struct {
|
||||||
|
UserAdministratorRights *ChatAdministratorRights `json:"user_administrator_rights,omitempty"`
|
||||||
|
BotAdministratorRights *ChatAdministratorRights `json:"bot_administrator_rights,omitempty"`
|
||||||
|
RequestID int `json:"request_id"`
|
||||||
|
ChatIsChannel bool `json:"chat_is_channel,omitempty"`
|
||||||
|
ChatIsForum bool `json:"chat_is_forum,omitempty"`
|
||||||
|
ChatHasUsername bool `json:"chat_has_username,omitempty"`
|
||||||
|
ChatIsCreated bool `json:"chat_is_created,omitempty"`
|
||||||
|
BotIsMember bool `json:"bot_is_member,omitempty"`
|
||||||
|
RequestName bool `json:"request_name,omitempty"`
|
||||||
|
RequestUsername bool `json:"request_username,omitempty"`
|
||||||
|
RequestPhoto bool `json:"request_photo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyKeyboardMarkup represents a custom keyboard with reply options.
|
||||||
|
type ReplyKeyboardMarkup struct {
|
||||||
|
InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"`
|
||||||
|
Keyboard [][]KeyboardButton `json:"keyboard"`
|
||||||
|
IsPersistent bool `json:"is_persistent,omitempty"`
|
||||||
|
ResizeKeyboard bool `json:"resize_keyboard,omitempty"`
|
||||||
|
OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"`
|
||||||
|
Selective bool `json:"selective,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
|
||||||
|
func (i ReplyKeyboardMarkup) ImplementsReplyMarkup() {}
|
||||||
|
|
||||||
|
// ReplyKeyboardRemove is used to remove the current custom keyboard and display the default letter-keyboard.
|
||||||
|
// By default, custom keyboards are displayed until a new keyboard is sent by a bot.
|
||||||
|
// An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see ReplyKeyboardMarkup).
|
||||||
|
// RemoveKeyboard MUST BE true.
|
||||||
|
type ReplyKeyboardRemove struct {
|
||||||
|
RemoveKeyboard bool `json:"remove_keyboard"`
|
||||||
|
Selective bool `json:"selective"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
|
||||||
|
func (r ReplyKeyboardRemove) ImplementsReplyMarkup() {}
|
||||||
|
|
||||||
|
// InlineKeyboardButton represents a button in an inline keyboard.
|
||||||
|
type InlineKeyboardButton struct {
|
||||||
|
CopyText *CopyTextButton `json:"copy_text,omitempty"`
|
||||||
|
CallbackGame *CallbackGame `json:"callback_game,omitempty"`
|
||||||
|
WebApp *WebAppInfo `json:"web_app,omitempty"`
|
||||||
|
LoginURL *LoginURL `json:"login_url,omitempty"`
|
||||||
|
SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"`
|
||||||
|
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
|
||||||
|
Style ButtonStyle `json:"style,omitempty"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
CallbackData string `json:"callback_data,omitempty"`
|
||||||
|
SwitchInlineQuery string `json:"switch_inline_query,omitempty"`
|
||||||
|
SwitchInlineQueryCurrentChat string `json:"switch_inline_query_current_chat,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Pay bool `json:"pay,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyTextButton represents an inline keyboard button that copies specified text to the clipboard.
|
||||||
|
type CopyTextButton struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlineKeyboardMarkup represents an inline keyboard.
|
||||||
|
type InlineKeyboardMarkup struct {
|
||||||
|
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard" query:"inline_keyboard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
|
||||||
|
func (i InlineKeyboardMarkup) ImplementsReplyMarkup() {}
|
||||||
|
|
||||||
|
// ForceReply is used to display a reply interface to the user (act as if the user has selected the bot's message and tapped 'Reply').
|
||||||
|
// This can be extremely useful if you want to create user-friendly step-by-step interfaces without having to sacrifice privacy mode.
|
||||||
|
type ForceReply struct {
|
||||||
|
InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"`
|
||||||
|
ForceReply bool `json:"force_reply"`
|
||||||
|
Selective bool `json:"selective"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
|
||||||
|
func (f ForceReply) ImplementsReplyMarkup() {}
|
||||||
|
|
||||||
|
// UpdateOptions contains the optional parameters used by the GetUpdates method.
|
||||||
|
type UpdateOptions struct {
|
||||||
|
AllowedUpdates []UpdateType `query:"allowed_updates"`
|
||||||
|
Offset int `query:"offset"`
|
||||||
|
Limit int `query:"limit"`
|
||||||
|
Timeout int `query:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookOptions contains the optional parameters used by the SetWebhook method.
|
||||||
|
type WebhookOptions struct {
|
||||||
|
IPAddress string `query:"ip_address"`
|
||||||
|
SecretToken string `query:"secret_token"`
|
||||||
|
Certificate InputFile
|
||||||
|
AllowedUpdates []UpdateType `query:"allowed_updates"`
|
||||||
|
MaxConnections int `query:"max_connections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseOptions contains the optional parameters used frequently in some Telegram API methods.
|
||||||
|
type BaseOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageOptions contains the optional parameters used by some Telegram API methods.
|
||||||
|
type MessageOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
|
||||||
|
Entities []MessageEntity `query:"entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int64 `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PinMessageOptions contains the optional parameters used by the PinChatMember method.
|
||||||
|
type PinMessageOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpinMessageOptions contains the optional parameters used by the UnpinChatMember method.
|
||||||
|
type UnpinMessageOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageID int `query:"message_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardOptions contains the optional parameters used by the ForwardMessage method.
|
||||||
|
type ForwardOptions struct {
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
VideoStartTimestamp int `query:"video_start_timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyOptions contains the optional parameters used by the CopyMessage method.
|
||||||
|
type CopyOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyMessagesOptions contains the optional parameters used by the CopyMessages methods.
|
||||||
|
type CopyMessagesOptions struct {
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
RemoveCaption bool `query:"remove_caption"`
|
||||||
|
VideoStartTimestamp int `query:"video_start_timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StickerOptions contains the optional parameters used by the SendSticker method.
|
||||||
|
type StickerOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
Emoji string `query:"emoji"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputFile is a struct which contains data about a file to be sent.
|
||||||
|
type InputFile struct {
|
||||||
|
id string
|
||||||
|
path string
|
||||||
|
url string
|
||||||
|
content []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInputFileID is a wrapper for InputFile which only fills the id field.
|
||||||
|
func NewInputFileID(ID string) InputFile {
|
||||||
|
return InputFile{id: ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInputFilePath is a wrapper for InputFile which only fills the path field.
|
||||||
|
func NewInputFilePath(filePath string) InputFile {
|
||||||
|
return InputFile{path: filePath}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInputFileURL is a wrapper for InputFile which only fills the url field.
|
||||||
|
func NewInputFileURL(url string) InputFile {
|
||||||
|
return InputFile{url: url}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInputFileBytes is a wrapper for InputFile which only fills the path and content fields.
|
||||||
|
func NewInputFileBytes(fileName string, content []byte) InputFile {
|
||||||
|
return InputFile{path: fileName, content: content}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PhotoOptions contains the optional parameters used by the SendPhoto method.
|
||||||
|
type PhotoOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
HasSpoiler bool `query:"has_spoiler"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioOptions contains the optional parameters used by the SendAudio method.
|
||||||
|
type AudioOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
Title string `query:"title"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
Performer string `query:"performer"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
Thumbnail InputFile
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
Duration int `query:"duration"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentOptions contains the optional parameters used by the SendDocument method.
|
||||||
|
type DocumentOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
Thumbnail InputFile
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
DisableContentTypeDetection bool `query:"disable_content_type_detection"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoOptions contains the optional parameters used by the SendVideo method.
|
||||||
|
// TODO: handle the cover correctly.
|
||||||
|
type VideoOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
Thumbnail InputFile
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
Duration int `query:"duration"`
|
||||||
|
Width int `query:"width"`
|
||||||
|
Height int `query:"height"`
|
||||||
|
HasSpoiler bool `query:"has_spoiler"`
|
||||||
|
SupportsStreaming bool `query:"supports_streaming"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
Cover InputFile
|
||||||
|
StartTimestamp int `query:"start_timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnimationOptions contains the optional parameters used by the SendAnimation method.
|
||||||
|
type AnimationOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
Thumbnail InputFile
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
Duration int `query:"duration"`
|
||||||
|
Width int `query:"width"`
|
||||||
|
Height int `query:"height"`
|
||||||
|
HasSpoiler bool `query:"has_spoiler"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoiceOptions contains the optional parameters used by the SendVoice method.
|
||||||
|
type VoiceOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
Duration int `query:"duration"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoNoteOptions contains the optional parameters used by the SendVideoNote method.
|
||||||
|
type VideoNoteOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
Thumbnail InputFile
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
Duration int `query:"duration"`
|
||||||
|
Length int `query:"length"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaidMediaOptions contains the optional parameters used by the SendPaidMedia method.
|
||||||
|
type PaidMediaOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
Payload string `query:"payload"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaGroupOptions contains the optional parameters used by the SendMediaGroup method.
|
||||||
|
type MediaGroupOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a custom constant to set an infinite live period value in LocationOptions and EditLocationOptions.
|
||||||
|
const InfiniteLivePeriod = 0x7FFFFFFF
|
||||||
|
|
||||||
|
// LocationOptions contains the optional parameters used by the SendLocation method.
|
||||||
|
type LocationOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
HorizontalAccuracy float64 `query:"horizontal_accuracy"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
LivePeriod int `query:"live_period"`
|
||||||
|
ProximityAlertRadius int `query:"proximity_alert_radius"`
|
||||||
|
Heading int `query:"heading"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditLocationOptions contains the optional parameters used by the EditMessageLiveLocation method.
|
||||||
|
type EditLocationOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||||
|
HorizontalAccuracy float64 `query:"horizontal_accuracy"`
|
||||||
|
Heading int `query:"heading"`
|
||||||
|
LivePeriod int `query:"live_period"`
|
||||||
|
ProximityAlertRadius int `query:"proximity_alert_radius"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopLocationOptions contains the optional parameters used by the StopMessageLiveLocation method.
|
||||||
|
type StopLocationOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VenueOptions contains the optional parameters used by the SendVenue method.
|
||||||
|
type VenueOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
FoursquareID string `query:"foursquare_id"`
|
||||||
|
FoursquareType string `query:"foursquare_type"`
|
||||||
|
GooglePlaceType string `query:"google_place_type"`
|
||||||
|
GooglePlaceID string `query:"google_place_id"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactOptions contains the optional parameters used by the SendContact method.
|
||||||
|
type ContactOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
VCard string `query:"vcard"`
|
||||||
|
LastName string `query:"last_name"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollOptions contains the optional parameters used by the SendPoll method.
|
||||||
|
type PollOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
Explanation string `query:"explanation"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ExplanationParseMode ParseMode `query:"explanation_parse_mode"`
|
||||||
|
QuestionParseMode ParseMode `query:"question_parse_mode"`
|
||||||
|
Type PollType `query:"type"`
|
||||||
|
ExplanationEntities []MessageEntity `query:"explanation_entities"`
|
||||||
|
QuestionEntities []MessageEntity `query:"question_entities"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
CorrectOptionID int `query:"correct_option_id"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
CloseDate int `query:"close_date"`
|
||||||
|
OpenPeriod int `query:"open_period"`
|
||||||
|
IsClosed bool `query:"is_closed"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
AllowsMultipleAnswers bool `query:"allows_multiple_answers"`
|
||||||
|
IsAnonymous bool `query:"is_anonymous"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopPollOptions contains the optional parameters used by the StopPoll method.
|
||||||
|
type StopPollOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BanOptions contains the optional parameters used by the BanChatMember method.
|
||||||
|
type BanOptions struct {
|
||||||
|
UntilDate int `query:"until_date"`
|
||||||
|
RevokeMessages bool `query:"revoke_messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnbanOptions contains the optional parameters used by the UnbanChatMember method.
|
||||||
|
type UnbanOptions struct {
|
||||||
|
OnlyIfBanned bool `query:"only_if_banned"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestrictOptions contains the optional parameters used by the RestrictChatMember method.
|
||||||
|
type RestrictOptions struct {
|
||||||
|
UseIndependentChatPermissions bool `query:"use_independent_chat_permissions"`
|
||||||
|
UntilDate int `query:"until_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromoteOptions contains the optional parameters used by the PromoteChatMember method.
|
||||||
|
type PromoteOptions struct {
|
||||||
|
IsAnonymous bool `query:"is_anonymous,omitempty"`
|
||||||
|
CanManageChat bool `query:"can_manage_chat,omitempty"`
|
||||||
|
CanPostMessages bool `query:"can_post_messages,omitempty"`
|
||||||
|
CanEditMessages bool `query:"can_edit_messages,omitempty"`
|
||||||
|
CanDeleteMessages bool `query:"can_delete_messages,omitempty"`
|
||||||
|
CanManageVideoChats bool `query:"can_manage_video_chats,omitempty"`
|
||||||
|
CanRestrictMembers bool `query:"can_restrict_members,omitempty"`
|
||||||
|
CanPromoteMembers bool `query:"can_promote_members,omitempty"`
|
||||||
|
CanChangeInfo bool `query:"can_change_info,omitempty"`
|
||||||
|
CanInviteUsers bool `query:"can_invite_users,omitempty"`
|
||||||
|
CanPinMessages bool `query:"can_pin_messages,omitempty"`
|
||||||
|
CanPostStories bool `json:"can_post_stories,omitempty"`
|
||||||
|
CanEditStories bool `json:"can_edit_stories,omitempty"`
|
||||||
|
CanDeleteStories bool `json:"can_delete_stories,omitempty"`
|
||||||
|
CanManageTopics bool `query:"can_manage_topics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfileOptions contains the optional parameters used by the GetUserProfilePhotos method.
|
||||||
|
type UserProfileOptions struct {
|
||||||
|
Offset int `query:"offset"`
|
||||||
|
Limit int `query:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfileAudioOptions contains the optional parameters used by the GetUserProfileAudios method.
|
||||||
|
type UserProfileAudioOptions struct {
|
||||||
|
Offset int `query:"offset"`
|
||||||
|
Limit int `query:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserEmojiStatusOptions contains the optional parameters used by the SetUserEmojiStatus method.
|
||||||
|
type UserEmojiStatusOptions struct {
|
||||||
|
EmojiStatusCustomEmojiID string `query:"emoji_status_custom_emoji_id"`
|
||||||
|
EmojiStatusExpirationDate string `query:"emoji_status_expiration_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatPermissionsOptions contains the optional parameters used by the SetChatPermissions method.
|
||||||
|
type ChatPermissionsOptions struct {
|
||||||
|
UseIndependentChatPermissions bool `query:"use_independent_chat_permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteLinkOptions contains the optional parameters used by the CreateChatInviteLink and EditChatInviteLink methods.
|
||||||
|
type InviteLinkOptions struct {
|
||||||
|
Name string `query:"name"`
|
||||||
|
ExpireDate int64 `query:"expire_date"`
|
||||||
|
MemberLimit int `query:"member_limit"`
|
||||||
|
CreatesJoinRequest bool `query:"creates_join_request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatSubscriptionInviteOptions contains the optional parameters used by the CreateChatSubscriptionInviteLink and EditChatSubscriptionInviteLink methods.
|
||||||
|
type ChatSubscriptionInviteOptions struct {
|
||||||
|
Name string `query:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallbackQueryOptions contains the optional parameters used by the AnswerCallbackQuery method.
|
||||||
|
type CallbackQueryOptions struct {
|
||||||
|
Text string `query:"text"`
|
||||||
|
URL string `query:"url"`
|
||||||
|
CacheTime int `query:"cache_time"`
|
||||||
|
ShowAlert bool `query:"show_alert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageIDOptions is a struct which contains data about a message to edit.
|
||||||
|
type MessageIDOptions struct {
|
||||||
|
inlineMessageID string `query:"inline_message_id"`
|
||||||
|
chatID int64 `query:"chat_id"`
|
||||||
|
messageID int `query:"message_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessageID is a wrapper for MessageIDOptions which only fills the chatID and messageID fields.
|
||||||
|
func NewMessageID(chatID int64, messageID int) MessageIDOptions {
|
||||||
|
return MessageIDOptions{chatID: chatID, messageID: messageID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInlineMessageID is a wrapper for MessageIDOptions which only fills the inlineMessageID fields.
|
||||||
|
func NewInlineMessageID(ID string) MessageIDOptions {
|
||||||
|
return MessageIDOptions{inlineMessageID: ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageTextOptions contains the optional parameters used by the EditMessageText method.
|
||||||
|
type MessageTextOptions struct {
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
Entities []MessageEntity `query:"entities"`
|
||||||
|
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||||
|
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageCaptionOptions contains the optional parameters used by the EditMessageCaption method.
|
||||||
|
type MessageCaptionOptions struct {
|
||||||
|
Caption string `query:"caption"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
ParseMode ParseMode `query:"parse_mode"`
|
||||||
|
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||||
|
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||||
|
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageMediaOptions contains the optional parameters used by the EditMessageMedia method.
|
||||||
|
type MessageMediaOptions struct {
|
||||||
|
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageReplyMarkupOptions contains the optional parameters used by the EditMessageReplyMarkup method.
|
||||||
|
type MessageReplyMarkupOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandOptions contains the optional parameters used by the SetMyCommands, DeleteMyCommands and GetMyCommands methods.
|
||||||
|
type CommandOptions struct {
|
||||||
|
LanguageCode string `query:"language_code"`
|
||||||
|
Scope BotCommandScope `query:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvoiceOptions contains the optional parameters used by the SendInvoice API method.
|
||||||
|
type InvoiceOptions struct {
|
||||||
|
StartParameter string `query:"start_parameter"`
|
||||||
|
ProviderData string `query:"provider_data"`
|
||||||
|
PhotoURL string `query:"photo_url"`
|
||||||
|
ProviderToken string `query:"provider_token"`
|
||||||
|
MessageEffectID string `query:"message_effect_id"`
|
||||||
|
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||||
|
SuggestedTipAmount []int `query:"suggested_tip_amounts"`
|
||||||
|
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||||
|
MaxTipAmount int `query:"max_tip_amount"`
|
||||||
|
PhotoSize int `query:"photo_size"`
|
||||||
|
PhotoWidth int `query:"photo_width"`
|
||||||
|
PhotoHeight int `query:"photo_height"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
SendPhoneNumberToProvider bool `query:"send_phone_number_to_provider"`
|
||||||
|
NeedShippingAddress bool `query:"need_shipping_address"`
|
||||||
|
NeedPhoneNumber bool `query:"need_phone_number"`
|
||||||
|
SendEmailToProvider bool `query:"send_email_to_provider"`
|
||||||
|
IsFlexible bool `query:"is_flexible"`
|
||||||
|
DisableNotification bool `query:"disable_notification"`
|
||||||
|
ProtectContent bool `query:"protect_content"`
|
||||||
|
NeedName bool `query:"need_name"`
|
||||||
|
NeedEmail bool `query:"need_email"`
|
||||||
|
AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInvoiceLinkOptions contains the optional parameters used by the CreateInvoiceLink API method.
|
||||||
|
// Currently, SubscriptionPeriod should always be set to 2592000 (30 days) if specified.
|
||||||
|
type CreateInvoiceLinkOptions struct {
|
||||||
|
ProviderData string `query:"provider_data"`
|
||||||
|
PhotoURL string `query:"photo_url"`
|
||||||
|
ProviderToken string `query:"provider_token"`
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
SuggestedTipAmounts []int `query:"suggested_tip_amounts"`
|
||||||
|
PhotoSize int `query:"photo_size"`
|
||||||
|
PhotoWidth int `query:"photo_width"`
|
||||||
|
PhotoHeight int `query:"photo_height"`
|
||||||
|
MaxTipAmount int `query:"max_tip_amount"`
|
||||||
|
SubscriptionPeriod int `query:"subscription_period"`
|
||||||
|
NeedPhoneNumber bool `query:"need_phone_number"`
|
||||||
|
NeepShippingAddress bool `query:"need_shipping_address"`
|
||||||
|
SendPhoneNumberToProvider bool `query:"send_phone_number_to_provider"`
|
||||||
|
SendEmailToProvider bool `query:"send_email_to_provider"`
|
||||||
|
IsFlexible bool `query:"is_flexible"`
|
||||||
|
NeedName bool `query:"need_name"`
|
||||||
|
NeedEmail bool `query:"need_email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShippingOption represents one shipping option.
|
||||||
|
type ShippingOption struct {
|
||||||
|
ID string `query:"id"`
|
||||||
|
Title string `query:"title"`
|
||||||
|
Prices []LabeledPrice `query:"prices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShippingQueryOptions contains the optional parameters used by the AnswerShippingQuery API method.
|
||||||
|
type ShippingQueryOptions struct {
|
||||||
|
ErrorMessage string `query:"error_message"`
|
||||||
|
ShippingOptions []ShippingOption `query:"shipping_options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreCheckoutOptions contains the optional parameters used by the AnswerPreCheckoutQuery API method.
|
||||||
|
type PreCheckoutOptions struct {
|
||||||
|
ErrorMessage string `query:"error_message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTopicOptions contains the optional parameters used by the CreateForumTopic API method.
|
||||||
|
type CreateTopicOptions struct {
|
||||||
|
IconCustomEmojiID string `query:"icon_custom_emoji_id"`
|
||||||
|
IconColor IconColor `query:"icon_color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditTopicOptions contains the optional parameters used by the EditForumTopic API method.
|
||||||
|
type EditTopicOptions struct {
|
||||||
|
Name string `query:"name"`
|
||||||
|
IconCustomEmojiID string `query:"icon_custom_emoji_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatActionOptions contains the optional parameters used by the SendChatAction API method.
|
||||||
|
type ChatActionOptions struct {
|
||||||
|
BusinessConnectionID string `query:"business_connection_id"`
|
||||||
|
MessageThreadID int `query:"message_thread_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageReactionOptions contains the optional parameters used by the SetMessageReaction API method.
|
||||||
|
type MessageReactionOptions struct {
|
||||||
|
Reaction []ReactionType `query:"reaction"`
|
||||||
|
IsBig bool `query:"is_big"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GiftOptions contains the optional parameters used by the SendGift API method.
|
||||||
|
type GiftOptions struct {
|
||||||
|
Text string `query:"text"`
|
||||||
|
TextParseMode string `query:"text_parse_mode"`
|
||||||
|
TextEntities []MessageEntity `query:"text_entities"`
|
||||||
|
ChatID int64 `query:"chat_id"`
|
||||||
|
PayForUpgrade bool `query:"pay_for_upgrade"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyOptions contains the optional parameters used by the VerifyUser and VerifyChat API methods.
|
||||||
|
type VerifyOptions struct {
|
||||||
|
CustomDescription string `query:"custom_description"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
msgIDOpts = MessageIDOptions{
|
||||||
|
chatID: 1,
|
||||||
|
messageID: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineMsgIDOpts = MessageIDOptions{
|
||||||
|
inlineMessageID: "inline",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewMessageID(t *testing.T) {
|
||||||
|
new := NewMessageID(1, 2)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(new, msgIDOpts) {
|
||||||
|
t.Logf("expected MessageIDOptions: %+v", msgIDOpts)
|
||||||
|
t.Logf("got MessageIDOptions: %+v", new)
|
||||||
|
t.Fatal("error: MessageIDOptions mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewInlineMessageID(t *testing.T) {
|
||||||
|
new := NewInlineMessageID("inline")
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(new, inlineMsgIDOpts) {
|
||||||
|
t.Logf("expected MessageIDOptions: %+v", inlineMsgIDOpts)
|
||||||
|
t.Logf("got MessageIDOptions: %+v", new)
|
||||||
|
t.Fatal("error: MessageIDOptions mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplyKeyboardMarkupImplementsReplyMarkup(_ *testing.T) {
|
||||||
|
i := ReplyKeyboardMarkup{}
|
||||||
|
i.ImplementsReplyMarkup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplyKeyboardRemoveImplementsReplyMarkup(_ *testing.T) {
|
||||||
|
i := ReplyKeyboardRemove{}
|
||||||
|
i.ImplementsReplyMarkup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineKeyboardMarkupImplementsReplyMarkup(_ *testing.T) {
|
||||||
|
i := InlineKeyboardMarkup{}
|
||||||
|
i.ImplementsReplyMarkup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForceReplyImplementsReplyMarkup(_ *testing.T) {
|
||||||
|
i := ForceReply{}
|
||||||
|
i.ImplementsReplyMarkup()
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PassportData contains information about Telegram Passport data shared with the bot by the user.
|
||||||
|
type PassportData struct {
|
||||||
|
Credentials EncryptedCredentials `json:"encrypted_credentials"`
|
||||||
|
Data []EncryptedPassportElement `json:"encrypted_passport_element"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassportFile represents a file uploaded to Telegram Passport.
|
||||||
|
// Currently all Telegram Passport files are in JPEG format when decrypted and don't exceed 10MB.
|
||||||
|
type PassportFile struct {
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
FileUniqueID string `json:"file_unique_id"`
|
||||||
|
FileSize int64 `json:"file_size"`
|
||||||
|
FileDate int64 `json:"file_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedPassportElementType is a custom type for the various possible options used as Type in EncryptedPassportElement.
|
||||||
|
type EncryptedPassportElementType string
|
||||||
|
|
||||||
|
// These are all the possible options that can be used as Type in EncryptedPassportElement.
|
||||||
|
const (
|
||||||
|
TypePersonalDetails EncryptedPassportElementType = "personal_details"
|
||||||
|
TypePassport = "passport"
|
||||||
|
TypeDriverLicense = "driver_license"
|
||||||
|
TypeIdentityCard = "identity_card"
|
||||||
|
TypeInternalPassport = "internal_passport"
|
||||||
|
TypeAddress = "address"
|
||||||
|
TypeUtilityBill = "utility_bill"
|
||||||
|
TypeBankStatement = "bank_statement"
|
||||||
|
TypeRentalAgreement = "rental_agreement"
|
||||||
|
TypePassportRegistration = "passport_registration"
|
||||||
|
TypeTemporaryRegistration = "temporary_registration"
|
||||||
|
TypePhoneNumber = "phone_number"
|
||||||
|
TypeEmail = "email"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncryptedPassportElement contains information about documents or other Telegram Passport elements shared with the bot by the user.
|
||||||
|
type EncryptedPassportElement struct {
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
Data string `json:"data,omitempty"`
|
||||||
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Files *[]PassportFile `json:"files,omitempty"`
|
||||||
|
FrontSide *PassportFile `json:"front_side,omitempty"`
|
||||||
|
ReverseSide *PassportFile `json:"reverse_side,omitempty"`
|
||||||
|
Selfie *PassportFile `json:"selfie,omitempty"`
|
||||||
|
Translation *[]PassportFile `json:"translation,omitempty"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedCredentials contains data required for decrypting and authenticating EncryptedPassportElement.
|
||||||
|
// See the Telegram Passport Documentation for a complete description of the data decryption and authentication processes.
|
||||||
|
// https://core.telegram.org/passport#receiving-information
|
||||||
|
type EncryptedCredentials struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassportElementErrorSource is a custom type for the various possible options used as Source in PassportElementSource.
|
||||||
|
type PassportElementErrorSource string
|
||||||
|
|
||||||
|
// These are all the possible options that can be used as Source in PassportElementSource.
|
||||||
|
const (
|
||||||
|
SourceData PassportElementErrorSource = "data"
|
||||||
|
SourceFrontSide = "front_side"
|
||||||
|
SourceReverseSide = "reverse_side"
|
||||||
|
SourceSelfie = "selfie"
|
||||||
|
SourceFile = "file"
|
||||||
|
SourceFiles = "files"
|
||||||
|
SourceTranslationFile = "translation_file"
|
||||||
|
SourceTranslationFiles = "translation_files"
|
||||||
|
SourceUnspecified = "unspecified"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PassportElementError is an interface for the various PassportElementError types.
|
||||||
|
type PassportElementError interface {
|
||||||
|
ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassportElementErrorDataField represents an issue in one of the data fields that was provided by the user.
|
||||||
|
// The error is considered resolved when the field's value changes.
|
||||||
|
// Source MUST BE SourceData.
|
||||||
|
// Type MUST BE one of TypePersonalDetails, TypePassport, TypeDriverLicense, TypeIdentityCard, TypeInternalPassport and TypeAddress.
|
||||||
|
type PassportElementErrorDataField struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
FieldName string `json:"field_name"`
|
||||||
|
DataHash string `json:"data_hash"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorDataField) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorFrontSide represents an issue with the front side of a document.
|
||||||
|
// The error is considered resolved when the file with the front side of the document changes.
|
||||||
|
// Source MUST BE SourceFrontSide.
|
||||||
|
// Type MUST BE one of TypeDriverLicense and TypeIdentityCard.
|
||||||
|
type PassportElementErrorFrontSide struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
FileHash string `json:"file_hash"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorFrontSide) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorReverseSide represents an issue with the reverse side of a document.
|
||||||
|
// The error is considered resolved when the file with the reverse side of the document changes.
|
||||||
|
// Source MUST BE SourceReverseSide.
|
||||||
|
// Type MUST BE one of TypeDriverLicense and TypeIdentityCard.
|
||||||
|
type PassportElementErrorReverseSide struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
FileHash string `json:"file_hash"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorReverseSide) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorSelfie represents an issue with the selfie with a document.
|
||||||
|
// The error is considered resolved when the file with the selfie changes.
|
||||||
|
// Source MUST BE SourceSelfie.
|
||||||
|
// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard and TypeIdentityPassport.
|
||||||
|
type PassportElementErrorSelfie struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
FileHash string `json:"file_hash"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorSelfie) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorFile represents an issue with the document scan.
|
||||||
|
// The error is considered resolved when the file with the document scan changes.
|
||||||
|
// Source MUST BE SourceFile.
|
||||||
|
// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard and TypeIdentityPassport.
|
||||||
|
type PassportElementErrorFile struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
FileHash string `json:"file_hash"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorFile) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorFiles represents an issue with a list of scans.
|
||||||
|
// The error is considered resolved when the list of files containing the scans changes.
|
||||||
|
// Source MUST BE SourceFiles.
|
||||||
|
// Type MUST BE one of TypeUtilityBill, TypeBankStatement, TypeRentalAgreement, TypePassportRegistration and TypeTemporaryRegistration.
|
||||||
|
type PassportElementErrorFiles struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
FileHashes []string `json:"file_hashes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorFiles) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorTranslationFile represents an issue with one of the files that constitute the translation of the document.
|
||||||
|
// The error is considered resolved when the file changes.
|
||||||
|
// Source MUST BE SourceTranslationFile.
|
||||||
|
// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard, TypeInternalPassport, TypeUtilityBill, TypeBankStatement,
|
||||||
|
// TypeRentalAgreement, TypePassportRegistration and TypeTemporaryRegistration.
|
||||||
|
type PassportElementErrorTranslationFile struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
FileHash string `json:"file_hash"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorTranslationFile) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorTranslationFiles represents an issue with the translated version of a document.
|
||||||
|
// The error is considered resolved when a file with the document translation changes.
|
||||||
|
// Source MUST BE SourceTranslationFiles.
|
||||||
|
// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard, TypeInternalPassport, TypeUtilityBill, TypeBankStatement,
|
||||||
|
// TypeRentalAgreement, TypePassportRegistration and TypeTemporaryRegistration.
|
||||||
|
type PassportElementErrorTranslationFiles struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
FileHashes []string `json:"file_hashes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorTranslationFiles) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// PassportElementErrorUnspecified represents an issue in an unspecified place.
|
||||||
|
// The error is considered resolved when new data is added.
|
||||||
|
type PassportElementErrorUnspecified struct {
|
||||||
|
Source PassportElementErrorSource `json:"source"`
|
||||||
|
Type EncryptedPassportElementType `json:"type"`
|
||||||
|
ElementHash string `json:"element_hash"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
|
||||||
|
func (p PassportElementErrorUnspecified) ImplementsPassportElementError() {}
|
||||||
|
|
||||||
|
// SetPassportDataErrors Informs a user that some of the Telegram Passport elements they provided contains errors.
|
||||||
|
// The user will not be able to re-submit their Passport to you until the errors are fixed.
|
||||||
|
// The contents of the field for which you returned the error must change.
|
||||||
|
func (a API) SetPassportDataErrors(userID int64, errors []PassportElementError) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
errorsArr, err := json.Marshal(errors)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("errors", string(errorsArr))
|
||||||
|
return res, client.get(a.base, "setPassportDataErrors", vals, &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPassportElementErrorDataField(_ *testing.T) {
|
||||||
|
p := PassportElementErrorDataField{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorFrontSide(_ *testing.T) {
|
||||||
|
p := PassportElementErrorFrontSide{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorReverseSide(_ *testing.T) {
|
||||||
|
p := PassportElementErrorReverseSide{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorSelfie(_ *testing.T) {
|
||||||
|
p := PassportElementErrorSelfie{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorFile(_ *testing.T) {
|
||||||
|
p := PassportElementErrorFile{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorFiles(_ *testing.T) {
|
||||||
|
p := PassportElementErrorFiles{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorTranslationFile(_ *testing.T) {
|
||||||
|
p := PassportElementErrorTranslationFile{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorTranslationFiles(_ *testing.T) {
|
||||||
|
p := PassportElementErrorTranslationFiles{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassportElementErrorUnspecified(_ *testing.T) {
|
||||||
|
p := PassportElementErrorUnspecified{}
|
||||||
|
p.ImplementsPassportElementError()
|
||||||
|
}
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LabeledPrice represents a portion of the price for goods or services.
|
||||||
|
type LabeledPrice struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
// Price of the product in the smallest units of the currency (integer, not float/double).
|
||||||
|
// For example, for a price of US$ 1.45 pass amount = 145.
|
||||||
|
// See the exp parameter in currencies.json, it shows the number of digits
|
||||||
|
// past the decimal point for each currency (2 for the majority of currencies).
|
||||||
|
Amount int `json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoice contains basic information about an invoice.
|
||||||
|
type Invoice struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StartParameter string `json:"start_parameter"`
|
||||||
|
// Three-letter ISO 4217 currency code.
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
// Total amount in the smallest units of the currency (integer, not float/double).
|
||||||
|
// For example, for a price of US$ 1.45 pass amount = 145.
|
||||||
|
// See the exp parameter in currencies.json, it shows the number of digits
|
||||||
|
// past the decimal point for each currency (2 for the majority of currencies).
|
||||||
|
TotalAmount int `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShippingAddress represents a shipping address.
|
||||||
|
type ShippingAddress struct {
|
||||||
|
// ISO 3166-1 alpha-2 country code.
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
State string `json:"state"`
|
||||||
|
City string `json:"city"`
|
||||||
|
StreetLine1 string `json:"street_line1"`
|
||||||
|
StreetLine2 string `json:"street_line2"`
|
||||||
|
PostCode string `json:"post_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderInfo represents information about an order.
|
||||||
|
type OrderInfo struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
ShippingAddress ShippingAddress `json:"shipping_address,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuccessfulPayment contains basic information about a successful payment.
|
||||||
|
type SuccessfulPayment struct {
|
||||||
|
OrderInfo OrderInfo `json:"order_info"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
InvoicePayload string `json:"invoice_payload"`
|
||||||
|
ShippingOptionID string `json:"shipping_option_id"`
|
||||||
|
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
|
||||||
|
ProviderPaymentChargeID string `json:"provider_payment_charge_id"`
|
||||||
|
TotalAmount int `json:"total_amount"`
|
||||||
|
SubscriptionExpirationDate int `json:"subscription_expiration_date,omitempty"`
|
||||||
|
IsRecurring bool `json:"is_recurring,omitempty"`
|
||||||
|
IsFirstRecurring bool `json:"is_first_recurring,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefundedPayment contains basic information about a refunded payment.
|
||||||
|
type RefundedPayment struct {
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
InvoicePayload string `json:"invoice_payload"`
|
||||||
|
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
|
||||||
|
ProviderPaymentChargeID string `json:"provider_payment_charge_id,omitempty"`
|
||||||
|
TotalAmount int `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShippingQuery contains information about an incoming shipping query.
|
||||||
|
type ShippingQuery struct {
|
||||||
|
ShippingAddress ShippingAddress `json:"shipping_address"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
InvoicePayload string `json:"invoice_payload"`
|
||||||
|
From User `json:"from"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreCheckoutQuery contains information about an incoming pre-checkout query.
|
||||||
|
type PreCheckoutQuery struct {
|
||||||
|
OrderInfo OrderInfo `json:"order_info,omitempty"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
InvoicePayload string `json:"invoice_payload"`
|
||||||
|
ShippingOptionID string `json:"shipping_option_id,omitempty"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
From User `json:"from"`
|
||||||
|
TotalAmount int `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaidMediaPurchased contains information about a paid media purchase.
|
||||||
|
type PaidMediaPurchased struct {
|
||||||
|
PaidMediaPayload string `json:"paid_media_payload"`
|
||||||
|
From User `json:"from"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevenueWithdrawalState describes the state of a revenue withdrawal operation.
|
||||||
|
type RevenueWithdrawalState interface {
|
||||||
|
ImplementsRevenueWithdrawalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevenueWithdrawalStatePending describes the state of a withdrawal in progress.
|
||||||
|
type RevenueWithdrawalStatePending struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
|
||||||
|
func (r RevenueWithdrawalStatePending) ImplementsRevenueWithdrawalState() {}
|
||||||
|
|
||||||
|
// RevenueWithdrawalStateSucceeded describes the state of a succeeded withdrawal.
|
||||||
|
type RevenueWithdrawalStateSucceeded struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Date int `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
|
||||||
|
func (r RevenueWithdrawalStateSucceeded) ImplementsRevenueWithdrawalState() {}
|
||||||
|
|
||||||
|
// RevenueWithdrawalStateFailed describes the state of a failed withdrawal, in which the transaction was refunded.
|
||||||
|
type RevenueWithdrawalStateFailed struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
|
||||||
|
func (r RevenueWithdrawalStateFailed) ImplementsRevenueWithdrawalState() {}
|
||||||
|
|
||||||
|
// AffiliateInfo
|
||||||
|
type AffiliateInfo struct {
|
||||||
|
AffiliateUser *User `json:"affiliate_user,omitempty"`
|
||||||
|
AffiliateChat *Chat `json:"affiliate_chat,omitempty"`
|
||||||
|
CommissionPerMille int `json:"commission_per_mille"`
|
||||||
|
Amount int `json:"amount"`
|
||||||
|
NanostarAmount int `json:"nanostar_amount,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionPartner describes the source of a transaction, or its recipient for outgoing transactions.
|
||||||
|
type TransactionPartner interface {
|
||||||
|
ImplementsTransactionPartner()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionPartnerAffiliateProgram describes the affiliate program that issued the affiliate commission received via this transaction.
|
||||||
|
// Type MUST be "affiliate_program".
|
||||||
|
type TransactionPartnerAffiliateProgram struct {
|
||||||
|
SponsorUser *User `json:"sponsor_user,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
CommissionPerMille int `json:"commission_per_mille,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||||
|
func (t TransactionPartnerAffiliateProgram) ImplementsTransactionPartner() {}
|
||||||
|
|
||||||
|
// TransactionPartnerFragment describes a withdrawal transaction with Fragment.
|
||||||
|
// Type MUST be "fragment".
|
||||||
|
type TransactionPartnerFragment struct {
|
||||||
|
WithdrawalState RevenueWithdrawalState `json:"withdrawal_state"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||||
|
func (t TransactionPartnerFragment) ImplementsTransactionPartner() {}
|
||||||
|
|
||||||
|
// TransactionPartnerUser describes a transaction with a user.
|
||||||
|
// Type MUST be "user".
|
||||||
|
type TransactionPartnerUser struct {
|
||||||
|
PaidMedia *[]PaidMedia `json:"paid_media,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
InvoicePayload string `json:"invoice_payload,omitempty"`
|
||||||
|
PaidMediaPayload string `json:"paid_media_payload,omitempty"`
|
||||||
|
User User `json:"user"`
|
||||||
|
Affiliate *AffiliateInfo `json:"affiliate,omitempty"`
|
||||||
|
Gift Gift `json:"gift,omitempty"`
|
||||||
|
SubscriptionPeriod int `json:"subscription_period,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionPartnerChat describes a transaction with a chat.
|
||||||
|
type TransactionPartnerChat struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Chat Chat `json:"chat"`
|
||||||
|
Gift Gift `json:"gift,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||||
|
func (t TransactionPartnerUser) ImplementsTransactionPartner() {}
|
||||||
|
|
||||||
|
// TransactionPartnerTelegramAds describes a withdrawal transaction to the Telegram Ads platform.
|
||||||
|
// Type MUST be "telegram_ads".
|
||||||
|
type TransactionPartnerTelegramAds struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||||
|
func (t TransactionPartnerTelegramAds) ImplementsTransactionPartner() {}
|
||||||
|
|
||||||
|
// TransactionPartnerTelegramApi describes a transaction with payment for paid broadcasting.
|
||||||
|
// Type MUST be "telegram_api".
|
||||||
|
type TransactionPartnerTelegramApi struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
RequestCount int `json:"request_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||||
|
func (t TransactionPartnerTelegramApi) ImplementsTransactionPartner() {}
|
||||||
|
|
||||||
|
// TransactionPartnerOther describes a transaction with an unknown source or recipient.
|
||||||
|
// Type MUST be "other".
|
||||||
|
type TransactionPartnerOther struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||||
|
func (t TransactionPartnerOther) ImplementsTransactionPartner() {}
|
||||||
|
|
||||||
|
// StarTransaction describes a Telegram Star transaction.
|
||||||
|
type StarTransaction struct {
|
||||||
|
Source TransactionPartner `json:"source"`
|
||||||
|
Receiver TransactionPartner `json:"receiver"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Amount int `json:"amount"`
|
||||||
|
NanostarAmount int `json:"nanostar_amount,omitempty"`
|
||||||
|
Date int `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StarTransactions contains a list of Telegram Star transactions.
|
||||||
|
type StarTransactions struct {
|
||||||
|
Transaction []StarTransaction `json:"transaction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StarTransactionsOptions contains the optional parameters used by the GetStarTransactions method.
|
||||||
|
type StarTransactionsOptions struct {
|
||||||
|
Offset int `query:"offset"`
|
||||||
|
Limit int `query:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendInvoice is used to send invoices.
|
||||||
|
func (a API) SendInvoice(chatID int64, title, description, payload, currency string, prices []LabeledPrice, opts *InvoiceOptions) (res APIResponseMessage, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
p, err := json.Marshal(prices)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vals.Set("chat_id", itoa(chatID))
|
||||||
|
vals.Set("title", title)
|
||||||
|
vals.Set("description", description)
|
||||||
|
vals.Set("payload", payload)
|
||||||
|
vals.Set("currency", currency)
|
||||||
|
vals.Set("prices", string(p))
|
||||||
|
return res, client.get(a.base, "sendInvoice", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInvoiceLink creates a link for an invoice.
|
||||||
|
func (a API) CreateInvoiceLink(title, description, payload, currency string, prices []LabeledPrice, opts *CreateInvoiceLinkOptions) (res APIResponseBase, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
p, err := json.Marshal(prices)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vals.Set("title", title)
|
||||||
|
vals.Set("description", description)
|
||||||
|
vals.Set("payload", payload)
|
||||||
|
vals.Set("currency", currency)
|
||||||
|
vals.Set("prices", string(p))
|
||||||
|
return res, client.get(a.base, "createInvoiceLink", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnswerShippingQuery is used to reply to shipping queries.
|
||||||
|
// If you sent an invoice requesting a shipping address and the parameter is_flexible was specified,
|
||||||
|
// the Bot API will send an Update with a shipping_query field to the bot.
|
||||||
|
func (a API) AnswerShippingQuery(shippingQueryID string, ok bool, opts *ShippingQueryOptions) (res APIResponseBase, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("shipping_query_id", shippingQueryID)
|
||||||
|
vals.Set("ok", btoa(ok))
|
||||||
|
return res, client.get(a.base, "answerShippingQuery", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnswerPreCheckoutQuery is used to respond to such pre-checkout queries.
|
||||||
|
// Once the user has confirmed their payment and shipping details,
|
||||||
|
// the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query.
|
||||||
|
// NOTE: The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent.
|
||||||
|
func (a API) AnswerPreCheckoutQuery(preCheckoutQueryID string, ok bool, opts *PreCheckoutOptions) (res APIResponseBase, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("pre_checkout_query_id", preCheckoutQueryID)
|
||||||
|
vals.Set("ok", btoa(ok))
|
||||||
|
return res, client.get(a.base, "answerPreCheckoutQuery", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStarTransactions returns the bot's Telegram Star transactions in chronological order.
|
||||||
|
func (a API) GetStarTransactions(opts *StarTransactionsOptions) (res APIResponseStarTransactions, err error) {
|
||||||
|
return res, client.get(a.base, "getStarTransactions", urlValues(opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefundStarPayment refunds a successful payment in Telegram Stars.
|
||||||
|
func (a API) RefundStarPayment(userID int64, telegramPaymentChargeID string) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
|
||||||
|
return res, client.get(a.base, "refundStarPayment", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditUserStarSubscription allows the bot to cancel or re-enable extension of a subscription paid in Telegram Stars.
|
||||||
|
func (a API) EditUserStarSubscription(userID int64, telegramPaymentChargeID string, isCanceled bool) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
|
||||||
|
vals.Set("is_canceled", btoa(isCanceled))
|
||||||
|
return res, client.get(a.base, "editUserStarSubscription", vals, &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRevenueWithdrawalStatePending(t *testing.T) {
|
||||||
|
r := RevenueWithdrawalStatePending{}
|
||||||
|
r.ImplementsRevenueWithdrawalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevenueWithdrawalStateSucceeded(t *testing.T) {
|
||||||
|
r := RevenueWithdrawalStateSucceeded{}
|
||||||
|
r.ImplementsRevenueWithdrawalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevenueWithdrawalStateFailed(t *testing.T) {
|
||||||
|
r := RevenueWithdrawalStateFailed{}
|
||||||
|
r.ImplementsRevenueWithdrawalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransactionPartnerFragment(t *testing.T) {
|
||||||
|
r := TransactionPartnerFragment{}
|
||||||
|
r.ImplementsTransactionPartner()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransactionPartnerUser(t *testing.T) {
|
||||||
|
r := TransactionPartnerUser{}
|
||||||
|
r.ImplementsTransactionPartner()
|
||||||
|
}
|
||||||
|
func TestTransactionPartnerTelegramAds(t *testing.T) {
|
||||||
|
r := TransactionPartnerTelegramAds{}
|
||||||
|
r.ImplementsTransactionPartner()
|
||||||
|
}
|
||||||
|
func TestTransactionPartnerOther(t *testing.T) {
|
||||||
|
r := TransactionPartnerOther{}
|
||||||
|
r.ImplementsTransactionPartner()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendInvoice(t *testing.T) {
|
||||||
|
_, err := api.SendInvoice(
|
||||||
|
chatID,
|
||||||
|
"TestSendInvoice",
|
||||||
|
"TestSendInvoiceDesc",
|
||||||
|
"echotron_test",
|
||||||
|
"XTR",
|
||||||
|
[]LabeledPrice{
|
||||||
|
{
|
||||||
|
Label: "Test",
|
||||||
|
Amount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateInvoiceLink(t *testing.T) {
|
||||||
|
_, err := api.CreateInvoiceLink(
|
||||||
|
"TestCreateInvoiceLink",
|
||||||
|
"TestCreateInvoiceLinkDesc",
|
||||||
|
"echotron_test",
|
||||||
|
"XTR",
|
||||||
|
[]LabeledPrice{
|
||||||
|
{
|
||||||
|
Label: "Test",
|
||||||
|
Amount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStarTransactions(t *testing.T) {
|
||||||
|
_, err := api.GetStarTransactions(
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toString(v reflect.Value) string {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return v.String()
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
return strconv.FormatFloat(v.Float(), 'f', -1, 64)
|
||||||
|
|
||||||
|
case reflect.Int, reflect.Int64:
|
||||||
|
return strconv.FormatInt(v.Int(), 10)
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
return strconv.FormatBool(v.Bool())
|
||||||
|
|
||||||
|
case reflect.Struct, reflect.Interface, reflect.Slice, reflect.Array:
|
||||||
|
b, _ := json.Marshal(v.Interface())
|
||||||
|
return string(b)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scan(i any, v url.Values) url.Values {
|
||||||
|
e := reflect.ValueOf(i)
|
||||||
|
|
||||||
|
if e.Kind() == reflect.Pointer {
|
||||||
|
e = e.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Kind() == reflect.Invalid {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < e.NumField(); i++ {
|
||||||
|
fTag := e.Type().Field(i).Tag
|
||||||
|
|
||||||
|
if name := fTag.Get("query"); name != "" && !e.Field(i).IsZero() {
|
||||||
|
v.Set(name, toString(e.Field(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlValues(i any) url.Values {
|
||||||
|
if i == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return scan(i, url.Values{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func addValues(vals url.Values, i any) url.Values {
|
||||||
|
if i == nil {
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
if vals == nil {
|
||||||
|
vals = make(url.Values)
|
||||||
|
}
|
||||||
|
return scan(i, vals)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scanTest struct {
|
||||||
|
i any
|
||||||
|
predefined url.Values
|
||||||
|
expected url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScan(t *testing.T) {
|
||||||
|
tests := []scanTest{
|
||||||
|
{
|
||||||
|
i: CommandOptions{
|
||||||
|
LanguageCode: "it",
|
||||||
|
Scope: BotCommandScope{Type: BCSTChat, ChatID: 33288},
|
||||||
|
},
|
||||||
|
predefined: url.Values{"foo": {"bar"}},
|
||||||
|
expected: url.Values{
|
||||||
|
"foo": {"bar"},
|
||||||
|
"language_code": {"it"},
|
||||||
|
"scope": {`{"type":"chat","chat_id":33288,"user_id":0}`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
result := scan(tt.i, tt.predefined)
|
||||||
|
if !reflect.DeepEqual(tt.expected, result) {
|
||||||
|
t.Fatalf("test #%d: result differs from expected value\n", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToStringDefault(t *testing.T) {
|
||||||
|
ret := toString(reflect.ValueOf(nil))
|
||||||
|
|
||||||
|
if ret != "" {
|
||||||
|
t.Fatalf("expected empty string, got %+v", ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUrlValues(t *testing.T) {
|
||||||
|
ret := urlValues(nil)
|
||||||
|
|
||||||
|
if ret != nil {
|
||||||
|
t.Fatalf("expected nil, got %+v", ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddValues(t *testing.T) {
|
||||||
|
vals := url.Values{}
|
||||||
|
ret := addValues(vals, nil)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(vals, ret) {
|
||||||
|
t.Fatalf("expected nil, got %+v", ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddValuesNil(t *testing.T) {
|
||||||
|
opts := MessageOptions{ParseMode: MarkdownV2}
|
||||||
|
vals := urlValues(opts)
|
||||||
|
ret := addValues(nil, opts)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(vals, ret) {
|
||||||
|
t.Fatalf("expected %+v, got %+v", vals, ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2023 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PollingUpdates is a wrapper function for PollingUpdatesOptions.
|
||||||
|
func PollingUpdates(token string) <-chan *Update {
|
||||||
|
return PollingUpdatesOptions(token, true, UpdateOptions{Timeout: 120})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollingUpdatesOptions returns a read-only channel of incoming updates from the Telegram API.
|
||||||
|
func PollingUpdatesOptions(token string, dropPendingUpdates bool, opts UpdateOptions) <-chan *Update {
|
||||||
|
var updates = make(chan *Update)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(updates)
|
||||||
|
|
||||||
|
var (
|
||||||
|
api = NewAPI(token)
|
||||||
|
timeout = opts.Timeout
|
||||||
|
isFirstRun = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// deletes webhook if present to run in long polling mode
|
||||||
|
if _, err := api.DeleteWebhook(dropPendingUpdates); err != nil {
|
||||||
|
log.Println("echotron.PollingUpdates", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if isFirstRun {
|
||||||
|
opts.Timeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := api.GetUpdates(&opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("echotron.PollingUpdates", err)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dropPendingUpdates || !isFirstRun {
|
||||||
|
for _, u := range response.Result {
|
||||||
|
updates <- u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(response.Result); l > 0 {
|
||||||
|
opts.Offset = response.Result[l-1].ID + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFirstRun {
|
||||||
|
isFirstRun = false
|
||||||
|
opts.Timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return updates
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookUpdates is a wrapper function for WebhookUpdatesOptions.
|
||||||
|
func WebhookUpdates(url, token string) <-chan *Update {
|
||||||
|
return WebhookUpdatesOptions(url, token, false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookUpdatesOptions returns a read-only channel of incoming updates from the Telegram API.
|
||||||
|
// The webhookUrl should be provided in the following format: '<hostname>:<port>/<path>',
|
||||||
|
// eg: 'https://example.com:443/bot_token'.
|
||||||
|
// WebhookUpdatesOptions will then proceed to communicate the webhook url '<hostname>/<path>'
|
||||||
|
// to Telegram and run a webserver that listens to ':<port>' and handles the path.
|
||||||
|
func WebhookUpdatesOptions(whURL, token string, dropPendingUpdates bool, opts *WebhookOptions) <-chan *Update {
|
||||||
|
u, err := url.Parse(whURL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wURL := u.Hostname() + u.EscapedPath()
|
||||||
|
api := NewAPI(token)
|
||||||
|
if _, err := api.SetWebhook(wURL, dropPendingUpdates, opts); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updates = make(chan *Update)
|
||||||
|
http.HandleFunc(u.EscapedPath(), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var update Update
|
||||||
|
|
||||||
|
jsn, err := readRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("echotron.WebhookUpdates", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(jsn, &update); err != nil {
|
||||||
|
log.Println("echotron.WebhookUpdates", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates <- &update
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(updates)
|
||||||
|
port := fmt.Sprintf(":%s", u.Port())
|
||||||
|
for {
|
||||||
|
if err := http.ListenAndServe(port, nil); err != nil {
|
||||||
|
log.Println("echotron.WebhookUpdates", err)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return updates
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPollingUpdates(t *testing.T) {
|
||||||
|
PollingUpdates(api.token)
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sticker represents a sticker.
|
||||||
|
type Sticker struct {
|
||||||
|
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
|
||||||
|
MaskPosition *MaskPosition `json:"mask_position,omitempty"`
|
||||||
|
Type StickerSetType `json:"type"`
|
||||||
|
FileUniqueID string `json:"file_unique_id"`
|
||||||
|
SetName string `json:"set_name,omitempty"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
Emoji string `json:"emoji,omitempty"`
|
||||||
|
CustomEmojiID string `json:"custom_emoji_id,omitempty"`
|
||||||
|
PremiumAnimation File `json:"premium_animation,omitempty"`
|
||||||
|
FileSize int `json:"file_size,omitempty"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
IsVideo bool `json:"is_video"`
|
||||||
|
IsAnimated bool `json:"is_animated"`
|
||||||
|
NeedsRepainting bool `json:"needs_repainting,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StickerSet represents a sticker set.
|
||||||
|
type StickerSet struct {
|
||||||
|
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
StickerType StickerSetType `json:"sticker_type"`
|
||||||
|
Stickers []Sticker `json:"stickers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StickerSetType represents the type of a sticker or of the entire set
|
||||||
|
type StickerSetType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RegularStickerSet StickerSetType = "regular"
|
||||||
|
MaskStickerSet = "mask"
|
||||||
|
CustomEmojiStickerSet = "custom_emoji"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StickerFormat is a custom type for the various sticker formats.
|
||||||
|
type StickerFormat string
|
||||||
|
|
||||||
|
// These are all the possible sticker formats.
|
||||||
|
const (
|
||||||
|
StaticFormat StickerFormat = "static"
|
||||||
|
AnimatedFormat = "animated"
|
||||||
|
VideoFormat = "video"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaskPosition describes the position on faces where a mask should be placed by default.
|
||||||
|
type MaskPosition struct {
|
||||||
|
Point MaskPoint `json:"point"`
|
||||||
|
XShift float32 `json:"x_shift"`
|
||||||
|
YShift float32 `json:"y_shift"`
|
||||||
|
Scale float32 `json:"scale"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaskPoint is a custom type for the various part of face where a mask should be placed.
|
||||||
|
type MaskPoint string
|
||||||
|
|
||||||
|
// These are all the possible parts of the face for a mask.
|
||||||
|
const (
|
||||||
|
ForeheadPoint MaskPoint = "forehead"
|
||||||
|
EyesPoint = "eyes"
|
||||||
|
MouthPoint = "mouth"
|
||||||
|
ChinPoint = "chin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewStickerSetOptions contains the optional parameters used in the CreateNewStickerSet method.
|
||||||
|
type NewStickerSetOptions struct {
|
||||||
|
StickerType StickerSetType `query:"sticker_type"`
|
||||||
|
NeedsRepainting bool `query:"needs_repainting"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputSticker is a struct which describes a sticker to be added to a sticker set.
|
||||||
|
type InputSticker struct {
|
||||||
|
MaskPosition *MaskPosition `json:"mask_position,omitempty"`
|
||||||
|
Keywords *[]string `json:"keywords,omitempty"`
|
||||||
|
Format StickerFormat `json:"format"`
|
||||||
|
Sticker InputFile `json:"-"`
|
||||||
|
EmojiList []string `json:"emoji_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// stickerEnvelope is a generic struct for all the various structs under the InputSticker interface.
|
||||||
|
type stickerEnvelope struct {
|
||||||
|
Sticker string `json:"sticker"`
|
||||||
|
InputSticker
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSticker is used to send static .WEBP or animated .TGS stickers.
|
||||||
|
func (a API) SendSticker(stickerID string, chatID int64, opts *StickerOptions) (res APIResponseMessage, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("sticker", stickerID)
|
||||||
|
vals.Set("chat_id", itoa(chatID))
|
||||||
|
return res, client.get(a.base, "sendSticker", addValues(vals, opts), &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStickerSet is used to get a sticker set.
|
||||||
|
func (a API) GetStickerSet(name string) (res APIResponseStickerSet, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("name", name)
|
||||||
|
return res, client.get(a.base, "getStickerSet", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCustomEmojiStickers is used to get information about custom emoji stickers by their identifiers.
|
||||||
|
func (a API) GetCustomEmojiStickers(customEmojiIDs ...string) (res APIResponseStickers, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
jsn, _ := json.Marshal(customEmojiIDs)
|
||||||
|
vals.Set("custom_emoji_ids", string(jsn))
|
||||||
|
return res, client.get(a.base, "getCustomEmojiStickers", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadStickerFile is used to upload a .PNG file with a sticker for later use in
|
||||||
|
// CreateNewStickerSet and AddStickerToSet methods (can be used multiple times).
|
||||||
|
func (a API) UploadStickerFile(userID int64, sticker InputFile, format StickerFormat) (res APIResponseFile, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("sticker_format", string(format))
|
||||||
|
return res, client.postFile(a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNewStickerSet is used to create a new sticker set owned by a user.
|
||||||
|
func (a API) CreateNewStickerSet(userID int64, name, title string, stickers []InputSticker, opts *NewStickerSetOptions) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("name", name)
|
||||||
|
vals.Set("title", title)
|
||||||
|
return res, client.postStickers(a.base, "createNewStickerSet", addValues(vals, opts), &res, stickers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStickerToSet is used to add a new sticker to a set created by the bot.
|
||||||
|
func (a API) AddStickerToSet(userID int64, name string, sticker InputSticker) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("name", name)
|
||||||
|
return res, client.postStickers(a.base, "addStickerToSet", vals, &res, sticker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStickerPositionInSet is used to move a sticker in a set created by the bot to a specific position.
|
||||||
|
func (a API) SetStickerPositionInSet(sticker string, position int) (res APIResponseBase, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("sticker", sticker)
|
||||||
|
vals.Set("position", itoa(int64(position)))
|
||||||
|
return res, client.get(a.base, "setStickerPositionInSet", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStickerFromSet is used to delete a sticker from a set created by the bot.
|
||||||
|
func (a API) DeleteStickerFromSet(sticker string) (res APIResponseBase, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("sticker", sticker)
|
||||||
|
return res, client.get(a.base, "deleteStickerFromSet", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceStickerInSet is used to replace an existing sticker in a sticker set with a new one.
|
||||||
|
// The method is equivalent to calling DeleteStickerFromSet, then AddStickerToSet, then SetStickerPositionInSet.
|
||||||
|
func (a API) ReplaceStickerInSet(userID int64, name string, old_sticker string, sticker InputSticker) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("name", name)
|
||||||
|
vals.Set("old_sticker", old_sticker)
|
||||||
|
return res, client.postStickers(a.base, "replaceStickerInSet", vals, &res, sticker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStickerEmojiList is used to change the list of emoji assigned to a regular or custom emoji sticker.
|
||||||
|
// The sticker must belong to a sticker set created by the bot.
|
||||||
|
func (a API) SetStickerEmojiList(sticker string, emojis []string) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
jsn, _ := json.Marshal(emojis)
|
||||||
|
|
||||||
|
vals.Set("sticker", sticker)
|
||||||
|
vals.Set("emoji_list", string(jsn))
|
||||||
|
return res, client.get(a.base, "setStickerEmojiList", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStickerKeywords is used to change search keywords assigned to a regular or custom emoji sticker.
|
||||||
|
// The sticker must belong to a sticker set created by the bot.
|
||||||
|
func (a API) SetStickerKeywords(sticker string, keywords []string) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
jsn, _ := json.Marshal(keywords)
|
||||||
|
|
||||||
|
vals.Set("sticker", sticker)
|
||||||
|
vals.Set("keywords", string(jsn))
|
||||||
|
return res, client.get(a.base, "setStickerKeywords", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStickerMaskPosition is used to change the mask position of a mask sticker.
|
||||||
|
// The sticker must belong to a sticker set that was created by the bot.
|
||||||
|
func (a API) SetStickerMaskPosition(sticker string, mask MaskPosition) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
jsn, _ := json.Marshal(mask)
|
||||||
|
|
||||||
|
vals.Set("sticker", sticker)
|
||||||
|
vals.Set("mask_position", string(jsn))
|
||||||
|
return res, client.get(a.base, "setStickerMaskPosition", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStickerSetTitle is used to set the title of a created sticker set.
|
||||||
|
func (a API) SetStickerSetTitle(name, title string) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("name", name)
|
||||||
|
vals.Set("title", title)
|
||||||
|
return res, client.get(a.base, "setStickerSetTitle", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStickerSetThumbnail is used to set the thumbnail of a sticker set.
|
||||||
|
func (a API) SetStickerSetThumbnail(name string, userID int64, thumbnail InputFile, format StickerFormat) (res APIResponseBase, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("name", name)
|
||||||
|
vals.Set("user_id", itoa(userID))
|
||||||
|
vals.Set("format", string(format))
|
||||||
|
return res, client.postFile(a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCustomEmojiStickerSetThumbnail is used to set the thumbnail of a custom emoji sticker set.
|
||||||
|
func (a API) SetCustomEmojiStickerSetThumbnail(name, emojiID string) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("name", name)
|
||||||
|
vals.Set("custom_emoji_id", emojiID)
|
||||||
|
return res, client.get(a.base, "setCustomEmojiStickerSetThumbnail", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStickerSet is used to delete a sticker set that was created by the bot.
|
||||||
|
func (a API) DeleteStickerSet(name string) (res APIResponseBool, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
vals.Set("name", name)
|
||||||
|
return res, client.get(a.base, "DeleteStickerSet", vals, &res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetForumTopicIconStickers is used to get custom emoji stickers, which can be used as a forum topic icon by any user.
|
||||||
|
func (a API) GetForumTopicIconStickers() (res APIResponseStickers, err error) {
|
||||||
|
return res, client.get(a.base, "getForumTopicIconStickers", nil, &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2018 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
stickerFile *File
|
||||||
|
stickerSet *StickerSet
|
||||||
|
stickerSetName = fmt.Sprintf("set%d_by_echotron_coverage_bot", time.Now().Unix())
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUploadStickerFile(t *testing.T) {
|
||||||
|
resp, err := api.UploadStickerFile(
|
||||||
|
chatID,
|
||||||
|
NewInputFilePath("assets/tests/echotron_test.png"),
|
||||||
|
StaticFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stickerFile = resp.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateNewStickerSet(t *testing.T) {
|
||||||
|
_, err := api.CreateNewStickerSet(
|
||||||
|
chatID,
|
||||||
|
stickerSetName,
|
||||||
|
"Echotron Coverage Pack",
|
||||||
|
[]InputSticker{
|
||||||
|
{
|
||||||
|
Sticker: NewInputFileID(stickerFile.FileID),
|
||||||
|
EmojiList: []string{"🤖"},
|
||||||
|
Format: StaticFormat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Sticker: NewInputFilePath("assets/tests/echotron_test.png"),
|
||||||
|
EmojiList: []string{"🤖"},
|
||||||
|
Format: StaticFormat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Sticker: NewInputFileURL(photoURL),
|
||||||
|
EmojiList: []string{"🤖"},
|
||||||
|
Format: StaticFormat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddStickerToSet(t *testing.T) {
|
||||||
|
_, err := api.AddStickerToSet(
|
||||||
|
chatID,
|
||||||
|
stickerSetName,
|
||||||
|
InputSticker{
|
||||||
|
Sticker: NewInputFilePath("assets/tests/echotron_sticker.png"),
|
||||||
|
EmojiList: []string{"🤖"},
|
||||||
|
Format: StaticFormat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCustomEmojiStickers(t *testing.T) {
|
||||||
|
_, err := api.GetCustomEmojiStickers(
|
||||||
|
"5407041870620531251",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStickerSet(t *testing.T) {
|
||||||
|
resp, err := api.GetStickerSet(
|
||||||
|
stickerSetName,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stickerSet = resp.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetStickerPositionInSet(t *testing.T) {
|
||||||
|
_, err := api.SetStickerPositionInSet(
|
||||||
|
stickerSet.Stickers[1].FileID,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetStickerEmojiList(t *testing.T) {
|
||||||
|
_, err := api.SetStickerEmojiList(
|
||||||
|
stickerSet.Stickers[0].FileID,
|
||||||
|
[]string{"🤖", "👾"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetStickerKeywords(t *testing.T) {
|
||||||
|
_, err := api.SetStickerKeywords(
|
||||||
|
stickerSet.Stickers[0].FileID,
|
||||||
|
[]string{"echotron"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetStickerSetTitle(t *testing.T) {
|
||||||
|
_, err := api.SetStickerSetTitle(
|
||||||
|
stickerSetName,
|
||||||
|
fmt.Sprintf("new_%s", stickerSetName),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceStickerInSet(t *testing.T) {
|
||||||
|
_, err := api.ReplaceStickerInSet(
|
||||||
|
chatID,
|
||||||
|
stickerSetName,
|
||||||
|
stickerSet.Stickers[0].FileID,
|
||||||
|
InputSticker{
|
||||||
|
Sticker: NewInputFileURL(photoURL),
|
||||||
|
EmojiList: []string{"🤖"},
|
||||||
|
Format: StaticFormat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteStickerFromSet(t *testing.T) {
|
||||||
|
_, err := api.DeleteStickerFromSet(
|
||||||
|
stickerSet.Stickers[1].FileID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendSticker(t *testing.T) {
|
||||||
|
_, err := api.SendSticker(
|
||||||
|
stickerSet.Stickers[0].FileID,
|
||||||
|
chatID,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetStickerSetThumbnail(t *testing.T) {
|
||||||
|
_, err := api.SetStickerSetThumbnail(
|
||||||
|
stickerSetName,
|
||||||
|
chatID,
|
||||||
|
NewInputFilePath("assets/tests/echotron_thumb.png"),
|
||||||
|
StaticFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteStickerSet(t *testing.T) {
|
||||||
|
_, err := api.DeleteStickerSet(stickerSetName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetForumTopicIconStickers(t *testing.T) {
|
||||||
|
res, err := api.GetForumTopicIconStickers()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Result) == 0 {
|
||||||
|
t.Fatal("error: Telegram returned no forum topic icon stickers")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAPIResponseBase(_ *testing.T) {
|
||||||
|
a := APIResponseBase{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseUpdate(_ *testing.T) {
|
||||||
|
a := APIResponseUpdate{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseUser(_ *testing.T) {
|
||||||
|
a := APIResponseUser{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseMessage(_ *testing.T) {
|
||||||
|
a := APIResponseMessage{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseMessageArray(_ *testing.T) {
|
||||||
|
a := APIResponseMessageArray{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseMessageID(_ *testing.T) {
|
||||||
|
a := APIResponseMessageID{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseCommands(_ *testing.T) {
|
||||||
|
a := APIResponseCommands{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseBool(_ *testing.T) {
|
||||||
|
a := APIResponseBool{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseString(_ *testing.T) {
|
||||||
|
a := APIResponseString{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseChat(_ *testing.T) {
|
||||||
|
a := APIResponseChat{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseInviteLink(_ *testing.T) {
|
||||||
|
a := APIResponseInviteLink{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseStickerSet(_ *testing.T) {
|
||||||
|
a := APIResponseStickerSet{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseUserProfile(_ *testing.T) {
|
||||||
|
a := APIResponseUserProfile{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseUserProfileAudios(_ *testing.T) {
|
||||||
|
a := APIResponseUserProfileAudios{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseFile(_ *testing.T) {
|
||||||
|
a := APIResponseFile{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseAdministrators(_ *testing.T) {
|
||||||
|
a := APIResponseAdministrators{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseChatMember(_ *testing.T) {
|
||||||
|
a := APIResponseChatMember{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseInteger(_ *testing.T) {
|
||||||
|
a := APIResponseInteger{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponsePoll(_ *testing.T) {
|
||||||
|
a := APIResponsePoll{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseGameHighScore(_ *testing.T) {
|
||||||
|
a := APIResponseGameHighScore{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseWebhook(_ *testing.T) {
|
||||||
|
a := APIResponseWebhook{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseSentWebAppMessage(_ *testing.T) {
|
||||||
|
a := APIResponseSentWebAppMessage{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseMenuButton(_ *testing.T) {
|
||||||
|
a := APIResponseMenuButton{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseChatAdministratorRights(_ *testing.T) {
|
||||||
|
a := APIResponseChatAdministratorRights{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseBotDescription(_ *testing.T) {
|
||||||
|
a := APIResponseBotDescription{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseBotShortDescription(_ *testing.T) {
|
||||||
|
a := APIResponseBotShortDescription{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseBusinessConnection(_ *testing.T) {
|
||||||
|
a := APIResponseBusinessConnection{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseStarTransactions(_ *testing.T) {
|
||||||
|
a := APIResponseStarTransactions{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponsePreparedInlineMessage(_ *testing.T) {
|
||||||
|
a := APIResponsePreparedInlineMessage{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIResponseGifts(_ *testing.T) {
|
||||||
|
a := APIResponseGifts{}
|
||||||
|
a.Base()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputMediaPhoto(_ *testing.T) {
|
||||||
|
i := InputMediaPhoto{}
|
||||||
|
i.media()
|
||||||
|
i.thumbnail()
|
||||||
|
i.groupable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputMediaVideo(_ *testing.T) {
|
||||||
|
i := InputMediaVideo{}
|
||||||
|
i.media()
|
||||||
|
i.thumbnail()
|
||||||
|
i.groupable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputMediaAnimation(_ *testing.T) {
|
||||||
|
i := InputMediaAnimation{}
|
||||||
|
i.media()
|
||||||
|
i.thumbnail()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputMediaAudio(_ *testing.T) {
|
||||||
|
i := InputMediaAudio{}
|
||||||
|
i.media()
|
||||||
|
i.thumbnail()
|
||||||
|
i.groupable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputMediaDocument(_ *testing.T) {
|
||||||
|
i := InputMediaDocument{}
|
||||||
|
i.media()
|
||||||
|
i.thumbnail()
|
||||||
|
i.groupable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputPaidMediaPhoto(_ *testing.T) {
|
||||||
|
i := InputPaidMediaPhoto{}
|
||||||
|
i.media()
|
||||||
|
i.groupable()
|
||||||
|
i.thumbnail()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputPaidMediaVideo(_ *testing.T) {
|
||||||
|
i := InputPaidMediaVideo{}
|
||||||
|
i.media()
|
||||||
|
i.groupable()
|
||||||
|
i.thumbnail()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputProfilePhotoStatic(_ *testing.T) {
|
||||||
|
i := InputProfilePhotoStatic{}
|
||||||
|
i.file()
|
||||||
|
i.inputProfilePhoto()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputProfilePhotoAnimated(_ *testing.T) {
|
||||||
|
i := InputProfilePhotoAnimated{}
|
||||||
|
i.file()
|
||||||
|
i.inputProfilePhoto()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundFillSolid(_ *testing.T) {
|
||||||
|
b := BackgroundFillSolid{}
|
||||||
|
b.ImplementsBackgroundFill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundFillGradient(_ *testing.T) {
|
||||||
|
b := BackgroundFillGradient{}
|
||||||
|
b.ImplementsBackgroundFill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundFillFreeformGradient(_ *testing.T) {
|
||||||
|
b := BackgroundFillFreeformGradient{}
|
||||||
|
b.ImplementsBackgroundFill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundTypeFill(_ *testing.T) {
|
||||||
|
b := BackgroundTypeFill{}
|
||||||
|
b.ImplementsBackgroundType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundTypeWallpaper(_ *testing.T) {
|
||||||
|
b := BackgroundTypeWallpaper{}
|
||||||
|
b.ImplementsBackgroundType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundTypePattern(_ *testing.T) {
|
||||||
|
b := BackgroundTypePattern{}
|
||||||
|
b.ImplementsBackgroundType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundTypeChatTheme(_ *testing.T) {
|
||||||
|
b := BackgroundTypeChatTheme{}
|
||||||
|
b.ImplementsBackgroundType()
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Echotron
|
||||||
|
* Copyright (C) 2022 The Echotron Contributors
|
||||||
|
*
|
||||||
|
* Echotron is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Echotron is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package echotron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebAppInfo contains information about a Web App.
|
||||||
|
type WebAppInfo struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SentWebAppMessage contains information about an inline message sent
|
||||||
|
// by a Web App on behalf of a user.
|
||||||
|
type SentWebAppMessage struct {
|
||||||
|
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebAppData contains data sent from a Web App to the bot.
|
||||||
|
type WebAppData struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
ButtonText string `json:"button_text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnswerWebAppQuery is used to set the result of an interaction with a Web App
|
||||||
|
// and send a corresponding message on behalf of the user to the chat from which
|
||||||
|
// the query originated.
|
||||||
|
func (a API) AnswerWebAppQuery(webAppQueryID string, result InlineQueryResult) (res APIResponseSentWebAppMessage, err error) {
|
||||||
|
var vals = make(url.Values)
|
||||||
|
|
||||||
|
resultJson, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vals.Set("web_app_query_id", webAppQueryID)
|
||||||
|
vals.Set("result", string(resultJson))
|
||||||
|
return res, client.get(a.base, "answerWebAppQuery", vals, &res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class JWTConfig(BaseModel):
|
||||||
|
SECRET_KEY: str
|
||||||
|
ALGORITHM: str = 'HS256'
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days
|
||||||
|
|
||||||
|
|
||||||
|
class JWTBase:
|
||||||
|
def __init__(self, config: JWTConfig) -> None:
|
||||||
|
self.config = config
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ['LoggerConfig', 'init']
|
||||||
|
|
||||||
|
from .logger import LoggerConfig, init
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ColoredConsoleFormatter(logging.Formatter):
|
||||||
|
TIME_COLOR = '\033[38;2;89;89;89m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
DARK_CYAN = '\033[36m'
|
||||||
|
DARK_YELLOW = '\033[33m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
|
||||||
|
LEVEL_COLORS = {
|
||||||
|
logging.DEBUG: '\033[36m',
|
||||||
|
logging.INFO: '\033[32m',
|
||||||
|
logging.WARNING: '\033[33m',
|
||||||
|
logging.ERROR: RED,
|
||||||
|
logging.CRITICAL: '\033[35m',
|
||||||
|
}
|
||||||
|
|
||||||
|
LEVEL_NAMES = {
|
||||||
|
logging.DEBUG: 'DBG',
|
||||||
|
logging.INFO: 'INF',
|
||||||
|
logging.WARNING: 'WRN',
|
||||||
|
logging.ERROR: 'ERR',
|
||||||
|
logging.CRITICAL: 'CRT',
|
||||||
|
}
|
||||||
|
|
||||||
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
timestamp = f'{self.TIME_COLOR}{datetime.fromtimestamp(record.created).strftime("%H:%M:%S")}{self.RESET}'
|
||||||
|
|
||||||
|
level_name = self.LEVEL_NAMES.get(record.levelno, record.levelname[:3])
|
||||||
|
level_color = self.LEVEL_COLORS.get(record.levelno, '')
|
||||||
|
level = f'{level_color}{level_name}{self.RESET}'
|
||||||
|
|
||||||
|
message_color = self.RED if record.levelno >= logging.ERROR else ''
|
||||||
|
message = f'{message_color}{self.BOLD}{record.getMessage()}{self.RESET}'
|
||||||
|
|
||||||
|
# Add extra fields
|
||||||
|
extra_parts = []
|
||||||
|
for key, value in record.__dict__.items():
|
||||||
|
if key not in [
|
||||||
|
'name',
|
||||||
|
'msg',
|
||||||
|
'args',
|
||||||
|
'created',
|
||||||
|
'filename',
|
||||||
|
'funcName',
|
||||||
|
'levelname',
|
||||||
|
'levelno',
|
||||||
|
'lineno',
|
||||||
|
'module',
|
||||||
|
'msecs',
|
||||||
|
'message',
|
||||||
|
'pathname',
|
||||||
|
'process',
|
||||||
|
'processName',
|
||||||
|
'relativeCreated',
|
||||||
|
'thread',
|
||||||
|
'threadName',
|
||||||
|
'exc_info',
|
||||||
|
'exc_text',
|
||||||
|
'stack_info',
|
||||||
|
'app_name',
|
||||||
|
'app_version',
|
||||||
|
'taskName',
|
||||||
|
'color_message',
|
||||||
|
]:
|
||||||
|
value_color = self.RED if key == 'error' else self.DARK_YELLOW
|
||||||
|
extra_parts.append(f'{self.DARK_CYAN}{key}{self.RESET}={value_color}{value}{self.RESET}')
|
||||||
|
|
||||||
|
if extra_parts:
|
||||||
|
message += f' {" ".join(extra_parts)}'
|
||||||
|
|
||||||
|
if record.levelno >= logging.WARNING:
|
||||||
|
location = f'{self.TIME_COLOR}{record.module}:{record.lineno}{self.RESET}'
|
||||||
|
result = f'{timestamp} {level} {location} {message}'
|
||||||
|
else:
|
||||||
|
result = f'{timestamp} {level} {message}'
|
||||||
|
|
||||||
|
if record.exc_info:
|
||||||
|
result += '\n' + self.formatException(record.exc_info)
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
|
||||||
|
class JSONFormatter(logging.Formatter):
|
||||||
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
log_data = {
|
||||||
|
'timestamp': datetime.fromtimestamp(record.created, UTC).isoformat(),
|
||||||
|
'level': record.levelname,
|
||||||
|
'message': record.getMessage(),
|
||||||
|
'module': record.module,
|
||||||
|
'package': record.name,
|
||||||
|
'app_name': getattr(record, 'app_name', 'unknown'),
|
||||||
|
'app_version': getattr(record, 'app_version', 'unknown'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.levelno >= logging.ERROR:
|
||||||
|
log_data['location'] = f'{record.pathname}:{record.lineno}'
|
||||||
|
|
||||||
|
if record.exc_info:
|
||||||
|
exc_type = record.exc_info[0]
|
||||||
|
log_data['error_type'] = exc_type.__name__ if exc_type else 'Unknown'
|
||||||
|
log_data['error_message'] = str(record.exc_info[1])
|
||||||
|
log_data['traceback'] = self.formatException(record.exc_info)
|
||||||
|
|
||||||
|
for key, value in record.__dict__.items():
|
||||||
|
if key not in {
|
||||||
|
'name',
|
||||||
|
'msg',
|
||||||
|
'args',
|
||||||
|
'created',
|
||||||
|
'filename',
|
||||||
|
'funcName',
|
||||||
|
'levelname',
|
||||||
|
'levelno',
|
||||||
|
'lineno',
|
||||||
|
'module',
|
||||||
|
'msecs',
|
||||||
|
'message',
|
||||||
|
'pathname',
|
||||||
|
'process',
|
||||||
|
'processName',
|
||||||
|
'relativeCreated',
|
||||||
|
'thread',
|
||||||
|
'threadName',
|
||||||
|
'exc_info',
|
||||||
|
'exc_text',
|
||||||
|
'stack_info',
|
||||||
|
'app_name',
|
||||||
|
'app_version',
|
||||||
|
}:
|
||||||
|
log_data[key] = value
|
||||||
|
|
||||||
|
return json.dumps(log_data, ensure_ascii=False)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
from .console_formatter import ColoredConsoleFormatter
|
||||||
|
from .json_formatter import JSONFormatter
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerConfig(pydantic.BaseModel):
|
||||||
|
APP_NAME: str
|
||||||
|
APP_VERSION: str
|
||||||
|
LEVEL: int = logging.INFO
|
||||||
|
PRETTY_CONSOLE: bool = False
|
||||||
|
|
||||||
|
@pydantic.field_validator('APP_VERSION')
|
||||||
|
@classmethod
|
||||||
|
def normalize_app_version(cls, v: str) -> str:
|
||||||
|
parts = v.removeprefix('v').split('.')
|
||||||
|
if len(parts) < 2 or any(not part.isdigit() for part in parts):
|
||||||
|
raise ValueError('APP_VERSION must look like 1.2.0')
|
||||||
|
commit_count = _get_git_commit_count()
|
||||||
|
if not commit_count:
|
||||||
|
return f'v{".".join(parts)}'
|
||||||
|
parts[-1] = commit_count
|
||||||
|
return f'v{".".join(parts)}'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_git_commit_count() -> str | None:
|
||||||
|
commit_count = os.getenv('GIT_COMMIT_COUNT')
|
||||||
|
if commit_count is None:
|
||||||
|
return None
|
||||||
|
normalized = commit_count.strip()
|
||||||
|
return normalized or None
|
||||||
|
|
||||||
|
|
||||||
|
def init(config: LoggerConfig) -> None:
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(config.LEVEL)
|
||||||
|
root_logger.handlers.clear()
|
||||||
|
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
handler.setLevel(config.LEVEL)
|
||||||
|
|
||||||
|
formatter = ColoredConsoleFormatter() if config.PRETTY_CONSOLE else JSONFormatter()
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(handler)
|
||||||
|
|
||||||
|
for logger_name in ['uvicorn', 'uvicorn.access', 'uvicorn.error']:
|
||||||
|
uvicorn_logger = logging.getLogger(logger_name)
|
||||||
|
uvicorn_logger.handlers = [handler]
|
||||||
|
uvicorn_logger.propagate = False
|
||||||
|
|
||||||
|
old_factory = logging.getLogRecordFactory()
|
||||||
|
|
||||||
|
def record_factory(*args: object, **kwargs: object) -> logging.LogRecord:
|
||||||
|
record = old_factory(*args, **kwargs)
|
||||||
|
record.app_name = config.APP_NAME
|
||||||
|
record.app_version = config.APP_VERSION
|
||||||
|
return record
|
||||||
|
|
||||||
|
logging.setLogRecordFactory(record_factory)
|
||||||
|
|
||||||
|
logging.info('Logger initialized', extra={'app': config.APP_NAME, 'version': config.APP_VERSION})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
from aiogram import Bot
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramConfig(pydantic.BaseModel):
|
||||||
|
TOKEN: str
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramBase:
|
||||||
|
def __init__(self, config: TelegramConfig) -> None:
|
||||||
|
self.bot: Bot = Bot(token=config.TOKEN)
|
||||||
|
logging.info('Telegram bot initialized')
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerConfig(pydantic.BaseModel):
|
||||||
|
INTERVAL_SECONDS: int = 60
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerBase:
|
||||||
|
def __init__(self, config: WorkerConfig) -> None:
|
||||||
|
self.config = config
|
||||||
|
self._task: asyncio.Task[None] | None = None
|
||||||
|
self._stop_event = asyncio.Event()
|
||||||
|
|
||||||
|
log.info('Worker initialized (interval=%ds)', config.INTERVAL_SECONDS)
|
||||||
|
|
||||||
|
async def _cycle_func(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
if self._task is not None:
|
||||||
|
log.warning('Worker already running')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._task = asyncio.create_task(self._run())
|
||||||
|
log.info('Worker started')
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
if self._task is None:
|
||||||
|
log.warning('Worker not running')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self._task, timeout=5.0)
|
||||||
|
except TimeoutError:
|
||||||
|
log.warning('Worker did not stop in time, cancelling')
|
||||||
|
self._task.cancel()
|
||||||
|
try:
|
||||||
|
await self._task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
log.info('Worker task cancelled')
|
||||||
|
|
||||||
|
self._task = None
|
||||||
|
log.info('Worker stopped')
|
||||||
|
|
||||||
|
async def _run(self) -> None:
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
await self._cycle_func()
|
||||||
|
except Exception:
|
||||||
|
log.exception('Error in worker cycle')
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self._stop_event.wait(), timeout=self.config.INTERVAL_SECONDS)
|
||||||
|
except TimeoutError:
|
||||||
|
pass # Время вышло, продолжаем
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
from shared.jwt_base import JWTBase
|
||||||
|
from src.usecase import JWTEncoder
|
||||||
|
|
||||||
|
|
||||||
|
class JWTPayload(pydantic.BaseModel):
|
||||||
|
user_id: uuid.UUID
|
||||||
|
telegram_id: int
|
||||||
|
username: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class JWT(JWTBase, JWTEncoder):
|
||||||
|
def encode_access_token(
|
||||||
|
self,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
telegram_id: int,
|
||||||
|
username: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
expire = datetime.datetime.now(datetime.UTC) + datetime.timedelta(
|
||||||
|
minutes=self.config.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'sub': str(user_id),
|
||||||
|
'telegram_id': telegram_id,
|
||||||
|
'username': username,
|
||||||
|
'exp': expire,
|
||||||
|
'type': 'access',
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded: str = jwt.encode(payload, self.config.SECRET_KEY, algorithm=self.config.ALGORITHM)
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
def decode_access_token(self, token: str) -> JWTPayload:
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, self.config.SECRET_KEY, algorithms=[self.config.ALGORITHM])
|
||||||
|
|
||||||
|
if payload.get('type') != 'access':
|
||||||
|
raise ValueError('Invalid token type')
|
||||||
|
|
||||||
|
user_id = payload.get('sub')
|
||||||
|
if not user_id:
|
||||||
|
raise ValueError('Token missing subject')
|
||||||
|
|
||||||
|
return JWTPayload(
|
||||||
|
user_id=uuid.UUID(user_id),
|
||||||
|
telegram_id=payload['telegram_id'],
|
||||||
|
username=payload.get('username'),
|
||||||
|
)
|
||||||
|
|
||||||
|
except jwt.ExpiredSignatureError as e:
|
||||||
|
raise ValueError('Token has expired') from e
|
||||||
|
except jwt.InvalidTokenError as e:
|
||||||
|
raise ValueError('Invalid token') from e
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from src.usecase import Parser
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FetchChannelResponse(BaseModel):
|
||||||
|
telegram_id: int
|
||||||
|
username: str | None
|
||||||
|
title: str | None
|
||||||
|
access_hash: int | None
|
||||||
|
pts: int | None
|
||||||
|
|
||||||
|
|
||||||
|
class ParserClient(Parser):
|
||||||
|
def __init__(self, base_url: str, timeout: float = 5.0):
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
async def fetch_telegram_channel(self, username: str) -> FetchChannelResponse | None:
|
||||||
|
username = username.lstrip('@')
|
||||||
|
|
||||||
|
url = f'{self.base_url}/fetch-telegram-channel'
|
||||||
|
params = {'username': username}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||||
|
try:
|
||||||
|
response = await client.get(url, params=params)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
log.info('Channel @%s not found in Telegram', username)
|
||||||
|
return None
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
data: dict[str, Any] = response.json()
|
||||||
|
return FetchChannelResponse.model_validate(data)
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
log.error('Parser HTTP error for @%s: %s', username, e)
|
||||||
|
raise
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
log.error('Parser timeout for @%s', username)
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Parser unexpected error for @%s: %s', username, e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def resolve_telegram_channel_by_invite(self, invite_link: str) -> FetchChannelResponse | None:
|
||||||
|
invite_link = invite_link.strip()
|
||||||
|
|
||||||
|
url = f'{self.base_url}/resolve-channel-by-invite'
|
||||||
|
payload = {'invite_link': invite_link}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(url, json=payload)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
log.info('Channel not found by invite link')
|
||||||
|
return None
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
data: dict[str, Any] = response.json()
|
||||||
|
return FetchChannelResponse.model_validate(data)
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
log.error('Parser HTTP error for invite link: %s', e)
|
||||||
|
raise
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
log.error('Parser timeout for invite link')
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Parser unexpected error for invite link: %s', e)
|
||||||
|
raise
|
||||||