● v2.0 · Multi-Guild Bot Architecture

Soyuz Bot — Discord ekosistemasi

Ikkita Discord server uchun yagona bot: Soyuz (umumiy ochiq) va Flame (GTA 5 RP, ichida oila tier'i). Zayavka tizimi dinamik savollar bilan, auto-rol, voice + GTA activity tracking, to'liq admin panel.

discord.schedule.uz · 2026-05-18 · single bot, 2 guilds, 1 admin panel

01 Ikkita Discord server

Bitta bot 2 ta server'ga ulanadi. Flame ichida ierarxiya bor: oddiy a'zo → tasdiqlangan o'yinchi → oila (yaqin doiradagi faol a'zolar).

S
PUBLIC GUILD

Soyuz

Umumiy ochiq server. Asosiy funksiya — auto-rol tugmasi. Yangi a'zo qoidalarni o'qib tugmani bossa, @Verified roli beriladi va barcha kanallarni ko'radi.

  • Verification button
  • Welcome message
  • Basic moderation
  • Voice activity tracking (umumiy)
F
GTA 5 RP GUILD

Flame (oila tier ichida)

GTA 5 RP loyihasi. Zayavka tizimi dinamik savollar bilan, qabul/rad, GTA o'yin vaqti monitoringi. Oila roli — eng faol va ishonchli a'zolar uchun ichki tier.

  • Dynamic zayavka forma
  • Admin review (accept/decline)
  • GTA V / FiveM playtime tracking
  • Oila tier — manual yoki auto-promotion

Flame ichidagi rol ierarxiyasi

┌──────────────────────────────────────────────────────────────┐ │ @Newcomer ───/zayavka───▶ [pending] │ │ │ │ │ │ │ ▼ │ │ │ admin review │ │ │ accept │ reject │ │ │ ▼ │ │ │ @Player (RP'da o'ynay oladi) │ │ │ │ │ │ │ [faollik kerakli darajada] │ │ │ ▼ │ │ └────────────────────▶ @Oila (ichki yaqin doira) │ │ │ │ │ ▼ │ │ @Admin / @Moderator │ └──────────────────────────────────────────────────────────────┘

02 Asosiy imkoniyatlar

01
Dinamik zayavka Flame Savollar admin tomonidan panelda boshqariladi. Qo'shish/o'chirish/tartib o'zgartirish — bot kodi o'zgarmaydi. Qator tiplar: text, long_text, number, choice (select), date.
02
Review pipeline Zayavka → maxsus #applications kanaliga embed → 2 ta tugma (Qabul/Rad) → admin bosadi → rol beriladi yoki DM bilan sabab yuboriladi.
03
Auto-rol tugmasi Soyuz Qoidalar kanalida persistent message — tugma bossa @Verified rol beriladi. Bot restartdan keyin ham ishlaydi (custom_id + add_view).
04
Voice activity tracking Soyuz Flame on_voice_state_update orqali har voice join/leave qayd etiladi. Daily/weekly/monthly leaderboard. AFK kanal hisobga olinmaydi.
04b
Oila promotion Flame Faol @Player'larni @Oila ga ko'tarish. Avto-promotion kriteriyalari: oxirgi 30 kun ichida X soat voice + Y soat GTA + admin tasdig'i. Yoki to'liq manual — admin paneldan tugma bilan.
05
GTA 5 RP playtime Flame on_presence_update orqali user.activities'ni kuzatadi. Grand Theft Auto V, FiveM, RAGE Multiplayer nomli aktivlik boshlanganda timer start, tugaganda stop.
06
Admin web panel discord.schedule.uz/admin — Discord OAuth orqali kirish. Zayavkalar, statistika, leaderboard, savol konstruktori, log filtrlash, eksport (CSV/JSON).
07
Audit log boshqaruv Har bir muhim hodisa DBga yoziladi: kim zayavka berdi, kim tasdiqladi/rad etdi, qachon rol berildi/olindi. Filter: sana, guild, user, action turi.

03 Yuqori darajadagi arxitektura

Soyuz GuildPublic · Verify · Voice
Flame GuildGTA RP · Zayavka · Oila tier
│                               │
└─────────────────────┘

╔════════════════════════════════════╗
║   Soyuz Bot   (discord.py)  ║
╚════════════════════════════════════╝
┌──────────────┬──────────────┬──────────────┐
PostgreSQLstate · audit
Rediscache · rate-limit
FastAPI admindiscord.schedule.uz/admin

04 Flame zayavka — dinamik savollar

Adminlar panel orqali savollar to'plamini boshqaradi. Foydalanuvchi /zayavka chaqirsa, bot DBdan aktiv savollarni o'qib modal yasaydi.

Discord tomonida ko'rinish

#applications · Flame
🔥 Flame BotBugun · 14:32
Yangi zayavka #247
Foydalanuvchi: @diablo_007 (392841029384029384)
Personaj ismi
Marco Rodriguez
Yosh (RL)
21
RP tajriba
2 yil GTA 5 RP, EclipseRL
Telegram
@diablo_marco
Personaj bio (qisqacha)
Bola Los Santos da tug'ilgan, otasi mexanik bo'lgan...
Ovozlar (3 admin)
✓ Qabul: 2 ✗ Rad: 1 @john, @ali (qabul) · @bek (rad)
"Yakunlash" tugmasi ≥1 ovozdan keyin paydo bo'ladi. Faqat @Admin bosa oladi. Majority qoidasi: qabul > rad → approve, aks → reject.

Savollar konstruktori (admin panel)

POST /admin/questions
{
  "guild_id": "flame",
  "questions": [
    { "order": 1, "type": "text",      "label": "Personaj ismi", "required": true, "max_length": 80 },
    { "order": 2, "type": "number",    "label": "Yosh (RL)", "min": 16, "max": 60 },
    { "order": 3, "type": "long_text", "label": "RP tajriba (kerakli serverlar, vaqt)", "required": true },
    { "order": 4, "type": "choice",    "label": "Frakciya", "options": ["LSPD", "EMS", "Mafia", "Civilian"] },
    { "order": 5, "type": "long_text", "label": "Personaj bio (qisqacha)", "max_length": 500 }
  ]
}

Discord modal cheklovi: max 5 input. Agar savollar ko'p bo'lsa, ketma-ket bir nechta modal ishlatiladi (wizard pattern) yoki choice tipiga select menyu ko'rsatiladi.

04b Vote yig'ish + yakunlash mexanizmi

Bir kishi qaror qilmaydi — ko'p admin ovoz beradi. Har bir tugma bosishi DB'ga yoziladi va embed real-time yangilanadi.

Qoidalar

DB sxema — votes

CREATE TABLE application_votes (
  id              SERIAL PRIMARY KEY,
  application_id  INT REFERENCES applications(id) ON DELETE CASCADE,
  voter_id        BIGINT NOT NULL,
  vote            TEXT   NOT NULL CHECK (vote IN ('approve','reject')),
  created_at      TIMESTAMPTZ DEFAULT now(),
  UNIQUE(application_id, voter_id)  -- 1 admin = 1 ovoz
);

Handler logikasi

class VoteView(ui.View):
    def __init__(self, app_id: int, has_votes: bool = False):
        super().__init__(timeout=None)
        self.app_id = app_id
        # "Yakunlash" tugmasini faqat ovoz bo'lsa qo'shamiz
        if has_votes:
            self.add_item(FinishButton(app_id))

    @ui.button(label="✓ Qabul", style=discord.ButtonStyle.success,
               custom_id="vote:approve")
    async def vote_approve(self, interaction, button):
        if MOD_ROLE_ID not in [r.id for r in interaction.user.roles]:
            return await interaction.response.send_message(
                "Ruxsat yo'q.", ephemeral=True)
        await db.upsert_vote(self.app_id, interaction.user.id, "approve")
        await self._refresh(interaction)

    @ui.button(label="✗ Rad", style=discord.ButtonStyle.danger,
               custom_id="vote:reject")
    async def vote_reject(self, interaction, button):
        if MOD_ROLE_ID not in [r.id for r in interaction.user.roles]:
            return await interaction.response.send_message(
                "Ruxsat yo'q.", ephemeral=True)
        await db.upsert_vote(self.app_id, interaction.user.id, "reject")
        await self._refresh(interaction)

    async def _refresh(self, interaction):
        votes = await db.get_votes(self.app_id)
        embed = build_application_embed(self.app_id, votes)
        # Yakunlash tugmasi shartli
        view = VoteView(self.app_id, has_votes=len(votes) > 0)
        await interaction.response.edit_message(embed=embed, view=view)


class FinishButton(ui.Button):
    def __init__(self, app_id):
        super().__init__(label="🏁 Yakunlash",
                         style=discord.ButtonStyle.primary,
                         custom_id=f"vote:finish:{app_id}")
        self.app_id = app_id

    async def callback(self, interaction):
        if ADMIN_ROLE_ID not in [r.id for r in interaction.user.roles]:
            return await interaction.response.send_message(
                "Yakunlash faqat Adminlar uchun.", ephemeral=True)

        votes = await db.get_votes(self.app_id)
        approve = sum(1 for v in votes if v.vote == "approve")
        reject  = sum(1 for v in votes if v.vote == "reject")

        if approve > reject:
            await apply_approval(self.app_id, finalizer=interaction.user.id)
            status, color = "Qabul qilindi ✓", discord.Color.green()
        else:
            await apply_rejection(self.app_id, finalizer=interaction.user.id)
            status, color = "Rad etildi ✗", discord.Color.red()

        embed = build_final_embed(self.app_id, status, color,
                                  approve, reject, interaction.user)
        await interaction.response.edit_message(embed=embed, view=None)

05 Soyuz — auto-rol tugmasi

Qoidalar kanalida bitta xabar: "Qoidalarni o'qib chiqdim". Tugma bossa rol beriladi.

#qoidalar · Soyuz
📘 Soyuz Bot
Soyuz Discord serverga xush kelibsiz!
Quyidagi tugmani bosib qoidalarga rozilik bildiring. Sizga @Verified roli beriladi va barcha kanallarni ko'ra olasiz.
class VerifyView(ui.View):
    def __init__(self):
        super().__init__(timeout=None)  # persistent

    @ui.button(label="✓ Roziman, rol bering",
               style=discord.ButtonStyle.success,
               custom_id="soyuz:verify")
    async def verify(self, interaction, button):
        role = interaction.guild.get_role(VERIFIED_ROLE_ID)
        if role in interaction.user.roles:
            return await interaction.response.send_message(
                "Sizda allaqachon rol bor.", ephemeral=True)

        await interaction.user.add_roles(role, reason="Self-verify button")
        await db.log_audit("verify", interaction.user.id, interaction.guild.id)
        await interaction.response.send_message(
            f"✅ {role.mention} roli berildi. Xush kelibsiz!",
            ephemeral=True)

05b Channel routing — har xabar o'z kanaliga

Bot turli hodisalarni turli kanalga yo'naltiradi. Mapping DB'da, admin paneldan tahrirlanadi.

HodisaKanal (default)Misol
application.new#applicationsZayavka kelgan embed + tugmalar
application.result#announcements"@diablo_007 Flame'ga qabul qilindi"
role.grant#audit-log"@user'ga @Player roli berildi by @admin"
voice.daily_summary#activity-logKunlik voice top-10 (har kuni 23:55)
gta.session#gta-activity"@user 2 soat 14 daqiqa GTA'da o'ynadi"
verify.success#welcome"Soyuz'ga xush kelibsiz, @user!"
oila.promotion#oila"@user @Oila tier'iga ko'tarildi"
moderation.warn#mod-logWarn/kick/ban hodisalari
bot.error#bot-errorsStacktrace, runtime exception (faqat admin ko'radi)

Mapping jadvali

CREATE TABLE channel_routes (
  id          SERIAL PRIMARY KEY,
  guild_id    BIGINT REFERENCES guilds(id) ON DELETE CASCADE,
  event       TEXT   NOT NULL,        -- application.new | role.grant | ...
  channel_id  BIGINT NOT NULL,
  enabled     BOOLEAN DEFAULT TRUE,
  template    TEXT,                   -- ixtiyoriy custom message template
  UNIQUE(guild_id, event)
);

Universal router

async def dispatch(guild_id: int, event: str, **payload):
    route = await db.get_route(guild_id, event)
    if not route or not route.enabled:
        return
    channel = bot.get_channel(route.channel_id)
    if not channel:
        return
    embed = render_event(event, payload, template=route.template)
    await channel.send(embed=embed)
    await db.log_audit(guild_id, event, payload)

# Misol:
await dispatch(FLAME_GUILD_ID, "application.result",
               user_id=user.id, status="approved",
               finalizer=admin.id)

Admin paneldan /admin/<guild>/routing sahifasi: har hodisa uchun kanal select, on/off toggle, template tahrirlash.

06 Oila tier — Flame ichki doirasi

@Player rolidan keyingi bosqich. Auto-promotion yoki manual berish.

Auto-promotion kriteriyalari (sozlanadi)

OILA_CRITERIA = {
    "voice_hours_30d":   20,    # so'nggi 30 kun — 20 soat voice
    "gta_hours_30d":     15,    # so'nggi 30 kun — 15 soat GTA/FiveM
    "min_days_in_guild": 30,    # serverda kamida 30 kun
    "no_warnings_30d":   True,  # oxirgi 30 kunda warn bo'lmasligi
}

Tekshirish — har kuni 03:00 da cron

@tasks.loop(time=time(3, 0))
async def check_oila_promotion():
    candidates = await db.find_oila_candidates(OILA_CRITERIA)
    for member_id in candidates:
        # Adminga DM / panel notification — manual tasdiqlash
        await notify_admins_about_candidate(member_id)
        # Yoki: agar auto_promote=True bo'lsa to'g'ridan-to'g'ri rol bering

Manual promotion — admin panel'dan

POST /admin/flame/users/{user_id}/promote-oila
→ bot.add_roles(member, OILA_ROLE)
→ DM: "Tabriklaymiz! Sizga @Oila roli berildi."
→ audit_logs: action=role.grant, target=user_id, payload={tier: "oila"}

07 Activity tracking

Voice — kim qancha vaqt o'tirdi

@bot.event
async def on_voice_state_update(member, before, after):
    if member.bot: return

    # Channel join
    if before.channel is None and after.channel is not None:
        if after.channel.id != AFK_CHANNEL_ID:
            await db.voice_session_start(
                user_id=member.id,
                guild_id=member.guild.id,
                channel_id=after.channel.id,
            )

    # Channel leave
    elif before.channel is not None and after.channel is None:
        await db.voice_session_end(user_id=member.id, guild_id=member.guild.id)

    # Channel switch — close + reopen
    elif before.channel != after.channel:
        await db.voice_session_end(user_id=member.id, guild_id=member.guild.id)
        if after.channel.id != AFK_CHANNEL_ID:
            await db.voice_session_start(member.id, member.guild.id, after.channel.id)

GTA 5 RP — kim qancha o'ynadi

GTA_NAMES = {"Grand Theft Auto V", "GTA V", "FiveM", "RAGE Multiplayer"}

@bot.event
async def on_presence_update(before, after):
    if after.bot: return

    before_gta = any(a.name in GTA_NAMES for a in before.activities)
    after_gta  = any(a.name in GTA_NAMES for a in after.activities)

    if not before_gta and after_gta:
        await db.game_session_start(after.id, after.guild.id, "GTA5")
    elif before_gta and not after_gta:
        await db.game_session_end(after.id, after.guild.id, "GTA5")

Muhim: presences privileged intent yoqilgan bo'lishi shart (Developer Portal → Bot → Privileged Intents → Presence Intent).

Bot restart bo'lganda — open session'larni yopish

@bot.event
async def on_ready():
    # Restart paytida ochiq qolgan voice/game sessiyalarni close qil
    await db.close_orphan_sessions(bot.user.id)
    # Aktiv voice'da turganlar uchun yangi sessiya boshlash
    for guild in bot.guilds:
        for vc in guild.voice_channels:
            if vc.id == AFK_CHANNEL_ID: continue
            for member in vc.members:
                if not member.bot:
                    await db.voice_session_start(member.id, guild.id, vc.id)

08 Admin panel — discord.schedule.uz/admin

Ikki xil kirish usuli: oddiy login/parol (asosiy) yoki Discord OAuth (ixtiyoriy). Faqat tasdiqlangan adminlar.

🔐 Login/parol auth

  • Email + parol orqali kirish
  • Parollar bcrypt (cost=12) bilan hash qilinadi
  • Session JWT yoki HttpOnly cookie
  • 2FA (TOTP) — ixtiyoriy
  • Brute-force himoyasi: 5 ta noto'g'ri urinishdan keyin 15 daqiqa lock
  • Birinchi admin python -m web.create_admin komandasi orqali

🎮 Discord OAuth (ixtiyoriy)

  • Discord login bilan tezkor kirish
  • Scope: identify guilds.members.read
  • Avtomatik rol tekshiruvi (@Admin guildda bormi?)
  • Login/parol bilan akkauntga bog'lanadi

users jadvali

CREATE TABLE users (
  id              SERIAL PRIMARY KEY,
  email           TEXT UNIQUE NOT NULL,
  password_hash   TEXT NOT NULL,       -- bcrypt
  display_name    TEXT NOT NULL,
  role            TEXT NOT NULL,       -- admin | moderator | viewer
  discord_id      BIGINT UNIQUE,       -- ixtiyoriy bog'lanish
  totp_secret     TEXT,                -- 2FA secret (encrypted)
  is_active       BOOLEAN DEFAULT TRUE,
  last_login_at   TIMESTAMPTZ,
  failed_attempts INT DEFAULT 0,
  locked_until    TIMESTAMPTZ,
  created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE sessions (
  token        TEXT PRIMARY KEY,
  user_id      INT REFERENCES users(id) ON DELETE CASCADE,
  ip           INET,
  user_agent   TEXT,
  expires_at   TIMESTAMPTZ NOT NULL,
  created_at   TIMESTAMPTZ DEFAULT now()
);
discord.schedule.uz/admin/flame

Pending zayavkalar

12
Kutilmoqda
+3 bugun
147
Qabul qilingan
+8 / hafta
34
Rad etilgan
19%
3.2h
O'rtacha ko'rib chiqish
SLA: 24h
#FoydalanuvchiPersonajVaqt
D
@diablo_007
392841029384029384
Marco Rodriguez
14:32 (2m)
A
@asadbek
192847291847291837
James Wilson
13:14 (1h)
S
@sherzod
102938472918374028
Tony Esposito
11:08 (3h)

Panel sahifalari

RouteMaqsad
/adminDashboard — barcha guild'lar bo'yicha umumiy statistika
/admin/flame/applicationsZayavkalar ro'yxati, filter, accept/decline web orqali ham
/admin/flame/questionsSavollar konstruktori — drag-drop tartib, qo'shish, o'chirish
/admin/<guild>/voiceVoice leaderboard (kun/hafta/oy)
/admin/flame/gtaGTA playtime — kim qancha o'ynadi
/admin/<guild>/logsAudit log — filter: action, user, sana
/admin/<guild>/settingsRollar, kanallar, intent toggle
/admin/exportCSV/JSON eksport

09 Ma'lumotlar bazasi sxemasi

-- Guild konfiguratsiyasi
CREATE TABLE guilds (
  id              BIGINT PRIMARY KEY,             -- discord guild ID
  name            TEXT   NOT NULL,
  type            TEXT   NOT NULL,                -- soyuz | flame
  review_channel  BIGINT,                         -- #applications
  verified_role   BIGINT,                         -- auto-rol
  mod_role        BIGINT,                         -- kim accept qila oladi
  afk_channel     BIGINT,                         -- voice tracking exclude
  settings        JSONB DEFAULT '{}'::jsonb,
  created_at      TIMESTAMPTZ DEFAULT now()
);

-- Dinamik savollar
CREATE TABLE application_questions (
  id          SERIAL PRIMARY KEY,
  guild_id    BIGINT REFERENCES guilds(id) ON DELETE CASCADE,
  ord         INT NOT NULL,
  type        TEXT NOT NULL,           -- text|long_text|number|choice|date
  label       TEXT NOT NULL,
  required    BOOLEAN DEFAULT TRUE,
  min_len     INT,  max_len  INT,
  min_val     INT,  max_val  INT,
  options     JSONB,                   -- ["LSPD","EMS",...]
  active      BOOLEAN DEFAULT TRUE,
  UNIQUE(guild_id, ord)
);

-- Zayavkalar
CREATE TABLE applications (
  id              SERIAL PRIMARY KEY,
  guild_id        BIGINT REFERENCES guilds(id),
  user_id         BIGINT NOT NULL,
  username        TEXT NOT NULL,
  status          TEXT NOT NULL DEFAULT 'pending',  -- pending|approved|rejected
  answers         JSONB NOT NULL,                   -- {q_id: value}
  reviewed_by     BIGINT,
  reviewer_note   TEXT,
  reject_reason   TEXT,
  created_at      TIMESTAMPTZ DEFAULT now(),
  reviewed_at     TIMESTAMPTZ
);
CREATE INDEX idx_app_status ON applications(guild_id, status);
CREATE INDEX idx_app_user   ON applications(user_id);

-- Voice sessiyalar
CREATE TABLE voice_sessions (
  id          BIGSERIAL PRIMARY KEY,
  user_id     BIGINT NOT NULL,
  guild_id    BIGINT NOT NULL,
  channel_id  BIGINT NOT NULL,
  started_at  TIMESTAMPTZ DEFAULT now(),
  ended_at    TIMESTAMPTZ,
  duration_s  INT GENERATED ALWAYS AS
              (EXTRACT(EPOCH FROM (ended_at - started_at))::INT) STORED
);
CREATE INDEX idx_voice_user_day ON voice_sessions(user_id, started_at);

-- GTA / game sessiyalar
CREATE TABLE game_sessions (
  id          BIGSERIAL PRIMARY KEY,
  user_id     BIGINT NOT NULL,
  guild_id    BIGINT NOT NULL,
  game        TEXT   NOT NULL,         -- "GTA5", "FiveM", ...
  started_at  TIMESTAMPTZ DEFAULT now(),
  ended_at    TIMESTAMPTZ,
  duration_s  INT
);
CREATE INDEX idx_game_user ON game_sessions(user_id, game);

-- Auto-rol tugmalari (Soyuz)
CREATE TABLE auto_role_buttons (
  id          SERIAL PRIMARY KEY,
  guild_id    BIGINT REFERENCES guilds(id),
  channel_id  BIGINT NOT NULL,
  message_id  BIGINT NOT NULL,
  custom_id   TEXT   NOT NULL UNIQUE,
  role_id     BIGINT NOT NULL,
  label       TEXT
);

-- Audit log
CREATE TABLE audit_logs (
  id          BIGSERIAL PRIMARY KEY,
  guild_id    BIGINT,
  actor_id    BIGINT,
  target_id   BIGINT,
  action      TEXT NOT NULL,   -- application.submit | application.approve | role.grant | voice.session ...
  payload     JSONB,
  created_at  TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_audit_time ON audit_logs(created_at DESC);
CREATE INDEX idx_audit_guild ON audit_logs(guild_id, action, created_at DESC);

10 Texnologiya stack

QatlamTanlovSabab
TilPython 3.12discord.py + asyncio + FastAPI yagona ekosistem
Discord SDKdiscord.py 2.4+slash, modal, button, view persistence
DBPostgreSQL 16JSONB savollar + audit log uchun zo'r
ORM/Driverasyncpg + SQLAlchemy 2 (async)tip-xavfsiz, fast
CacheRediscooldown, rate-limit, session
WebFastAPI + Jinja2 + HTMXSPA shovqinisiz interaktiv
AuthDiscord OAuth2panel kirishi rol orqali
Processsystemd × 2dsbot + dsbot-web
Reverse proxynginx + certbotHTTPS, static

11 Deploy va sozlash

Loyiha tuzilmasi

dsbot/
├── bot/
│   ├── main.py                 # entry — bot.run()
│   ├── config.py               # .env loader
│   ├── db.py                   # asyncpg pool
│   ├── cogs/
│   │   ├── applications.py     # /zayavka — dinamik modal
│   │   ├── verification.py     # Soyuz auto-rol
│   │   ├── voice_tracker.py    # on_voice_state_update
│   │   ├── game_tracker.py     # on_presence_update
│   │   └── admin.py            # /sync, /addquestion (fallback)
│   ├── views/
│   │   ├── verify_view.py
│   │   ├── application_modal.py
│   │   └── review_view.py
│   └── utils/
│       ├── permissions.py
│       └── embeds.py
├── web/
│   ├── main.py                 # FastAPI app
│   ├── routers/{applications,activity,questions,logs}.py
│   ├── templates/              # Jinja2 + HTMX
│   └── static/
├── migrations/                 # alembic
├── .env
├── requirements.txt
└── systemd/
    ├── dsbot.service
    └── dsbot-web.service

.env

DISCORD_TOKEN=...
DATABASE_URL=postgresql://dsbot:***@localhost/dsbot
REDIS_URL=redis://localhost:6379/3
OAUTH_CLIENT_ID=...
OAUTH_CLIENT_SECRET=...
OAUTH_REDIRECT=https://discord.schedule.uz/admin/auth/callback
SOYUZ_GUILD_ID=...
FLAME_GUILD_ID=...
FLAME_OILA_ROLE_ID=...
SESSION_SECRET=...

systemd — bot

# /etc/systemd/system/dsbot.service
[Unit]
Description=Soyuz Discord Bot
After=network.target postgresql.service

[Service]
Type=simple
User=dsbot
WorkingDirectory=/opt/dsbot
EnvironmentFile=/opt/dsbot/.env
ExecStart=/opt/dsbot/.venv/bin/python -m bot.main
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

systemd — web admin

# /etc/systemd/system/dsbot-web.service
[Unit]
Description=Soyuz Bot Admin Panel
After=network.target

[Service]
Type=simple
User=dsbot
WorkingDirectory=/opt/dsbot
EnvironmentFile=/opt/dsbot/.env
ExecStart=/opt/dsbot/.venv/bin/uvicorn web.main:app \
          --host 127.0.0.1 --port 8090 --workers 2
Restart=always

[Install]
WantedBy=multi-user.target

nginx — discord.schedule.uz

server {
    listen 443 ssl http2;
    server_name discord.schedule.uz;

    ssl_certificate     /etc/letsencrypt/live/discord.schedule.uz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/discord.schedule.uz/privkey.pem;

    # Admin panel
    location /admin/ {
        proxy_pass http://127.0.0.1:8090/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Bu hujjat (static landing)
    location / {
        root /var/www/discord.schedule.uz;
        try_files $uri $uri/ /index.html;
    }
}

12 Xavfsizlik va o'rnatish nyuanslari

Privileged Intents

Developer Portal → Bot → quyidagilarni yoqing:

  • Server Members — yangi a'zo, rol berish
  • Presence Intent — GTA aktivligini kuzatish uchun MAJBURIY

⚠ 100+ guild bo'lsa Discord'dan verifikatsiya so'raydi. Sizda 3 ta — muammosiz.

Rol ierarxiyasi

Bot roli quyidagilardan yuqorida bo'lishi shart:

  • @Verified (Soyuz)
  • @Player (Flame, zayavka qabul qilinganda)

Aks holda: discord.Forbidden: Missing Permissions

Permission tekshiruvi

Har button handler boshida:

if MOD_ROLE_ID not in [r.id for r in user.roles]:
    return ephemeral("Ruxsat yo'q.")

Rate-limit / cooldown

  • 1 user → 1 pending zayavka (UNIQUE constraint)
  • Rad etilgandan keyin 24 soat cooldown (Redis TTL)
  • Auto-rol tugma — 1 marta bosish kifoya, qaytarilsa ephemeral xabar

Open session'lar

Bot restart bo'lsa, voice/game sessiyalar yopilmay qoladi. on_ready'da:

  • UPDATE voice_sessions SET ended_at=now() WHERE ended_at IS NULL
  • Yangi sessiyalarni hozir voice'da turganlardan boshlash

Admin panel auth

  • Discord OAuth (scope=identify guilds.members.read)
  • User rollarini guild'da tekshirish — @Admin bo'lmasa 403
  • HttpOnly + Secure cookie, CSRF token (FastAPI middleware)

13 Log boshqaruv — qanday ishlaydi

Har bir muhim hodisa audit_logs jadvaliga yoziladi. Admin panelda filter, qidiruv, eksport.

# Action turlari
application.submit       # zayavka yuborildi
application.approve      # qabul
application.reject       # rad
role.grant               # rol berildi (auto yoki manual)
role.revoke              # rol olindi
voice.session_start
voice.session_end
game.session_start
game.session_end
verify.button_click      # Soyuz tugmasi bosildi
admin.question_change    # savollar tahrirlandi

Panel'da /admin/<guild>/logs sahifasi: sana oralig'i, user, action turi bo'yicha filter, CSV eksport, real-time stream (Server-Sent Events).


Keyingi qadamlar

1
Discord Developer Portal — Application yarat, Bot tabida tokenni oling, Privileged Intents (Members + Presence) yoqing.
2
OAuth2 invite link — 3 ta serverga bot'ni qo'shing: Soyuz, Oila, Flame. Permissions: Manage Roles, Send Messages, Embed Links, View Channels, Read Message History.
3
VPS sozlash — PostgreSQL, Redis, Python 3.12, dsbot user. Reponi /opt/dsbot ga clone qiling.
4
.env — Discord token, guild ID'lar, role ID'lar, channel ID'lar, OAuth secret.
5
Migrationalembic upgrade head, keyin 3 guild yozuvini qo'lda yoki /sync komanda orqali yarating.
6
Test — Soyuz'da tugmali rol, Flame'da zayavka, voice + GTA tracking real foydalanuvchida sinab ko'ring.
7
systemd start + nginx /admin/ proxy + certbot SSL. Sayt va panel bir xil domen ostida.