1000f8a80d
- Add gRPC service modules: auth, channel, channel settings, member, permission - Update protobuf definitions and generated code - Remove immediate/ real-time module (superseded by IM service) - Update etcd discovery and registration - Update cache, error, config, and build infrastructure - Add ADR documentation - Update OpenAPI spec
244 lines
8.2 KiB
Rust
244 lines
8.2 KiB
Rust
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<String, Status> {
|
|
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, Status> {
|
|
Uuid::parse_str(s).map_err(|_| Status::invalid_argument(format!("invalid {}", field)))
|
|
}
|
|
|
|
fn to_timestamp(dt: chrono::DateTime<chrono::Utc>) -> 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<ListMembersRequest>,
|
|
) -> Result<Response<ListMembersResponse>, 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<PbChannelMember> = 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<InviteMemberRequest>,
|
|
) -> Result<Response<InviteMemberResponse>, 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<UpdateMemberRequest>,
|
|
) -> Result<Response<UpdateMemberResponse>, 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<KickMemberRequest>,
|
|
) -> Result<Response<KickMemberResponse>, 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<JoinChannelRequest>,
|
|
) -> Result<Response<JoinChannelResponse>, 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<LeaveChannelRequest>,
|
|
) -> Result<Response<LeaveChannelResponse>, 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<IsMemberRequest>,
|
|
) -> Result<Response<IsMemberResponse>, 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 }))
|
|
}
|
|
}
|