This commit is contained in:
parent
be1451ce45
commit
33da605c03
13 changed files with 534 additions and 9 deletions
52
src/db.rs
52
src/db.rs
|
|
@ -8,10 +8,13 @@ use sea_orm::{
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
entity::{channels, direct_messages, guild_members, guilds, invites, messages, users},
|
||||
entity::{
|
||||
channels, direct_messages, guild_members, guilds, invites, messages, soundboard_sounds,
|
||||
users,
|
||||
},
|
||||
models::{
|
||||
BasicUser, Channel, DmConversation, DmMessageWithAuthor, Guild, Invite, MessageWithAuthor,
|
||||
User,
|
||||
SoundboardSound, User,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -575,3 +578,48 @@ async fn increment_invite_use_count(
|
|||
active.update(txn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_sounds(db: &DatabaseConnection, guild_id: Uuid) -> Result<Vec<SoundboardSound>> {
|
||||
let rows = soundboard_sounds::Entity::find()
|
||||
.filter(soundboard_sounds::Column::GuildId.eq(guild_id))
|
||||
.order_by_asc(soundboard_sounds::Column::CreatedAt)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(map_sound).collect())
|
||||
}
|
||||
|
||||
pub async fn create_sound(
|
||||
db: &DatabaseConnection,
|
||||
guild_id: Uuid,
|
||||
created_by_user_id: Uuid,
|
||||
name: &str,
|
||||
icon: &str,
|
||||
file_path: &str,
|
||||
) -> Result<SoundboardSound> {
|
||||
let model = soundboard_sounds::Entity::insert(soundboard_sounds::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
guild_id: Set(guild_id),
|
||||
created_by_user_id: Set(created_by_user_id),
|
||||
name: Set(name.to_string()),
|
||||
icon: Set(icon.to_string()),
|
||||
file_path: Set(file_path.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(db)
|
||||
.await?;
|
||||
|
||||
Ok(map_sound(model))
|
||||
}
|
||||
|
||||
fn map_sound(model: soundboard_sounds::Model) -> SoundboardSound {
|
||||
SoundboardSound {
|
||||
id: model.id,
|
||||
guild_id: model.guild_id,
|
||||
name: model.name,
|
||||
icon: model.icon,
|
||||
file_path: model.file_path,
|
||||
created_by_user_id: model.created_by_user_id,
|
||||
created_at: model.created_at,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ pub mod guild_members;
|
|||
pub mod guilds;
|
||||
pub mod invites;
|
||||
pub mod messages;
|
||||
pub mod soundboard_sounds;
|
||||
pub mod users;
|
||||
|
|
|
|||
49
src/entity/soundboard_sounds.rs
Normal file
49
src/entity/soundboard_sounds.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "soundboard_sounds")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub guild_id: Uuid,
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub file_path: String,
|
||||
pub created_by_user_id: Uuid,
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::guilds::Entity",
|
||||
from = "Column::GuildId",
|
||||
to = "super::guilds::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Guilds,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::users::Entity",
|
||||
from = "Column::CreatedByUserId",
|
||||
to = "super::users::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
Users,
|
||||
}
|
||||
|
||||
impl Related<super::guilds::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Guilds.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Users.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
@ -11,7 +11,9 @@ use uuid::Uuid;
|
|||
use crate::{
|
||||
AppState,
|
||||
auth::{self, ApiError, AuthUser},
|
||||
chat, db, voice,
|
||||
chat, db,
|
||||
models::{Guild, SoundboardSound},
|
||||
voice,
|
||||
};
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
|
|
@ -37,6 +39,10 @@ pub fn routes() -> Router<AppState> {
|
|||
)
|
||||
.route("/guilds/{guild_id}/invites", post(create_invite))
|
||||
.route("/invites/{code}/join", post(join_invite))
|
||||
.route(
|
||||
"/guilds/{guild_id}/sounds",
|
||||
get(list_sounds).post(upload_sound),
|
||||
)
|
||||
.route("/channels", post(create_channel))
|
||||
.route(
|
||||
"/channels/{channel_id}/messages",
|
||||
|
|
@ -399,11 +405,8 @@ async fn join_invite(
|
|||
State(state): State<AppState>,
|
||||
user: AuthUser,
|
||||
Path(code): Path<String>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let guild = db::join_invite(&state.db, &code.to_uppercase(), user.id)
|
||||
.await
|
||||
.map_err(|e| ApiError::bad_request(&format!("failed to join invite: {e}")))?;
|
||||
|
||||
) -> Result<Json<Guild>, ApiError> {
|
||||
let guild = db::join_invite(&state.db, &code, user.id).await?;
|
||||
Ok(Json(guild))
|
||||
}
|
||||
|
||||
|
|
@ -675,6 +678,87 @@ async fn chat_ws(
|
|||
Ok(ws.on_upgrade(move |socket| chat::handle_socket(state, socket, user.id)))
|
||||
}
|
||||
|
||||
async fn list_sounds(
|
||||
State(state): State<AppState>,
|
||||
user: AuthUser,
|
||||
Path(guild_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<SoundboardSound>>, ApiError> {
|
||||
ensure_guild_member(&state, guild_id, user.id).await?;
|
||||
let sounds = db::list_sounds(&state.db, guild_id).await?;
|
||||
Ok(Json(sounds))
|
||||
}
|
||||
|
||||
async fn upload_sound(
|
||||
State(state): State<AppState>,
|
||||
user: AuthUser,
|
||||
Path(guild_id): Path<Uuid>,
|
||||
mut multipart: axum::extract::Multipart,
|
||||
) -> Result<Json<SoundboardSound>, ApiError> {
|
||||
ensure_guild_member(&state, guild_id, user.id).await?;
|
||||
|
||||
let mut name = None;
|
||||
let mut icon = None;
|
||||
let mut file_data = None;
|
||||
let mut file_name = None;
|
||||
|
||||
while let Some(field) = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|e| ApiError::bad_request(&e.to_string()))?
|
||||
{
|
||||
let field_name = field.name().unwrap_or_default().to_string();
|
||||
if field_name == "name" {
|
||||
name = Some(
|
||||
field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ApiError::bad_request(&e.to_string()))?,
|
||||
);
|
||||
} else if field_name == "icon" {
|
||||
icon = Some(
|
||||
field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ApiError::bad_request(&e.to_string()))?,
|
||||
);
|
||||
} else if field_name == "file" {
|
||||
file_name = Some(field.file_name().unwrap_or("sound.mp3").to_string());
|
||||
file_data = Some(
|
||||
field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| ApiError::bad_request(&e.to_string()))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let (name, icon, file_data, file_name) = match (name, icon, file_data, file_name) {
|
||||
(Some(n), Some(i), Some(d), Some(f)) => (n, i, d, f),
|
||||
_ => return Err(ApiError::bad_request("missing fields")),
|
||||
};
|
||||
|
||||
let extension = std::path::Path::new(&file_name)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("mp3");
|
||||
|
||||
let safe_file_name = format!("{}.{}", Uuid::new_v4(), extension);
|
||||
let upload_dir = std::path::Path::new("static/uploads/soundboard");
|
||||
tokio::fs::create_dir_all(upload_dir)
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(&e.to_string()))?;
|
||||
|
||||
let file_path = upload_dir.join(&safe_file_name);
|
||||
tokio::fs::write(&file_path, file_data)
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(&e.to_string()))?;
|
||||
|
||||
let web_path = format!("/static/uploads/soundboard/{}", safe_file_name);
|
||||
|
||||
let sound = db::create_sound(&state.db, guild_id, user.id, &name, &icon, &web_path).await?;
|
||||
Ok(Json(sound))
|
||||
}
|
||||
|
||||
async fn ensure_guild_member(
|
||||
state: &AppState,
|
||||
guild_id: Uuid,
|
||||
|
|
|
|||
86
src/migration/m20260224_000005_soundboard.rs
Normal file
86
src/migration/m20260224_000005_soundboard.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(SoundboardSounds::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(SoundboardSounds::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(SoundboardSounds::GuildId).uuid().not_null())
|
||||
.col(ColumnDef::new(SoundboardSounds::Name).string().not_null())
|
||||
.col(ColumnDef::new(SoundboardSounds::Icon).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(SoundboardSounds::FilePath)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SoundboardSounds::CreatedByUserId)
|
||||
.uuid()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SoundboardSounds::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_soundboard_sounds_guild")
|
||||
.from(SoundboardSounds::Table, SoundboardSounds::GuildId)
|
||||
.to(Guilds::Table, Guilds::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_soundboard_sounds_user")
|
||||
.from(SoundboardSounds::Table, SoundboardSounds::CreatedByUserId)
|
||||
.to(Users::Table, Users::Id),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(SoundboardSounds::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum SoundboardSounds {
|
||||
Table,
|
||||
Id,
|
||||
GuildId,
|
||||
Name,
|
||||
Icon,
|
||||
FilePath,
|
||||
CreatedByUserId,
|
||||
CreatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Guilds {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ mod m20260213_000001_init;
|
|||
mod m20260213_000002_invites;
|
||||
mod m20260213_000003_channel_kind;
|
||||
mod m20260213_000004_direct_messages;
|
||||
mod m20260224_000005_soundboard;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ impl MigratorTrait for Migrator {
|
|||
Box::new(m20260213_000002_invites::Migration),
|
||||
Box::new(m20260213_000003_channel_kind::Migration),
|
||||
Box::new(m20260213_000004_direct_messages::Migration),
|
||||
Box::new(m20260224_000005_soundboard::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,3 +84,14 @@ pub struct Invite {
|
|||
pub max_uses: Option<i32>,
|
||||
pub use_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoundboardSound {
|
||||
pub id: Uuid,
|
||||
pub guild_id: Uuid,
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub file_path: String,
|
||||
pub created_by_user_id: Uuid,
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
}
|
||||
|
|
|
|||
24
src/voice.rs
24
src/voice.rs
|
|
@ -64,6 +64,10 @@ enum ServerEvent {
|
|||
Error {
|
||||
message: String,
|
||||
},
|
||||
PlaySound {
|
||||
user_id: Uuid,
|
||||
sound_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -83,6 +87,9 @@ enum ClientEvent {
|
|||
SetSpeakingStatus {
|
||||
is_speaking: bool,
|
||||
},
|
||||
PlaySound {
|
||||
sound_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl VoiceHub {
|
||||
|
|
@ -245,6 +252,20 @@ impl VoiceHub {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn play_sound(&self, room_id: Uuid, user_id: Uuid, sound_url: String) {
|
||||
let rooms = self.rooms.read().await;
|
||||
let Some(room) = rooms.get(&room_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for peer in room.values() {
|
||||
let _ = peer.tx.send(ServerEvent::PlaySound {
|
||||
user_id,
|
||||
sound_url: sound_url.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_socket(state: AppState, socket: WebSocket, room_id: Uuid, user_id: Uuid) {
|
||||
|
|
@ -305,6 +326,9 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, room_id: Uuid, us
|
|||
.set_speaking_status(room_id, user_id, is_speaking)
|
||||
.await;
|
||||
}
|
||||
Ok(ClientEvent::PlaySound { sound_url }) => {
|
||||
state.voice.play_sound(room_id, user_id, sound_url).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = tx.send(ServerEvent::Error {
|
||||
message: format!("invalid voice message: {err}"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue