use tonic::{Request, Response, Status}; use uuid::Uuid; use crate::pb::im::channel_audit_service_server::ChannelAuditService; use crate::pb::im::channel_invitation_service_server::ChannelInvitationService; use crate::pb::im::channel_repo_link_service_server::ChannelRepoLinkService; use crate::pb::im::channel_role_service_server::ChannelRoleService; use crate::pb::im::channel_slash_command_service_server::ChannelSlashCommandService; use crate::pb::im::channel_webhook_service_server::ChannelWebhookService; use crate::pb::im::custom_emoji_service_server::CustomEmojiService; use crate::pb::im::forum_tag_service_server::ForumTagService; use crate::pb::im::im_integration_service_server::ImIntegrationService; use crate::pb::im::stage_service_server::StageService; use crate::pb::im::voice_service_server::VoiceService; use crate::pb::im::{ AcceptInvitationRequest, AcceptInvitationResponse, CreateChannelRoleRequest, CreateChannelRoleResponse, CreateCustomEmojiRequest, CreateCustomEmojiResponse, CreateForumTagRequest, CreateForumTagResponse, CreateIntegrationRequest, CreateIntegrationResponse, CreateInvitationRequest, CreateInvitationResponse, CreateRepoLinkRequest, CreateRepoLinkResponse, CreateSlashCommandRequest, CreateSlashCommandResponse, CreateStageRequest, CreateStageResponse, CreateWebhookRequest, CreateWebhookResponse, DeleteChannelRoleRequest, DeleteChannelRoleResponse, DeleteCustomEmojiRequest, DeleteCustomEmojiResponse, DeleteForumTagRequest, DeleteForumTagResponse, DeleteIntegrationRequest, DeleteIntegrationResponse, DeleteRepoLinkRequest, DeleteRepoLinkResponse, DeleteSlashCommandRequest, DeleteSlashCommandResponse, DeleteStageRequest, DeleteStageResponse, DeleteWebhookRequest, DeleteWebhookResponse, GetStageRequest, GetStageResponse, ListChannelEventsRequest, ListChannelEventsResponse, ListChannelRolesRequest, ListChannelRolesResponse, ListCustomEmojisRequest, ListCustomEmojisResponse, ListForumTagsRequest, ListForumTagsResponse, ListIntegrationsRequest, ListIntegrationsResponse, ListInvitationsRequest, ListInvitationsResponse, ListRepoLinksRequest, ListRepoLinksResponse, ListSlashCommandsRequest, ListSlashCommandsResponse, ListVoiceParticipantsRequest, ListVoiceParticipantsResponse, ListWebhooksRequest, ListWebhooksResponse, RevokeInvitationRequest, RevokeInvitationResponse, UpdateChannelRoleRequest, UpdateChannelRoleResponse, UpdateForumTagRequest, UpdateForumTagResponse, UpdateIntegrationRequest, UpdateIntegrationResponse, UpdateSlashCommandRequest, UpdateSlashCommandResponse, UpdateStageRequest, UpdateStageResponse, UpdateVoiceStateRequest, UpdateVoiceStateResponse, UpdateWebhookRequest, UpdateWebhookResponse, }; use crate::service::im::channel_roles::{CreateChannelRoleParams, UpdateChannelRoleParams}; use crate::service::im::custom_emojis::CreateCustomEmojiParams; use crate::service::im::forum_tags::{CreateForumTagParams, UpdateForumTagParams}; use crate::service::im::integrations::{CreateIntegrationParams, UpdateIntegrationParams}; use crate::service::im::invitations::CreateInvitationParams; use crate::service::im::repo_links::CreateRepoLinkParams; use crate::service::im::session::ImSession; use crate::service::im::slash_commands::{CreateSlashCommandParams, UpdateSlashCommandParams}; use crate::service::im::stages::{CreateStageParams, UpdateStageParams}; use crate::service::im::voice::UpdateVoiceStateParams; use crate::service::im::webhooks::{CreateWebhookParams, UpdateWebhookParams}; use crate::service::AppService; fn to_proto_ts(dt: chrono::DateTime) -> Option { Some(prost_types::Timestamp { seconds: dt.timestamp(), nanos: dt.timestamp_subsec_nanos() as i32, }) } fn parse_uuid(s: &str, field: &str) -> Result { Uuid::parse_str(s).map_err(|e| Status::invalid_argument(format!("{field}: {e}"))) } fn system_session() -> ImSession { ImSession::new(Uuid::nil()) } // Section: ChannelRoleGrpcService pub struct ChannelRoleGrpcService { service: AppService, } impl ChannelRoleGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } #[tonic::async_trait] impl ChannelRoleService for ChannelRoleGrpcService { async fn list_channel_roles( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let roles = self .service .im .channel_role_list(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; let proto_roles = roles .into_iter() .map(|r| crate::pb::im::ChannelRole { id: r.id.to_string(), channel_id: r.channel_id.to_string(), name: r.name, permissions: r.permissions.iter().map(|p| p.to_string()).collect(), assignable: r.assignable, created_at: to_proto_ts(r.created_at), updated_at: to_proto_ts(r.updated_at), }) .collect(); Ok(Response::new(ListChannelRolesResponse { roles: proto_roles, })) } async fn create_channel_role( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let params = CreateChannelRoleParams { name: req.name, description: None, permissions: req.permissions, assignable: req.assignable, }; let r = self .service .im .channel_role_create(&session, channel_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateChannelRoleResponse { role: Some(crate::pb::im::ChannelRole { id: r.id.to_string(), channel_id: r.channel_id.to_string(), name: r.name, permissions: r.permissions.iter().map(|p| p.to_string()).collect(), assignable: r.assignable, created_at: to_proto_ts(r.created_at), updated_at: to_proto_ts(r.updated_at), }), })) } async fn update_channel_role( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let role_id = parse_uuid(&req.role_id, "role_id")?; let session = system_session(); let params = UpdateChannelRoleParams { name: req.name, description: None, permissions: if req.permissions.is_empty() { None } else { Some(req.permissions) }, assignable: req.assignable, }; let r = self .service .im .channel_role_update(&session, role_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateChannelRoleResponse { role: Some(crate::pb::im::ChannelRole { id: r.id.to_string(), channel_id: r.channel_id.to_string(), name: r.name, permissions: r.permissions.iter().map(|p| p.to_string()).collect(), assignable: r.assignable, created_at: to_proto_ts(r.created_at), updated_at: to_proto_ts(r.updated_at), }), })) } async fn delete_channel_role( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let role_id = parse_uuid(&req.role_id, "role_id")?; let session = system_session(); self.service .im .channel_role_delete(&session, role_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteChannelRoleResponse {})) } } // Section: ChannelInvitationGrpcService pub struct ChannelInvitationGrpcService { service: AppService, } impl ChannelInvitationGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn invitation_to_proto(inv: crate::models::channels::ChannelInvitation) -> crate::pb::im::ChannelInvitation { let status = if inv.accepted_at.is_some() { "accepted" } else if inv.revoked_at.is_some() { "revoked" } else { "pending" }; crate::pb::im::ChannelInvitation { id: inv.id.to_string(), channel_id: inv.channel_id.to_string(), invited_by: inv.invited_by.to_string(), invited_user_id: inv.invited_user_id.map(|id| id.to_string()).unwrap_or_default(), role: inv.role.to_string(), status: status.to_string(), created_at: to_proto_ts(inv.created_at), updated_at: inv.accepted_at.or(inv.revoked_at).map(to_proto_ts).flatten().or_else(|| to_proto_ts(inv.created_at)), } } #[tonic::async_trait] impl ChannelInvitationService for ChannelInvitationGrpcService { async fn list_invitations( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let invitations = self .service .im .invitation_list(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListInvitationsResponse { invitations: invitations.into_iter().map(invitation_to_proto).collect(), })) } async fn create_invitation( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let invited_user_id = parse_uuid(&req.invited_user_id, "invited_user_id")?; let session = system_session(); let params = CreateInvitationParams { invited_user_id: Some(invited_user_id), email: None, role: Some(req.role), expires_in_hours: None, }; let inv = self .service .im .invitation_create(&session, channel_id, Uuid::nil(), params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateInvitationResponse { invitation: Some(invitation_to_proto(inv)), })) } async fn accept_invitation( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let invitation_id = parse_uuid(&req.invitation_id, "invitation_id")?; let session = system_session(); let inv = self .service .im .invitation_accept(&session, invitation_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(AcceptInvitationResponse { invitation: Some(invitation_to_proto(inv)), })) } async fn revoke_invitation( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let invitation_id = parse_uuid(&req.invitation_id, "invitation_id")?; let session = system_session(); self.service .im .invitation_revoke(&session, invitation_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(RevokeInvitationResponse {})) } } // Section: ChannelWebhookGrpcService pub struct ChannelWebhookGrpcService { service: AppService, } impl ChannelWebhookGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn webhook_to_proto(wh: crate::models::channels::ChannelWebhook) -> crate::pb::im::ChannelWebhook { crate::pb::im::ChannelWebhook { id: wh.id.to_string(), channel_id: wh.channel_id.to_string(), name: wh.name, url: wh.url, secret: wh.secret_ciphertext.unwrap_or_default(), events: wh.events.iter().map(|e| e.to_string()).collect(), active: wh.active, created_at: to_proto_ts(wh.created_at), updated_at: to_proto_ts(wh.updated_at), } } #[tonic::async_trait] impl ChannelWebhookService for ChannelWebhookGrpcService { async fn list_webhooks( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let webhooks = self .service .im .webhook_list(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListWebhooksResponse { webhooks: webhooks.into_iter().map(webhook_to_proto).collect(), })) } async fn create_webhook( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let params = CreateWebhookParams { name: req.name, url: req.url, secret: req.secret, events: req.events, }; let wh = self .service .im .webhook_create(&session, channel_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateWebhookResponse { webhook: Some(webhook_to_proto(wh)), })) } async fn update_webhook( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let webhook_id = parse_uuid(&req.webhook_id, "webhook_id")?; let session = system_session(); let params = UpdateWebhookParams { name: req.name, url: req.url, secret: req.secret, events: if req.events.is_empty() { None } else { Some(req.events) }, active: req.active, }; let wh = self .service .im .webhook_update(&session, webhook_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateWebhookResponse { webhook: Some(webhook_to_proto(wh)), })) } async fn delete_webhook( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let webhook_id = parse_uuid(&req.webhook_id, "webhook_id")?; let session = system_session(); self.service .im .webhook_delete(&session, webhook_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteWebhookResponse {})) } } // Section: ChannelSlashCommandGrpcService pub struct ChannelSlashCommandGrpcService { service: AppService, } impl ChannelSlashCommandGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn slash_command_to_proto(cmd: crate::models::channels::ChannelSlashCommand) -> crate::pb::im::ChannelSlashCommand { crate::pb::im::ChannelSlashCommand { id: cmd.id.to_string(), channel_id: cmd.channel_id.map(|id| id.to_string()).unwrap_or_default(), command: cmd.command, description: cmd.description.unwrap_or_default(), request_url: cmd.request_url, scopes: cmd.scopes.iter().map(|s| s.to_string()).collect(), created_at: to_proto_ts(cmd.created_at), updated_at: to_proto_ts(cmd.updated_at), } } #[tonic::async_trait] impl ChannelSlashCommandService for ChannelSlashCommandGrpcService { async fn list_slash_commands( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let commands = self .service .im .slash_command_list(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListSlashCommandsResponse { commands: commands.into_iter().map(slash_command_to_proto).collect(), })) } async fn create_slash_command( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let params = CreateSlashCommandParams { command: req.command, description: Some(req.description), request_url: req.request_url, secret: None, scopes: req.scopes, }; let cmd = self .service .im .slash_command_create(&session, channel_id, Uuid::nil(), params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateSlashCommandResponse { command: Some(slash_command_to_proto(cmd)), })) } async fn update_slash_command( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let command_id = parse_uuid(&req.command_id, "command_id")?; let session = system_session(); let params = UpdateSlashCommandParams { command: None, description: req.description, request_url: req.request_url, secret: None, scopes: if req.scopes.is_empty() { None } else { Some(req.scopes) }, enabled: None, }; let cmd = self .service .im .slash_command_update(&session, command_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateSlashCommandResponse { command: Some(slash_command_to_proto(cmd)), })) } async fn delete_slash_command( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let command_id = parse_uuid(&req.command_id, "command_id")?; let session = system_session(); self.service .im .slash_command_delete(&session, command_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteSlashCommandResponse {})) } } // Section: ChannelRepoLinkGrpcService pub struct ChannelRepoLinkGrpcService { service: AppService, } impl ChannelRepoLinkGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn repo_link_to_proto(link: crate::models::channels::ChannelRepoLink) -> crate::pb::im::ChannelRepoLink { crate::pb::im::ChannelRepoLink { id: link.id.to_string(), channel_id: link.channel_id.to_string(), repo_id: link.repo_id.to_string(), link_type: link.link_type.to_string(), events: link.notify_events.iter().map(|e| e.to_string()).collect(), created_at: to_proto_ts(link.created_at), updated_at: to_proto_ts(link.updated_at), } } #[tonic::async_trait] impl ChannelRepoLinkService for ChannelRepoLinkGrpcService { async fn list_repo_links( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let links = self .service .im .repo_link_list(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListRepoLinksResponse { links: links.into_iter().map(repo_link_to_proto).collect(), })) } async fn create_repo_link( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let repo_id = parse_uuid(&req.repo_id, "repo_id")?; let session = system_session(); let params = CreateRepoLinkParams { repo_id, link_type: req.link_type, notify_events: req.events, }; let link = self .service .im .repo_link_create(&session, channel_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateRepoLinkResponse { link: Some(repo_link_to_proto(link)), })) } async fn delete_repo_link( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let link_id = parse_uuid(&req.link_id, "link_id")?; let session = system_session(); self.service .im .repo_link_delete(&session, link_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteRepoLinkResponse {})) } } // Section: ImIntegrationGrpcService pub struct ImIntegrationGrpcService { service: AppService, } impl ImIntegrationGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn integration_to_proto(integ: crate::models::channels::ImIntegration) -> crate::pb::im::ImIntegration { crate::pb::im::ImIntegration { id: integ.id.to_string(), channel_id: integ.internal_channel_id.map(|id| id.to_string()).unwrap_or_default(), provider: integ.provider.to_string(), external_channel_id: integ.external_channel_id.unwrap_or_default(), sync_direction: integ.sync_direction.to_string(), active: integ.enabled, created_at: to_proto_ts(integ.created_at), updated_at: to_proto_ts(integ.updated_at), } } #[tonic::async_trait] impl ImIntegrationService for ImIntegrationGrpcService { async fn list_integrations( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let workspace_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let integrations = self .service .im .integration_list(&session, workspace_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListIntegrationsResponse { integrations: integrations.into_iter().map(integration_to_proto).collect(), })) } async fn create_integration( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let workspace_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let params = CreateIntegrationParams { provider: req.provider, name: String::new(), external_workspace_id: None, internal_channel_id: None, external_channel_id: Some(req.external_channel_id), bot_token: None, webhook_url: None, sync_direction: req.sync_direction, }; let integ = self .service .im .integration_create(&session, workspace_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateIntegrationResponse { integration: Some(integration_to_proto(integ)), })) } async fn update_integration( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let integration_id = parse_uuid(&req.integration_id, "integration_id")?; let session = system_session(); let params = UpdateIntegrationParams { name: None, external_channel_id: None, webhook_url: None, sync_direction: req.sync_direction, enabled: req.active, }; let integ = self .service .im .integration_update(&session, integration_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateIntegrationResponse { integration: Some(integration_to_proto(integ)), })) } async fn delete_integration( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let integration_id = parse_uuid(&req.integration_id, "integration_id")?; let session = system_session(); self.service .im .integration_delete(&session, integration_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteIntegrationResponse {})) } } // Section: CustomEmojiGrpcService pub struct CustomEmojiGrpcService { service: AppService, } impl CustomEmojiGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn emoji_to_proto(e: crate::models::channels::CustomEmoji) -> crate::pb::im::CustomEmoji { crate::pb::im::CustomEmoji { id: e.id.to_string(), workspace_id: e.workspace_id.to_string(), name: e.name, image_url: e.url, created_at: to_proto_ts(e.created_at), } } #[tonic::async_trait] impl CustomEmojiService for CustomEmojiGrpcService { async fn list_custom_emojis( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let workspace_id = parse_uuid(&req.workspace_id, "workspace_id")?; let session = system_session(); let emojis = self .service .im .custom_emoji_list(&session, workspace_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListCustomEmojisResponse { emojis: emojis.into_iter().map(emoji_to_proto).collect(), })) } async fn create_custom_emoji( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let workspace_id = parse_uuid(&req.workspace_id, "workspace_id")?; let session = system_session(); let params = CreateCustomEmojiParams { name: req.name, url: req.image_url, animated: None, }; let emoji = self .service .im .custom_emoji_create(&session, workspace_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateCustomEmojiResponse { emoji: Some(emoji_to_proto(emoji)), })) } async fn delete_custom_emoji( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let emoji_id = parse_uuid(&req.emoji_id, "emoji_id")?; let session = system_session(); self.service .im .custom_emoji_delete(&session, emoji_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteCustomEmojiResponse {})) } } // Section: ForumTagGrpcService pub struct ForumTagGrpcService { service: AppService, } impl ForumTagGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn forum_tag_to_proto(t: crate::models::channels::ForumTag) -> crate::pb::im::ForumTag { crate::pb::im::ForumTag { id: t.id.to_string(), channel_id: t.channel_id.to_string(), name: t.name, moderated: t.moderated, position: t.position, created_at: to_proto_ts(t.created_at), updated_at: to_proto_ts(t.updated_at), } } #[tonic::async_trait] impl ForumTagService for ForumTagGrpcService { async fn list_forum_tags( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let tags = self .service .im .forum_tag_list(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListForumTagsResponse { tags: tags.into_iter().map(forum_tag_to_proto).collect(), })) } async fn create_forum_tag( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let params = CreateForumTagParams { name: req.name, emoji_id: None, emoji_name: None, moderated: Some(req.moderated), position: req.position, }; let tag = self .service .im .forum_tag_create(&session, channel_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateForumTagResponse { tag: Some(forum_tag_to_proto(tag)), })) } async fn update_forum_tag( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let tag_id = parse_uuid(&req.tag_id, "tag_id")?; let session = system_session(); let params = UpdateForumTagParams { name: req.name, emoji_id: None, emoji_name: None, moderated: req.moderated, position: req.position, }; let tag = self .service .im .forum_tag_update(&session, tag_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateForumTagResponse { tag: Some(forum_tag_to_proto(tag)), })) } async fn delete_forum_tag( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let tag_id = parse_uuid(&req.tag_id, "tag_id")?; let session = system_session(); self.service .im .forum_tag_delete(&session, tag_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteForumTagResponse {})) } } // Section: VoiceGrpcService pub struct VoiceGrpcService { service: AppService, } impl VoiceGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn voice_participant_to_proto(p: crate::models::channels::VoiceParticipant) -> crate::pb::im::VoiceParticipant { crate::pb::im::VoiceParticipant { id: p.id.to_string(), channel_id: p.channel_id.to_string(), user_id: p.user_id.to_string(), muted: p.muted, deafened: p.deafened, joined_at: to_proto_ts(p.joined_at), } } #[tonic::async_trait] impl VoiceService for VoiceGrpcService { async fn list_voice_participants( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let participants = self .service .im .voice_participant_list(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(ListVoiceParticipantsResponse { participants: participants .into_iter() .map(voice_participant_to_proto) .collect(), })) } async fn update_voice_state( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let params = UpdateVoiceStateParams { session_id: None, muted: req.muted, deafened: req.deafened, self_muted: None, self_deafened: None, self_video: None, streaming: None, }; let p = self .service .im .voice_state_update(&session, channel_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateVoiceStateResponse { participant: Some(voice_participant_to_proto(p)), })) } } // Section: StageGrpcService pub struct StageGrpcService { service: AppService, } impl StageGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn stage_to_proto(s: crate::models::channels::Stage) -> crate::pb::im::Stage { crate::pb::im::Stage { id: s.id.to_string(), channel_id: s.channel_id.to_string(), topic: s.topic, privacy_level: s.privacy_level.to_string(), discoverable: s.discoverable, started_at: to_proto_ts(s.started_at), ended_at: s.ended_at.map(to_proto_ts).flatten(), created_at: to_proto_ts(s.created_at), updated_at: to_proto_ts(s.updated_at), } } #[tonic::async_trait] impl StageService for StageGrpcService { async fn get_stage( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let stage = self .service .im .stage_get(&session, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(GetStageResponse { stage: stage.map(stage_to_proto), })) } async fn create_stage( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let params = CreateStageParams { topic: req.topic, privacy_level: Some(req.privacy_level), discoverable: Some(req.discoverable), }; let stage = self .service .im .stage_create(&session, channel_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(CreateStageResponse { stage: Some(stage_to_proto(stage)), })) } async fn update_stage( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let stage_id = parse_uuid(&req.stage_id, "stage_id")?; let session = system_session(); let params = UpdateStageParams { topic: req.topic, privacy_level: req.privacy_level, discoverable: req.discoverable, }; let stage = self .service .im .stage_update(&session, stage_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateStageResponse { stage: Some(stage_to_proto(stage)), })) } async fn delete_stage( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let stage_id = parse_uuid(&req.stage_id, "stage_id")?; let session = system_session(); self.service .im .stage_delete(&session, stage_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(DeleteStageResponse {})) } } // Section: ChannelAuditGrpcService pub struct ChannelAuditGrpcService { service: AppService, } impl ChannelAuditGrpcService { pub fn new(service: AppService) -> Self { Self { service } } } fn audit_event_to_proto(e: crate::models::channels::ChannelEvent) -> crate::pb::im::ChannelAuditEvent { crate::pb::im::ChannelAuditEvent { id: e.id.to_string(), channel_id: e.channel_id.to_string(), actor_id: e.actor_id.map(|id| id.to_string()).unwrap_or_default(), event_type: e.event_type.to_string(), target_type: e.target_type.map(|t| t.to_string()).unwrap_or_default(), target_id: e.target_id.map(|id| id.to_string()).unwrap_or_default(), old_value: e.old_value.map(|v| v.to_string()), new_value: e.new_value.map(|v| v.to_string()), created_at: to_proto_ts(e.created_at), } } #[tonic::async_trait] impl ChannelAuditService for ChannelAuditGrpcService { async fn list_channel_events( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = parse_uuid(&req.channel_id, "channel_id")?; let session = system_session(); let events = self .service .im .audit_list(&session, channel_id, req.limit as i64, req.offset as i64) .await .map_err(|e| Status::internal(e.to_string()))?; let total = events.len() as i32; Ok(Response::new(ListChannelEventsResponse { events: events.into_iter().map(audit_event_to_proto).collect(), total, })) } } pub struct ChannelSettingsServices { pub channel_role: ChannelRoleGrpcService, pub channel_invitation: ChannelInvitationGrpcService, pub channel_webhook: ChannelWebhookGrpcService, pub channel_slash_command: ChannelSlashCommandGrpcService, pub channel_repo_link: ChannelRepoLinkGrpcService, pub im_integration: ImIntegrationGrpcService, pub custom_emoji: CustomEmojiGrpcService, pub forum_tag: ForumTagGrpcService, pub voice: VoiceGrpcService, pub stage: StageGrpcService, pub channel_audit: ChannelAuditGrpcService, } impl ChannelSettingsServices { pub fn new(service: crate::service::AppService) -> Self { Self { channel_role: ChannelRoleGrpcService::new(service.clone()), channel_invitation: ChannelInvitationGrpcService::new(service.clone()), channel_webhook: ChannelWebhookGrpcService::new(service.clone()), channel_slash_command: ChannelSlashCommandGrpcService::new(service.clone()), channel_repo_link: ChannelRepoLinkGrpcService::new(service.clone()), im_integration: ImIntegrationGrpcService::new(service.clone()), custom_emoji: CustomEmojiGrpcService::new(service.clone()), forum_tag: ForumTagGrpcService::new(service.clone()), voice: VoiceGrpcService::new(service.clone()), stage: StageGrpcService::new(service.clone()), channel_audit: ChannelAuditGrpcService::new(service), } } }