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.
Bitta bot 2 ta server'ga ulanadi. Flame ichida ierarxiya bor: oddiy a'zo → tasdiqlangan o'yinchi → oila (yaqin doiradagi faol a'zolar).
Umumiy ochiq server. Asosiy funksiya — auto-rol tugmasi. Yangi a'zo qoidalarni o'qib tugmani bossa, @Verified roli beriladi va barcha kanallarni ko'radi.
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.
text, long_text, number, choice (select), date.
#applications kanaliga embed → 2 ta tugma (Qabul/Rad) → admin bosadi → rol beriladi yoki DM bilan sabab yuboriladi.
@Verified rol beriladi. Bot restartdan keyin ham ishlaydi (custom_id + add_view).
on_voice_state_update orqali har voice join/leave qayd etiladi. Daily/weekly/monthly leaderboard. AFK kanal hisobga olinmaydi.
@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.
on_presence_update orqali user.activities'ni kuzatadi. Grand Theft Auto V, FiveM, RAGE Multiplayer nomli aktivlik boshlanganda timer start, tugaganda stop.
Adminlar panel orqali savollar to'plamini boshqaradi. Foydalanuvchi /zayavka chaqirsa, bot DBdan aktiv savollarni o'qib modal yasaydi.
@diablo_007 (392841029384029384)@Admin bosa oladi. Majority qoidasi: qabul > rad → approve, aks → reject.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.
Bir kishi qaror qilmaydi — ko'p admin ovoz beradi. Har bir tugma bosishi DB'ga yoziladi va embed real-time yangilanadi.
✓ Qabul: N | ✗ Rad: M + kim ovoz berganini ro'yxati.edit_message bilan view yangilanadi).@Admin roli bossa oladi.approve_count > reject_count → qabul, aks → rad. Teng bo'lsa — yakunlovchining ovozi hal qiladi.view=None), embed yopiq holatga o'tadi.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
);
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)
Qoidalar kanalida bitta xabar: "Qoidalarni o'qib chiqdim". Tugma bossa rol beriladi.
@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)
Bot turli hodisalarni turli kanalga yo'naltiradi. Mapping DB'da, admin paneldan tahrirlanadi.
| Hodisa | Kanal (default) | Misol |
|---|---|---|
application.new | #applications | Zayavka 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-log | Kunlik 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-log | Warn/kick/ban hodisalari |
bot.error | #bot-errors | Stacktrace, runtime exception (faqat admin ko'radi) |
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)
);
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.
@Player rolidan keyingi bosqich. Auto-promotion yoki manual berish.
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
}
@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
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"}
@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_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.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)
Ikki xil kirish usuli: oddiy login/parol (asosiy) yoki Discord OAuth (ixtiyoriy). Faqat tasdiqlangan adminlar.
bcrypt (cost=12) bilan hash qilinadipython -m web.create_admin komandasi orqaliidentify guilds.members.read@Admin guildda bormi?)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()
);
| Route | Maqsad |
|---|---|
/admin | Dashboard — barcha guild'lar bo'yicha umumiy statistika |
/admin/flame/applications | Zayavkalar ro'yxati, filter, accept/decline web orqali ham |
/admin/flame/questions | Savollar konstruktori — drag-drop tartib, qo'shish, o'chirish |
/admin/<guild>/voice | Voice leaderboard (kun/hafta/oy) |
/admin/flame/gta | GTA playtime — kim qancha o'ynadi |
/admin/<guild>/logs | Audit log — filter: action, user, sana |
/admin/<guild>/settings | Rollar, kanallar, intent toggle |
/admin/export | CSV/JSON eksport |
-- 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);
| Qatlam | Tanlov | Sabab |
|---|---|---|
| Til | Python 3.12 | discord.py + asyncio + FastAPI yagona ekosistem |
| Discord SDK | discord.py 2.4+ | slash, modal, button, view persistence |
| DB | PostgreSQL 16 | JSONB savollar + audit log uchun zo'r |
| ORM/Driver | asyncpg + SQLAlchemy 2 (async) | tip-xavfsiz, fast |
| Cache | Redis | cooldown, rate-limit, session |
| Web | FastAPI + Jinja2 + HTMX | SPA shovqinisiz interaktiv |
| Auth | Discord OAuth2 | panel kirishi rol orqali |
| Process | systemd × 2 | dsbot + dsbot-web |
| Reverse proxy | nginx + certbot | HTTPS, static |
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
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=...
# /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
# /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
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;
}
}
Developer Portal → Bot → quyidagilarni yoqing:
⚠ 100+ guild bo'lsa Discord'dan verifikatsiya so'raydi. Sizda 3 ta — muammosiz.
Bot roli quyidagilardan yuqorida bo'lishi shart:
@Verified (Soyuz)@Player (Flame, zayavka qabul qilinganda)Aks holda: discord.Forbidden: Missing Permissions
Har button handler boshida:
if MOD_ROLE_ID not in [r.id for r in user.roles]:
return ephemeral("Ruxsat yo'q.")
Bot restart bo'lsa, voice/game sessiyalar yopilmay qoladi. on_ready'da:
UPDATE voice_sessions SET ended_at=now() WHERE ended_at IS NULLscope=identify guilds.members.read)@Admin bo'lmasa 403Har 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).
Manage Roles, Send Messages, Embed Links, View Channels, Read Message History./opt/dsbot ga clone qiling.alembic upgrade head, keyin 3 guild yozuvini qo'lda yoki /sync komanda orqali yarating.