From 1035d3bc21c80a7dc6a53c1d75552830816aaa08 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sun, 8 Oct 2023 00:18:39 -0400 Subject: [PATCH] First pass on bot management page --- service/src/bot/api/routes.rs | 54 ++++++++++++++- ui/package-lock.json | 24 ++++++- ui/package.json | 1 + ui/src/api/guilds.ts | 33 +++++++++ ui/src/api/guilds.types.ts | 13 ++++ ui/src/api/index.ts | 2 +- ui/src/app/management/page.tsx | 106 +++++++++++++++++++++++++++++ ui/src/components/Topbar/index.tsx | 4 ++ 8 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 ui/src/api/guilds.ts create mode 100644 ui/src/api/guilds.types.ts create mode 100644 ui/src/app/management/page.tsx diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index ab5212e..976b096 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -166,6 +166,56 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + handler.queue().stop(); + } + + HttpResponse::Ok().finish() +} + +#[post("/{guild_id}/voice/resume")] +async fn resume(path: web::Path, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + if let Err(err) = handler.queue().resume() { + warn!("Could not resume track: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + } + + HttpResponse::Ok().finish() +} + #[post("/{guild_id}/voice/pause")] async fn pause(path: web::Path, data: web::Data>) -> HttpResponse { let guild_id = path.into_inner(); @@ -201,7 +251,9 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(get_text_channels) .service(get_voice_channels) .service(send_message) - .service(pause) .service(play) + .service(stop) + .service(resume) + .service(pause) ); } \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 2d20efe..0f90ac5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@mantine/core": "^7.1.2", + "@mantine/form": "^7.1.2", "@mantine/hooks": "^7.1.2", "@mantine/modals": "^7.1.2", "@mantine/notifications": "^7.1.2", @@ -225,6 +226,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@mantine/form": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.1.2.tgz", + "integrity": "sha512-FnUu5XNmRM265G0wy19qSRiItG/2eQ0GQCctnokw6ws9ZnCU1NqvsmpuDE/UiV4YCAOhAVHfqnjG/8tsrlw7ug==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "klona": "^2.0.5" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/@mantine/hooks": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.1.2.tgz", @@ -2305,8 +2318,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -3353,6 +3365,14 @@ "json-buffer": "3.0.1" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", diff --git a/ui/package.json b/ui/package.json index 7be067a..9df65eb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@mantine/core": "^7.1.2", + "@mantine/form": "^7.1.2", "@mantine/hooks": "^7.1.2", "@mantine/modals": "^7.1.2", "@mantine/notifications": "^7.1.2", diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts new file mode 100644 index 0000000..09de488 --- /dev/null +++ b/ui/src/api/guilds.ts @@ -0,0 +1,33 @@ +import { getRequest, postRequest } from '.'; +import { GuildChannel, GuildInfo } from './guilds.types'; + +export async function getGuilds(): Promise { + const response = await getRequest('guilds', {}); + return response?.data || { data: [] }; +} + +export async function getTextChannels(guildId: number): Promise { + const response = await getRequest(`guilds/${guildId}/text`, {}); + return response?.data || { data: [] }; +} + +export async function getVoiceChannels(guildId: number): Promise { + const response = await getRequest(`guilds/${guildId}/voice`, {}); + return response?.data || { data: [] }; +} + +export async function playTrack(guildId: number, channelId: number, track: string): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/play`, { track_url: track }); +} + +export async function stopTrack(guildId: number, channelId: number): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/stop`, {}); +} + +export async function pauseTrack(guildId: number, channelId: number): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/pause`, {}); +} + +export async function resumeTrack(guildId: number, channelId: number): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/resume`, {}); +} diff --git a/ui/src/api/guilds.types.ts b/ui/src/api/guilds.types.ts new file mode 100644 index 0000000..995153d --- /dev/null +++ b/ui/src/api/guilds.types.ts @@ -0,0 +1,13 @@ +export interface GuildInfo { + id: number; + icon?: string; + name: string; + owner: boolean; +} + +export interface GuildChannel { + id: number; + name: string; + type: string; + guild_id: number; +} diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index e0baecc..027cf23 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -12,7 +12,7 @@ export async function getRequest(endpoint: string, params: any): Promise | undefined> { const response = await axios - .post(`${serviceHost}:${servicePort}/${endpoint}`, { body }) + .post(`${serviceHost}:${servicePort}/${endpoint}`, body || {}) .catch((error) => console.error(error)); return response || undefined; } diff --git a/ui/src/app/management/page.tsx b/ui/src/app/management/page.tsx new file mode 100644 index 0000000..5eac9be --- /dev/null +++ b/ui/src/app/management/page.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { + getGuilds, + getTextChannels, + getVoiceChannels, + pauseTrack, + playTrack, + resumeTrack, + stopTrack +} from '@/api/guilds'; +import { GuildChannel, GuildInfo } from '@/api/guilds.types'; +import { Button, Slider, Tabs, TextInput } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import React, { useEffect, useState } from 'react'; + +export default function Page() { + const [guilds, setGuilds] = useState([]); + const [activeGuild, setActiveGuild] = useState(null); + const [textChannels, setTextChannels] = useState([]); + const [voiceChannels, setVoiceChannels] = useState([]); + + useEffect(() => { + getGuilds().then((g) => { + setGuilds(g); + if (g.length > 0) { + setActiveGuild(g[0]); + } + }); + }, []); + + useEffect(() => { + if (activeGuild) { + getTextChannels(activeGuild.id).then((c) => setTextChannels(c)); + getVoiceChannels(activeGuild.id).then((c) => setVoiceChannels(c)); + } + }, [activeGuild]); + + const playForm = useForm({ + initialValues: { + trackUrl: '' + } + }); + + return ( + + + {guilds.map((guild) => ( + setActiveGuild(guild)}> + {guild.name} + + ))} + + {guilds.map((guild) => ( + +

{guild.name}

+

Text Channels

+ + + {textChannels.map((channel) => ( + + {channel.name} + + ))} + + {textChannels.map((channel) => ( + + {channel.name} + + ))} + +

Voice Channels

+ + + {voiceChannels.map((channel) => ( + + {channel.name} + + ))} + + {voiceChannels.map((channel) => ( + + {channel.name} +
{ + playTrack(activeGuild!.id, channel.id, values.trackUrl); + })} + > + + + + + + +
+ {}} /> +
+
+ ))} +
+
+ ))} +
+ ); +} diff --git a/ui/src/components/Topbar/index.tsx b/ui/src/components/Topbar/index.tsx index 780e7a5..5b00233 100644 --- a/ui/src/components/Topbar/index.tsx +++ b/ui/src/components/Topbar/index.tsx @@ -32,6 +32,10 @@ const headerItems = [ { name: 'Spells', link: '/spells' + }, + { + name: 'Management', + link: '/management' } ];