Files
gitks/grpc/member.rs
T
zhenyi 1000f8a80d chore(infra): add gRPC layer, update protobufs, remove immediate module
- 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
2026-06-10 18:49:42 +08:00

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 }))
}
}