use tonic::{Request, Response, Status}; use uuid::Uuid; use crate::models::channels::ChannelMember; use crate::pb::im::member_service_server::MemberService; use crate::pb::im::{ ChannelMember as PbChannelMember, InviteMemberRequest, InviteMemberResponse, IsMemberRequest, IsMemberResponse, JoinChannelRequest, JoinChannelResponse, KickMemberRequest, KickMemberResponse, LeaveChannelRequest, LeaveChannelResponse, ListMembersRequest, ListMembersResponse, UpdateMemberRequest, UpdateMemberResponse, }; use crate::service::im::session::ImSession; use crate::service::im::members::{InviteMemberParams, UpdateMemberParams}; use crate::service::AppService; pub struct MemberGrpcService { service: AppService, } impl MemberGrpcService { pub fn new(service: AppService) -> Self { Self { service } } async fn resolve_workspace_name(&self, channel_id: Uuid) -> Result { let channel = self .service .im .resolve_channel(channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; let ws_name: String = sqlx::query_scalar("SELECT name FROM workspace WHERE id = $1") .bind(channel.workspace_id) .fetch_optional(self.service.ctx.db.reader()) .await .map_err(|e| Status::internal(e.to_string()))? .ok_or_else(|| Status::not_found("workspace not found"))?; Ok(ws_name) } fn parse_uuid(s: &str, field: &str) -> Result { Uuid::parse_str(s).map_err(|_| Status::invalid_argument(format!("invalid {}", field))) } fn to_timestamp(dt: chrono::DateTime) -> prost_types::Timestamp { prost_types::Timestamp { seconds: dt.timestamp(), nanos: dt.timestamp_subsec_nanos() as i32, } } fn to_pb_member(m: ChannelMember) -> PbChannelMember { PbChannelMember { id: m.id.to_string(), channel_id: m.channel_id.to_string(), user_id: m.user_id.to_string(), role: m.role.to_string(), status: m.status.to_string(), muted: m.muted, pinned: m.pinned, last_read_message_id: m.last_read_message_id.map(|id| id.to_string()), last_read_at: m.last_read_at.map(Self::to_timestamp), joined_at: m.joined_at.map(Self::to_timestamp), left_at: m.left_at.map(Self::to_timestamp), created_at: Some(Self::to_timestamp(m.created_at)), updated_at: Some(Self::to_timestamp(m.updated_at)), } } } #[tonic::async_trait] impl MemberService for MemberGrpcService { async fn list_members( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?; let wk_name = self.resolve_workspace_name(channel_id).await?; let session = ImSession::new(Uuid::nil()); let members = self .service .im .member_list(&session, &wk_name, channel_id, req.limit as i64, req.offset as i64) .await .map_err(|e| Status::internal(e.to_string()))?; let pb_members: Vec = members.into_iter().map(Self::to_pb_member).collect(); let total = pb_members.len() as i32; Ok(Response::new(ListMembersResponse { members: pb_members, total, })) } async fn invite_member( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?; let user_id = Self::parse_uuid(&req.user_id, "user_id")?; let wk_name = self.resolve_workspace_name(channel_id).await?; let params = InviteMemberParams { user_id, role: req.role, }; let session = ImSession::new(Uuid::nil()); let member = self .service .im .member_invite(&session, &wk_name, channel_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(InviteMemberResponse { member: Some(Self::to_pb_member(member)), })) } async fn update_member( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?; let user_id = Self::parse_uuid(&req.user_id, "user_id")?; let wk_name = self.resolve_workspace_name(channel_id).await?; let params = UpdateMemberParams { role: req.role, muted: req.muted, pinned: req.pinned, }; let session = ImSession::new(Uuid::nil()); let member = self .service .im .member_update(&session, &wk_name, channel_id, user_id, params) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(UpdateMemberResponse { member: Some(Self::to_pb_member(member)), })) } async fn kick_member( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?; let user_id = Self::parse_uuid(&req.user_id, "user_id")?; let wk_name = self.resolve_workspace_name(channel_id).await?; let session = ImSession::new(Uuid::nil()); self.service .im .member_kick(&session, &wk_name, channel_id, user_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(KickMemberResponse {})) } async fn join_channel( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?; let user_id = Self::parse_uuid(&req.user_id, "user_id")?; let wk_name = self.resolve_workspace_name(channel_id).await?; let session = ImSession::new(user_id); let member = self .service .im .member_join(&session, &wk_name, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(JoinChannelResponse { member: Some(Self::to_pb_member(member)), })) } async fn leave_channel( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?; let user_id = Self::parse_uuid(&req.user_id, "user_id")?; let wk_name = self.resolve_workspace_name(channel_id).await?; let session = ImSession::new(user_id); self.service .im .member_leave(&session, &wk_name, channel_id) .await .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(LeaveChannelResponse {})) } async fn is_member( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); let channel_id = Self::parse_uuid(&req.channel_id, "channel_id")?; let user_id = Self::parse_uuid(&req.user_id, "user_id")?; let is_member = self .service .im .is_channel_member(channel_id, user_id) .await .map_err(|e| Status::internal(e.to_string()))?; let role = if is_member { self.service .im .channel_member_role(channel_id, user_id) .await .map_err(|e| Status::internal(e.to_string()))? .to_string() } else { String::new() }; Ok(Response::new(IsMemberResponse { is_member, role })) } }