feat: server management page with log channel editing only for admins
This commit is contained in:
12 files changed
+236
-84
No files matched your search
@@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import AdminAccessPage from '@/components/AdminAccessPage.vue'
|
import AdminAccessPage from '@/components/AdminAccessPage.vue'
|
||||||
|
import AdminServersPage from '@/components/AdminServersPage.vue'
|
||||||
import AdminUsersPage from '@/components/AdminUsersPage.vue'
|
import AdminUsersPage from '@/components/AdminUsersPage.vue'
|
||||||
|
|
||||||
defineProps<{ username: string; userId: number }>()
|
defineProps<{ username: string; userId: number }>()
|
||||||
const currentPath = ref(window.location.pathname)
|
const currentPath = ref(window.location.pathname)
|
||||||
const activePage = computed(() => currentPath.value.endsWith('/access') ? 'access' : 'users')
|
const activePage = computed(() => currentPath.value.endsWith('/access')
|
||||||
|
? 'access'
|
||||||
|
: currentPath.value.endsWith('/servers') ? 'servers' : 'users')
|
||||||
|
|
||||||
function navigate(path: string) {
|
function navigate(path: string) {
|
||||||
window.history.pushState({}, '', path)
|
window.history.pushState({}, '', path)
|
||||||
@@ -38,13 +41,14 @@ async function logout() {
|
|||||||
<nav class="sidebar-nav" aria-label="Menu administratora">
|
<nav class="sidebar-nav" aria-label="Menu administratora">
|
||||||
<a href="/admin/users" title="Użytkownicy" :class="{ active: activePage === 'users' }" @click.prevent="navigate('/admin/users')"><span class="nav-icon">♙</span>Użytkownicy</a>
|
<a href="/admin/users" title="Użytkownicy" :class="{ active: activePage === 'users' }" @click.prevent="navigate('/admin/users')"><span class="nav-icon">♙</span>Użytkownicy</a>
|
||||||
<a href="/admin/access" title="Dostęp do serwerów" :class="{ active: activePage === 'access' }" @click.prevent="navigate('/admin/access')"><span class="nav-icon">▦</span>Dostęp do serwerów</a>
|
<a href="/admin/access" title="Dostęp do serwerów" :class="{ active: activePage === 'access' }" @click.prevent="navigate('/admin/access')"><span class="nav-icon">▦</span>Dostęp do serwerów</a>
|
||||||
|
<a href="/admin/servers" title="Serwery" :class="{ active: activePage === 'servers' }" @click.prevent="navigate('/admin/servers')"><span class="nav-icon">#</span>Serwery</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="sidebar-bottom"><div class="admin-identity"><span class="identity-dot"/><span><small>Zalogowano jako</small><strong>{{ username }}</strong></span></div><button class="sidebar-logout" type="button" @click="logout">Wyloguj</button></div>
|
<div class="sidebar-bottom"><div class="admin-identity"><span class="identity-dot"/><span><small>Zalogowano jako</small><strong>{{ username }}</strong></span></div><button class="sidebar-logout" type="button" @click="logout">Wyloguj</button></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="admin-main">
|
<section class="admin-main">
|
||||||
<header class="admin-topbar"><div><span class="topbar-label">ADMINISTRACJA</span><span class="topbar-separator">/</span><span>{{ activePage === 'access' ? 'Dostęp do serwerów' : 'Użytkownicy' }}</span></div><div class="topbar-actions"><a class="topbar-link" href="/">Wróć do panelu</a><button class="admin-button admin-button-danger" type="button" @click="restartBot">Restart bota</button></div></header>
|
<header class="admin-topbar"><div><span class="topbar-label">ADMINISTRACJA</span><span class="topbar-separator">/</span><span>{{ activePage === 'access' ? 'Dostęp do serwerów' : activePage === 'servers' ? 'Serwery' : 'Użytkownicy' }}</span></div><div class="topbar-actions"><a class="topbar-link" href="/">Wróć do panelu</a><button class="admin-button admin-button-danger" type="button" @click="restartBot">Restart bota</button></div></header>
|
||||||
<div class="admin-content"><AdminUsersPage v-if="activePage === 'users'" :current-user-id="userId"/><AdminAccessPage v-else/></div>
|
<div class="admin-content"><AdminUsersPage v-if="activePage === 'users'" :current-user-id="userId"/><AdminAccessPage v-else-if="activePage === 'access'"/><AdminServersPage v-else/></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
type LogChannel = { channel_id: string; channel_name: string }
|
||||||
|
type Server = {
|
||||||
|
guild_id: string
|
||||||
|
guild_name: string
|
||||||
|
log_channel_id: string | null
|
||||||
|
disabled: boolean
|
||||||
|
channels: LogChannel[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = ref<Server[]>([])
|
||||||
|
const drafts = ref<Record<string, string>>({})
|
||||||
|
const loading = ref(true)
|
||||||
|
const saving = ref(new Set<string>())
|
||||||
|
const errors = ref<Record<string, string>>({})
|
||||||
|
const loadError = ref('')
|
||||||
|
const changedIds = computed(() => servers.value
|
||||||
|
.filter((server) => (drafts.value[server.guild_id] ?? '') !== (server.log_channel_id ?? ''))
|
||||||
|
.map((server) => server.guild_id))
|
||||||
|
|
||||||
|
async function loadServers() {
|
||||||
|
loading.value = true
|
||||||
|
loadError.value = ''
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/servers')
|
||||||
|
const payload = await response.json().catch(() => null)
|
||||||
|
if (!response.ok) throw new Error(payload?.detail ?? 'Nie udało się pobrać serwerów.')
|
||||||
|
servers.value = payload
|
||||||
|
drafts.value = Object.fromEntries(payload.map((server: Server) => [
|
||||||
|
server.guild_id, server.disabled ? '' : server.log_channel_id ?? '',
|
||||||
|
]))
|
||||||
|
} catch (cause) {
|
||||||
|
loadError.value = cause instanceof Error ? cause.message : 'Nie udało się pobrać serwerów.'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(server: Server) {
|
||||||
|
const guildId = server.guild_id
|
||||||
|
saving.value = new Set(saving.value).add(guildId)
|
||||||
|
errors.value = { ...errors.value, [guildId]: '' }
|
||||||
|
try {
|
||||||
|
const selected = drafts.value[guildId] ?? ''
|
||||||
|
const response = await fetch(`/api/admin/servers/${guildId}/log-channel`, {
|
||||||
|
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ channel_id: selected || null }),
|
||||||
|
})
|
||||||
|
const payload = await response.json().catch(() => null)
|
||||||
|
if (!response.ok) throw new Error(payload?.detail ?? 'Nie udało się zapisać kanału logów.')
|
||||||
|
server.log_channel_id = payload.log_channel_id
|
||||||
|
server.disabled = payload.disabled
|
||||||
|
} catch (cause) {
|
||||||
|
errors.value = {
|
||||||
|
...errors.value,
|
||||||
|
[guildId]: cause instanceof Error ? cause.message : 'Nie udało się zapisać kanału logów.',
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const pending = new Set(saving.value)
|
||||||
|
pending.delete(guildId)
|
||||||
|
saving.value = pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { void loadServers() })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="admin-page-content">
|
||||||
|
<div class="admin-page-heading">
|
||||||
|
<div><p class="admin-eyebrow">KONFIGURACJA</p><h2>Serwery</h2><p>Ustaw kanał, na który bot wysyła powiadomienia, lub wyłącz je dla serwera.</p></div>
|
||||||
|
<button class="server-refresh" type="button" :disabled="loading" @click="loadServers">Odśwież</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="loadError" class="server-error">{{ loadError }}</p>
|
||||||
|
<div v-if="loading" class="server-state">Ładowanie serwerów…</div>
|
||||||
|
<div v-else-if="!servers.length" class="server-state">Bot nie jest obecnie na żadnym serwerze Discorda.</div>
|
||||||
|
<section v-for="server in servers" :key="server.guild_id" class="server-card">
|
||||||
|
<header class="server-card-heading">
|
||||||
|
<span class="server-icon">#</span>
|
||||||
|
<div><h3>{{ server.guild_name }}</h3><p>{{ server.channels.length }} dostępnych kanałów tekstowych</p></div>
|
||||||
|
<span class="server-state-badge" :class="drafts[server.guild_id] ? 'server-state-enabled' : 'server-state-disabled'">{{ drafts[server.guild_id] ? 'Logi włączone' : 'Logi wyłączone' }}</span>
|
||||||
|
</header>
|
||||||
|
<div class="server-settings-row">
|
||||||
|
<label :for="`log-channel-${server.guild_id}`">Kanał logów</label>
|
||||||
|
<select :id="`log-channel-${server.guild_id}`" v-model="drafts[server.guild_id]" class="server-channel-select" :disabled="saving.has(server.guild_id)">
|
||||||
|
<option value="">Wyłącz kanał logów</option>
|
||||||
|
<option v-for="channel in server.channels" :key="channel.channel_id" :value="channel.channel_id"># {{ channel.channel_name }}</option>
|
||||||
|
</select>
|
||||||
|
<button class="server-save" type="button" :disabled="saving.has(server.guild_id) || !changedIds.includes(server.guild_id)" @click="save(server)">{{ saving.has(server.guild_id) ? 'Zapisywanie…' : 'Zapisz' }}</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="errors[server.guild_id]" class="server-error">{{ errors[server.guild_id] }}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-page-content{display:grid;gap:18px}.admin-page-heading{display:flex;align-items:center;justify-content:space-between;gap:16px}.admin-page-heading h2{margin:2px 0 6px;font-size:28px}.admin-page-heading p:last-child{margin:0;color:#a9b1c4}.admin-eyebrow{margin:0;color:#9c88ff;font-size:11px;font-weight:700;letter-spacing:.14em}.server-refresh,.server-save{min-height:38px;padding:0 15px;border:1px solid #41465c;border-radius:9px;background:#252a3c;color:#eceeff;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer}.server-refresh:hover:not(:disabled){background:#30364b}.server-save{border-color:#8c70f2;background:#7657e8;color:#fff}.server-save:hover:not(:disabled){background:#886df1}.server-refresh:disabled,.server-save:disabled{opacity:.45;cursor:not-allowed}.server-card{padding:22px 24px;border:1px solid #303449;border-radius:16px;background:#171a29;box-shadow:0 12px 36px #0003}.server-card-heading{display:flex;align-items:center;gap:12px;padding-bottom:18px;border-bottom:1px solid #2b2f42}.server-icon{display:grid;place-items:center;width:38px;height:38px;border-radius:11px;background:#30294e;color:#beadff;font-size:20px}.server-card-heading h3{margin:0 0 3px;color:#f3f4f8;font-size:16px}.server-card-heading p{margin:0;color:#9299ac;font-size:12px}.server-state-badge{margin-left:auto;padding:6px 10px;border-radius:999px;font-size:11px}.server-state-enabled{background:#30294e;color:#cbbfff}.server-state-disabled{background:#282a35;color:#a6aabb}.server-settings-row{display:grid;grid-template-columns:140px minmax(220px,1fr) auto;align-items:center;gap:14px;padding-top:18px}.server-settings-row label{color:#c5cada;font-size:13px;font-weight:600}.server-channel-select{width:100%;height:40px;padding:0 11px;border:1px solid #41465c;border-radius:8px;background:#101321;color:#f3f4f8;font:inherit}.server-channel-select:focus{outline:2px solid #9b83ff66;border-color:#9b83ff}.server-state{padding:32px;color:#a9b1c4;text-align:center}.server-error{margin:12px 0 0;color:#ff8d9b;font-size:13px}@media(max-width:680px){.admin-page-heading{align-items:flex-start;flex-direction:column}.server-card{padding:17px}.server-settings-row{grid-template-columns:1fr}.server-settings-row label{margin-bottom:-7px}.server-save{width:100%}.server-state-badge{font-size:10px}}
|
||||||
|
</style>
|
||||||
@@ -9,13 +9,10 @@ const props = defineProps<{ status: BotStatus | null }>()
|
|||||||
|
|
||||||
const { notify, run } = useToasts()
|
const { notify, run } = useToasts()
|
||||||
const selected = ref('')
|
const selected = ref('')
|
||||||
const selectedNotification = ref('')
|
|
||||||
const defaultPlays = ref(5)
|
const defaultPlays = ref(5)
|
||||||
|
|
||||||
const channels = computed(() => props.status?.voice_channels ?? [])
|
const channels = computed(() => props.status?.voice_channels ?? [])
|
||||||
const active = computed(() => props.status?.active_voice_channel ?? null)
|
const active = computed(() => props.status?.active_voice_channel ?? null)
|
||||||
const notificationChannels = computed(() => props.status?.text_channels ?? [])
|
|
||||||
const activeNotificationId = computed(() => props.status?.active_text_channel_id ?? null)
|
|
||||||
|
|
||||||
// Wybór podąża za realnym połączeniem, a bez niego domyślnie wskazuje pierwszy kanał.
|
// Wybór podąża za realnym połączeniem, a bez niego domyślnie wskazuje pierwszy kanał.
|
||||||
watch(
|
watch(
|
||||||
@@ -38,18 +35,6 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
[activeNotificationId, notificationChannels],
|
|
||||||
([activeChannel, available]) => {
|
|
||||||
if (activeChannel) {
|
|
||||||
selectedNotification.value = activeChannel
|
|
||||||
} else if (!available.some((channel) => channel.channel_id === selectedNotification.value)) {
|
|
||||||
selectedNotification.value = available[0]?.channel_id ?? ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
async function toggle() {
|
async function toggle() {
|
||||||
if (active.value) {
|
if (active.value) {
|
||||||
await run(() => api.disconnect(), '', 'Błąd rozłączania')
|
await run(() => api.disconnect(), '', 'Błąd rozłączania')
|
||||||
@@ -62,15 +47,6 @@ async function toggle() {
|
|||||||
await run(() => api.connect(selected.value), '', 'Nie udało się połączyć')
|
await run(() => api.connect(selected.value), '', 'Nie udało się połączyć')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setNotificationChannel() {
|
|
||||||
if (!selectedNotification.value) return
|
|
||||||
await run(
|
|
||||||
() => api.setNotificationChannel(selectedNotification.value),
|
|
||||||
'Ustawiono kanał logów',
|
|
||||||
'Nie udało się ustawić kanału logów',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveDefaultPlays() {
|
async function saveDefaultPlays() {
|
||||||
await run(
|
await run(
|
||||||
() => api.setDefaultPlays(defaultPlays.value),
|
() => api.setDefaultPlays(defaultPlays.value),
|
||||||
@@ -114,30 +90,6 @@ async function saveDefaultPlays() {
|
|||||||
{{ active ? 'Odłącz' : 'Połącz' }}
|
{{ active ? 'Odłącz' : 'Połącz' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification-channel-row">
|
|
||||||
<label for="notification-channel">Kanał logów</label>
|
|
||||||
<select
|
|
||||||
id="notification-channel"
|
|
||||||
v-model="selectedNotification"
|
|
||||||
class="premium-select"
|
|
||||||
:disabled="!notificationChannels.length"
|
|
||||||
@change="setNotificationChannel"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-if="!notificationChannels.length"
|
|
||||||
value=""
|
|
||||||
>
|
|
||||||
Brak dostępnych kanałów tekstowych
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-for="channel in notificationChannels"
|
|
||||||
:key="channel.channel_id"
|
|
||||||
:value="channel.channel_id"
|
|
||||||
>
|
|
||||||
# {{ channel.channel_name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="default-plays-row">
|
<div class="default-plays-row">
|
||||||
<label for="default-plays">Domyślny licznik</label>
|
<label for="default-plays">Domyślny licznik</label>
|
||||||
<input
|
<input
|
||||||
@@ -160,25 +112,6 @@ async function saveDefaultPlays() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.notification-channel-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-channel-row label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 130px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-channel-row .premium-select {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-plays-row {
|
.default-plays-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -20,15 +20,17 @@ class NotificationChannelResolver:
|
|||||||
def resolve(
|
def resolve(
|
||||||
self, guild_id: int, remembered: discord.abc.Messageable | None
|
self, guild_id: int, remembered: discord.abc.Messageable | None
|
||||||
) -> discord.abc.Messageable | None:
|
) -> discord.abc.Messageable | None:
|
||||||
if remembered:
|
if self._config.is_notification_channel_disabled(guild_id):
|
||||||
return remembered
|
return None
|
||||||
|
|
||||||
saved_id = self._config.get_notification_channel_id(guild_id)
|
saved_id = self._config.get_notification_channel_id(guild_id)
|
||||||
if saved_id:
|
if saved_id:
|
||||||
channel = self._bot.get_channel(saved_id)
|
channel = self._bot.get_channel(saved_id)
|
||||||
if channel:
|
if channel:
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
|
if remembered:
|
||||||
|
return remembered
|
||||||
|
|
||||||
return self.first_writable(self._bot.get_guild(guild_id))
|
return self.first_writable(self._bot.get_guild(guild_id))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -25,7 +25,23 @@ class PostgresGuildConfig:
|
|||||||
self._ensure(guild_id)
|
self._ensure(guild_id)
|
||||||
with self._database.connect() as connection:
|
with self._database.connect() as connection:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"UPDATE guilds SET notification_channel_id=%s WHERE id=%s", (channel_id, guild_id)
|
"UPDATE guilds SET notification_channel_id=%s, notification_channel_disabled=FALSE WHERE id=%s",
|
||||||
|
(channel_id, guild_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_notification_channel_disabled(self, guild_id: int) -> bool:
|
||||||
|
with self._database.connect() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT notification_channel_disabled FROM guilds WHERE id=%s", (guild_id,)
|
||||||
|
).fetchone()
|
||||||
|
return bool(row[0]) if row else False
|
||||||
|
|
||||||
|
def set_notification_channel_disabled(self, guild_id: int, disabled: bool) -> None:
|
||||||
|
self._ensure(guild_id)
|
||||||
|
with self._database.connect() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE guilds SET notification_channel_disabled=%s WHERE id=%s",
|
||||||
|
(disabled, guild_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_default_plays(self, guild_id: int | None = None) -> int:
|
def get_default_plays(self, guild_id: int | None = None) -> int:
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
|
||||||
|
|
||||||
|
class DisableLogChannelMigration:
|
||||||
|
version = 3
|
||||||
|
name = "disable_log_channel"
|
||||||
|
|
||||||
|
def apply(self, connection: psycopg.Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"ALTER TABLE guilds ADD COLUMN notification_channel_disabled BOOLEAN NOT NULL DEFAULT FALSE"
|
||||||
|
)
|
||||||
|
|
||||||
|
def rollback(self, connection: psycopg.Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"ALTER TABLE guilds DROP COLUMN notification_channel_disabled"
|
||||||
|
)
|
||||||
@@ -6,5 +6,8 @@ InitialSchemaMigration = import_module(
|
|||||||
".0001_initial_schema", __name__
|
".0001_initial_schema", __name__
|
||||||
).InitialSchemaMigration
|
).InitialSchemaMigration
|
||||||
UsersMigration = import_module(".0002_users", __name__).UsersMigration
|
UsersMigration = import_module(".0002_users", __name__).UsersMigration
|
||||||
|
DisableLogChannelMigration = import_module(
|
||||||
|
".0003_disable_log_channel", __name__
|
||||||
|
).DisableLogChannelMigration
|
||||||
|
|
||||||
MIGRATIONS = (InitialSchemaMigration(), UsersMigration())
|
MIGRATIONS = (InitialSchemaMigration(), UsersMigration(), DisableLogChannelMigration())
|
||||||
@@ -8,6 +8,10 @@ class GuildConfigRepository(Protocol):
|
|||||||
|
|
||||||
def set_notification_channel_id(self, guild_id: int, channel_id: int | None) -> None: ...
|
def set_notification_channel_id(self, guild_id: int, channel_id: int | None) -> None: ...
|
||||||
|
|
||||||
|
def is_notification_channel_disabled(self, guild_id: int) -> bool: ...
|
||||||
|
|
||||||
|
def set_notification_channel_disabled(self, guild_id: int, disabled: bool) -> None: ...
|
||||||
|
|
||||||
def get_default_plays(self, guild_id: int | None = None) -> int: ...
|
def get_default_plays(self, guild_id: int | None = None) -> int: ...
|
||||||
|
|
||||||
def set_default_plays(self, guild_id: int, value: int) -> None: ...
|
def set_default_plays(self, guild_id: int, value: int) -> None: ...
|
||||||
@@ -9,6 +9,7 @@ from zyrarbot.storage.ports.guild_config import GuildConfigRepository
|
|||||||
from zyrarbot.storage.ports.uploads import UploadsRepository
|
from zyrarbot.storage.ports.uploads import UploadsRepository
|
||||||
from zyrarbot.ui.api.connection_manager import ConnectionManager
|
from zyrarbot.ui.api.connection_manager import ConnectionManager
|
||||||
from zyrarbot.ui.api.routers import (
|
from zyrarbot.ui.api.routers import (
|
||||||
|
admin_router,
|
||||||
auth_router,
|
auth_router,
|
||||||
playback_router,
|
playback_router,
|
||||||
queue_router,
|
queue_router,
|
||||||
@@ -22,6 +23,7 @@ from zyrarbot.ui.api.status_service import StatusService
|
|||||||
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
||||||
|
|
||||||
ROUTERS = (
|
ROUTERS = (
|
||||||
|
admin_router.router,
|
||||||
auth_router.router,
|
auth_router.router,
|
||||||
status_router.router,
|
status_router.router,
|
||||||
playback_router.router,
|
playback_router.router,
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from zyrarbot.music.notification_channel_resolver import NotificationChannelResolver
|
||||||
|
from zyrarbot.ui.api.auth import require_admin
|
||||||
|
from zyrarbot.ui.api.dependencies import get_broadcaster, get_config, get_guilds, get_player
|
||||||
|
from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster
|
||||||
|
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||||
|
|
||||||
|
|
||||||
|
class LogChannelUpdate(BaseModel):
|
||||||
|
channel_id: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/servers")
|
||||||
|
async def list_servers(
|
||||||
|
_: dict = Depends(require_admin),
|
||||||
|
guilds: GuildResolver = Depends(get_guilds),
|
||||||
|
player=Depends(get_player),
|
||||||
|
config=Depends(get_config),
|
||||||
|
) -> list[dict]:
|
||||||
|
result = []
|
||||||
|
for guild in guilds.guilds():
|
||||||
|
channel = player.notification_channel(guild.id)
|
||||||
|
disabled = config.is_notification_channel_disabled(guild.id)
|
||||||
|
result.append({
|
||||||
|
"guild_id": str(guild.id),
|
||||||
|
"guild_name": guild.name,
|
||||||
|
"log_channel_id": None if disabled or channel is None else str(channel.id),
|
||||||
|
"disabled": disabled,
|
||||||
|
"channels": [
|
||||||
|
{"channel_id": str(item.id), "channel_name": item.name}
|
||||||
|
for item in NotificationChannelResolver.writable_channels(guild)
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/servers/{guild_id}/log-channel")
|
||||||
|
async def set_log_channel(
|
||||||
|
guild_id: int,
|
||||||
|
body: LogChannelUpdate,
|
||||||
|
_: dict = Depends(require_admin),
|
||||||
|
guilds: GuildResolver = Depends(get_guilds),
|
||||||
|
player=Depends(get_player),
|
||||||
|
config=Depends(get_config),
|
||||||
|
broadcaster: StatusBroadcaster = Depends(get_broadcaster),
|
||||||
|
) -> dict[str, str | bool | None]:
|
||||||
|
guild = next((item for item in guilds.guilds() if item.id == guild_id), None)
|
||||||
|
if guild is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Nie znaleziono serwera")
|
||||||
|
|
||||||
|
if body.channel_id is None or body.channel_id == "":
|
||||||
|
player.set_notification_channel(guild_id, None)
|
||||||
|
config.set_notification_channel_id(guild_id, None)
|
||||||
|
config.set_notification_channel_disabled(guild_id, True)
|
||||||
|
await broadcaster.broadcast()
|
||||||
|
return {"guild_id": str(guild_id), "log_channel_id": None, "disabled": True}
|
||||||
|
|
||||||
|
if not body.channel_id.isdecimal():
|
||||||
|
raise HTTPException(status_code=400, detail="Nieprawidłowe ID kanału")
|
||||||
|
channel = guilds.channel(int(body.channel_id))
|
||||||
|
if not isinstance(channel, discord.TextChannel) or channel.guild.id != guild_id:
|
||||||
|
raise HTTPException(status_code=404, detail="Kanał nie należy do tego serwera")
|
||||||
|
if channel not in NotificationChannelResolver.writable_channels(guild):
|
||||||
|
raise HTTPException(status_code=403, detail="Bot nie może pisać na tym kanale")
|
||||||
|
|
||||||
|
player.set_notification_channel(guild_id, channel)
|
||||||
|
config.set_notification_channel_id(guild_id, channel.id)
|
||||||
|
await broadcaster.broadcast()
|
||||||
|
return {"guild_id": str(guild_id), "log_channel_id": str(channel.id), "disabled": False}
|
||||||
@@ -17,6 +17,7 @@ from zyrarbot.ui.api.requests.connect_request import ConnectRequest
|
|||||||
from zyrarbot.ui.api.requests.notification_channel_request import NotificationChannelRequest
|
from zyrarbot.ui.api.requests.notification_channel_request import NotificationChannelRequest
|
||||||
from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster
|
from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster
|
||||||
from zyrarbot.ui.api.auth import current_user
|
from zyrarbot.ui.api.auth import current_user
|
||||||
|
from zyrarbot.ui.api.auth import require_admin
|
||||||
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["voice"])
|
router = APIRouter(prefix="/api", tags=["voice"])
|
||||||
@@ -30,6 +31,7 @@ async def set_notification_channel(
|
|||||||
player: MusicPlayer = Depends(get_player),
|
player: MusicPlayer = Depends(get_player),
|
||||||
config: GuildConfigRepository = Depends(get_config),
|
config: GuildConfigRepository = Depends(get_config),
|
||||||
broadcaster: StatusBroadcaster = Depends(get_broadcaster),
|
broadcaster: StatusBroadcaster = Depends(get_broadcaster),
|
||||||
|
_: dict = Depends(require_admin),
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
try:
|
try:
|
||||||
channel_id = int(request.channel_id)
|
channel_id = int(request.channel_id)
|
||||||
@@ -85,11 +87,6 @@ async def connect(
|
|||||||
|
|
||||||
await channel.connect()
|
await channel.connect()
|
||||||
|
|
||||||
if not player.notification_channel(guild_id):
|
|
||||||
player.set_notification_channel(
|
|
||||||
guild_id, NotificationChannelResolver.first_writable(channel.guild)
|
|
||||||
)
|
|
||||||
|
|
||||||
await broadcaster.broadcast()
|
await broadcaster.broadcast()
|
||||||
return {"status": "connected", "channel": channel.name}
|
return {"status": "connected", "channel": channel.name}
|
||||||
|
|
||||||
|
|||||||
@@ -48,16 +48,13 @@ class VoiceCog(MusicCogBase, name="Voice"):
|
|||||||
if existing:
|
if existing:
|
||||||
await existing.disconnect()
|
await existing.disconnect()
|
||||||
await channel.connect()
|
await channel.connect()
|
||||||
if not self.player.notification_channel(interaction.guild.id):
|
|
||||||
self.player.set_notification_channel(
|
|
||||||
interaction.guild.id, NotificationChannelResolver.first_writable(interaction.guild)
|
|
||||||
)
|
|
||||||
await self.player.trigger_state_change()
|
await self.player.trigger_state_change()
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
embed=message_embed("Kanał głosowy", f"Połączono z kanałem **{channel.name}**.")
|
embed=message_embed("Kanał głosowy", f"Połączono z kanałem **{channel.name}**.")
|
||||||
)
|
)
|
||||||
|
|
||||||
@discord.app_commands.command(name="notificationchannel", description="Ustaw kanał logów")
|
@discord.app_commands.command(name="notificationchannel", description="Ustaw kanał logów")
|
||||||
|
@discord.app_commands.checks.has_permissions(administrator=True)
|
||||||
@discord.app_commands.describe(channel="Kanał tekstowy")
|
@discord.app_commands.describe(channel="Kanał tekstowy")
|
||||||
async def notification_channel(
|
async def notification_channel(
|
||||||
self, interaction: discord.Interaction, channel: discord.TextChannel
|
self, interaction: discord.Interaction, channel: discord.TextChannel
|
||||||
|
|||||||
Reference in new issue
Block a user