Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/code_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Python Code Check

on:
push:
branches:
- main
- 'mk*'

pull_request:
branches:
- main
- 'mk*'

workflow_dispatch:

jobs:
check_code:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
# pip install -r requirements.txt

- name: Run Ruff
run: ruff check .

24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
__pycache__/
*.pyc
.env
.token
cogs/__pycache__/
cogs/*.pyc
.vscode/
.idea/
*$py.class
*.py[cod]
.venv

.genai_token
phrases.json
context.txt
currency_cache.json
last_currency_update.txt
system_message.txt
uptime.json
config.ini
last_update.txt
settings.py
scripts/
llm_context.json
Binary file modified README.md
Binary file not shown.
8 changes: 8 additions & 0 deletions docs/EN/cog_hot_reloading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# About quickly reloading cogs

On Discord, this is done using commands like `/reload_cogs`, but there are a few things to keep in mind:
- If the cog you’re reloading imports modules from `core` or `modules`, and the code in `core` or `modules` has been changed, the reloaded cog won’t receive the new changes from those modules. You will need to reload the entire bot. [^1]

> In other words, **changes to the `core` and `modules` are not pulled in** by reloading cogs.

[^1]: We haven’t yet written a system to fix this, as it is overkill for the current scale of the project.
70 changes: 70 additions & 0 deletions docs/EN/walkthroughs/multi-server-phrases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Multi-Server Phrases Support

The phrases system (`phrases.json`) supports separate text sets for each Discord server. This allows the bot to respond in different languages or styles depending on the server.

## `phrases.json` Structure

The top-level key is a server ID (string) or `"global"` for phrases without a server context:

```json
{
"global": {
"main": { "token_file_not_found": "..." },
"utils": { "on_connected": "...", "on_resumed": "..." }
},
"123456789012345678": {
"errors": { "cooldown_message": "...", "access_denied": "..." },
"utils": { "ping_response": "..." }
}
}
```

## Usage

Phrases are accessed via the `get_phrases()` function from `core/utils.py`:

```python
from core.utils import get_phrases

# Phrases for a specific server
text = get_phrases(guild_id).get("errors", {}).get("cooldown_message", "Fallback text")

# Global phrases (no server context)
text = get_phrases().get("main", {}).get("token_file_not_found", "Fallback text")
```

| Call | Result |
|---|---|
| `get_phrases()` | Phrases from the `"global"` key |
| `get_phrases(guild_id)` | Phrases for server `str(guild_id)` |

If the key is not found, an empty `{}` is returned. All `.get()` calls with fallback values continue to work as before.

## Choosing the Right Context

### Guild-context — use `get_phrases(guild_id)`

When the code has access to a guild object — slash commands, prefix commands, event listeners like `on_message` or `on_command_error`:

```python
text = get_phrases(inter.guild.id).get("utils", {}).get("ping_response", "...")
text = get_phrases(message.guild.id).get("olive", {}).get("system_instruction", "...")
```

### Channel-context — use `get_phrases(channel.guild.id)`

When the bot sends a message to a channel but doesn't have a direct guild reference — error handlers, startup notifications:

```python
channel = await self.bot.get_or_fetch_channel(channel_id)
text = get_phrases(channel.guild.id).get("category", {}).get("key", "...")
```

### Global-context — use `get_phrases()`

When there is no server context at all — `print()` calls, embed generators that write to a shared cache, module initialization:

```python
raw_embed_data = get_phrases().get("uptime_embed", {}).get("embed_data", {...})
text = get_phrases().get("utils", {}).get("on_connected", "Bot connected.")
```
161 changes: 161 additions & 0 deletions docs/UK/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Архітектура Olive Bot

## Загальна схема

```
main.py --> core/bot.py (OliveBot)
├── core/cache.py (глобальний стан)
├── core/utils.py (утиліти + фрази)
├── modules/ (зовнішні інтеграції)
└── cogs/ (модулі бота)
├── statistic_message_loop.py (головний цикл виводу)
├── hosting_embed.py
├── currency_embed.py
├── battery_embed.py
├── uptime_embed.py
├── active_cogs_embed.py
├── olive.py (AI-асистент)
├── chatops.py (git pull, reload, debug)
├── errors.py (обробка помилок)
├── phrases_tools.py (редагування фраз)
└── utils.py (ping, статистика, події)
```

## Точка входу — `main.py`

1. Створює екземпляр `OliveBot` з інтентами та списком тестових серверів.
2. Завантажує фрази з `phrases.json` через `core.utils.load_phrases()`.
3. Автоматично знаходить та завантажує всі `.py`-файли з директорії `cogs/`.
4. Читає токен бота з файлу (шлях задано в `settings.py`) і запускає бота.
5. У `on_ready` ініціалізує `configLock`, перевіряє час з останнього запуску через `config.ini` та надсилає повідомлення про старт у канал `bot_news`.

## Ядро (`core/`)

### `core/bot.py` — OliveBot

Наслідує `commands.Bot` з disnake. Перевизначає:
- `load_extension()` — при завантаженні когу записує час завантаження в `cache.active_cogs_list`.
- `unload_extension()` — при вивантаженні видаляє ког зі списку активних.
Створює:
- `get_or_fetch_channel()` — спочатку шукає канал у кеші disnake, потім робить запит до API, якщо нема кешу.

### `core/cache.py` — Глобальний стан

Набір глобальних змінних, які використовуються як спільний стан між когами:

| Змінна | Призначення |
|--------|-------------|
| `embeds_to_send` | Словник embed-об'єктів, які `statistic_message_loop` відправляє в канали. |
| `configLock` | `asyncio.Lock` для безпечного доступу до `config.ini`. |
| `llm_client` | Екземпляр `LLMClient` для AI-асистента. |
| `active_cogs_list` | Словник `{ім'я_когу: час_завантаження}`. |
| `_phrases` | Завантажені фрази з `phrases.json`. |

### `core/utils.py` — Утиліти

- `u_decline(number, forms)` — відмінювання українських слів після числа (1 година, 2 години, 5 годин).
- `format_embed_data(data, **kwargs)` — рекурсивно підставляє значення у шаблони embed-даних (словники, списки, рядки).

- `get_phrases(guild_id)` — повертає фрази для конкретного сервера або глобальні.
- `load_phrases()` — завантажує `phrases.json` у `cache._phrases`.

## Коги (`cogs/`) — Модулі бота

### Принцип роботи embed-когів

Більшість когів щодо embed працюють за однаковим шляхом:

1. **Цикл збору даних** (наприклад, `@tasks.loop(seconds=10)`) збирає інформацію (RAM, курс валют, батарея тощо).
2. Бере шаблон embed з фраз: `get_phrases().get("назва_когу", {}).get("ключ_embed", {фолбек})`.
3. Форматує шаблон через `format_embed_data()` з актуальними даними.
4. Записує готовий `disnake.Embed` у `core.cache.embeds_to_send["ключ"]`.

Самі embed-коги не надсилають повідомлення — вони лише оновлюють кеш.

### `statistic_message_loop.py` — Головний цикл виводу ембедів

Редагує "вічне" повідомлення в Discord:

1. **`before_main_loop`**: при старті очищує зазначені канали (`channels["statistic"]`), надсилає початкове повідомлення і зберігає посилання на нього.
2. **`main_loop`** (кожні 10 сек): збирає всі embed з `cache.embeds_to_send`, фільтрує за `embeds_blacklist` для кожного сервера, порівнює з попередніми (щоб не робити зайвих запитів), і редагує повідомлення.
3. **Обробка помилок**: при HTTP 5xx або мережевих помилках використовує exponential backoff (5с -> 10с -> ... -> 150с макс).


### `olive.py` — AI-асистент

- Використовує Google GenAI через `modules/llm_client.py`.
- Зберігає контекст розмови для кожного сервера у `llm_context.json`.
- Вмикається/вимикається через `/turn_olive`.

### `chatops.py` — Операційні команди

Доступні лише власнику бота:
- `/git_pull` — виконує `git pull` на хості з валідацією вхідних параметрів.
- `/reload_cogs` — перезавантажує один або всі коги.
- `/unload_cogs` — вивантажує ког(-и).
- `/turn_debug_mode` — перемикає debug-режим у `config.ini`.

### `errors.py` — Загальна обробка помилок

- `CommandOnCooldown` — повідомляє про cooldown + анти-флуд: якщо користувач надсилає команди частіше ніж раз на 4 секунди — кік з сервера.
- `NotOwner` / `MissingPermissions` — повідомлення про відсутність прав.
- Решта помилок прокидаються назовні.

## Модулі (`modules/`)

### `modules/llm_client.py`

Обгортка над Google GenAI SDK:
- Читає API-токен з `.genai_token` або змінної середовища `GENAI_API_KEY`.
- Назва моделі береться з фраз (`phrases.json`).

## Конфігурація

### `settings.py`

Статичні налаштування, які діють після перезапуску бота:
- Шляхи до файлів (`cogs`, `token_file`, `config.ini`).
- ID каналів та серверів.
- Увімкнення/вимкнення embed-модулів (`enable_*_embed`).

### `config.ini`

Динамічні налаштування, що змінюються під час роботи бота:
- `debug_mode` — якщо увімкнено, пропускаються затримки при старті та повідомлення про запуск.
- `last_run_time` — час останнього запуску (для захисту від частих перезапусків).

Доступ до `config.ini` захищений через `asyncio.Lock` (`cache.configLock`).

### `phrases.json` — Система фраз

Більшість текстових повідомлень бота (embed-шаблони, відповіді на команди, системні повідомлення) зберігаються у `phrases.json`. Структура:

```json
{
"global": {
"назва_когу": {
"ключ": "значення з {форматуванням} для всіх серверів"
}
},
"ID_сервера": {
"назва_когу": {
"ключ": "значення для конкретного сервера"
}
}
}
```

Фрази можна редагувати через команду `/edit_phrases` без перезапуску бота (`/reload_phrases`).

## Потік даних

```
phrases.json -> cache._phrases -> get_phrases() -> коги

settings.py -> конфігурація когів при завантаженні

Embed-коги -> cache.embeds_to_send -> statistic_message_loop -> Discord канали

config.ini <- -> cache.configLock <- -> main.py / chatops / utils
```
8 changes: 8 additions & 0 deletions docs/UK/cog_hot_reloading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Про швидкий перезапуск когів

Через Discord це робиться командами типу `/reload_cogs`, однак слід урахувати дещо:
- Якщо ког, який ви перезавантажуєте, містить імпорт модулів з `core` або `modules`, і при цьому код у `core` або `modules` був змінений, то перезавантажений ког не отримає нових змін з цих модулів. Треба буде перезавантажувати всього бота. [^1]

> Тобто, **зміни в ядрі та модулях не підтягуються** через перезавантаження когів.

[^1]: Ми ще не написали систему для виправлення цього, оскільки це надлишково для тепершінього масштабу проєкту.
34 changes: 34 additions & 0 deletions docs/UK/setup-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Setup Instructions
1. Склонувати репозиторій та перейти у його директорію:
```bash
git clone https://github.com/oleh-devlab/olive.git
cd olive
```
2. Створити віртуальне середовище Python та активувати його:
```bash
python -m venv .venv

# Windows
.venv\Scripts\activate
# Linux/MacOS
source .venv/bin/activate
```
3. Перемістити `settings.py.example` у каталог джерела (`src/`, або таким чином, щоби був на одному рівні з `main.py`).
4. Перейменувати `settings.py.example` в `settings.py` та заповнити необхідні поля (див. коментарі в файлі).
5. Файли з токенами створити у каталозі джерела (`src/`, в одній теці з `main.py` та `settings.py`) відповідно до їхніх імен у `settings.py`.
- Можете не вставляти токени для модулів, які вимкнені (див. `settings.py`).
6. Переконатися, що використані шляхи до токенів (див. `settings.py`) є у `.gitignore`.
7. Встановити залежності.
- Залежності з невикористовуваних модулів можна не встановлювати. Перегляньте `settings.py` для вимкнення непотрібних модулів і відредагуйте `requirements.txt` відповідно.
```bash
pip install -r requirements.txt
```
- Якщо ви редагували `requirements.txt` для пропуску залежностей, після встановлення рекомендується повернути його до початкового стану, щоб уникнути конфліктів при роботі git.
- *Порада:* Ви можете швидко повернути файл до попереднього стану, виконавши команду `git checkout -- requirements.txt` або `git restore requirements.txt`.
8. *(опційно)* Заповнити `phrases.json`.
- Основна документація про phrases.json поки що відсутня, тому треба дивитися код та заповнювати те, що ви хочете змінити. Але ви можете переглянути документацію мультисерверного формату: [Англійською](/docs/EN/walkthroughs/multi-server-phrases.md) | [Українською](/docs/UK/walkthroughs/multi-server-phrases.md).
9. Запустити бота (рекомендовано з каталогу `src`, або того, у якому `main.py`):
```bash
cd src
python main.py
```
Loading
Loading