420dedbc1e
- Add IM service modules: audit, channel roles, custom emojis, forum tags, integrations, invitations, repo links, slash commands, stages, voice, webhooks - Add PR service modules: review requests, templates - Add repo service modules: contributors, release assets, git extras (archive, branch rename, commit extras, diff/merge, tag, tree) - Add user service: social (follow/block) - Add internal auth service - Update existing service modules with expanded functionality - Remove deleted IM modules: articles, delivery trace, drafts, follows, messages, polls, presence, reactions, threads
227 lines
7.8 KiB
Rust
227 lines
7.8 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
use crate::error::AppError;
|
|
use crate::models::common::{DeviceType, PresenceStatus};
|
|
use crate::models::users::{UserBlock, UserFollow, UserPresence};
|
|
use crate::service::UserService;
|
|
use crate::session::Session;
|
|
|
|
use super::util::ensure_affected;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
pub struct UpdatePresenceParams {
|
|
pub status: PresenceStatus,
|
|
pub custom_status_text: Option<String>,
|
|
pub custom_status_emoji: Option<String>,
|
|
pub device_type: Option<DeviceType>,
|
|
pub ip_address: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
struct UserPresenceRow {
|
|
id: Uuid,
|
|
user_id: Uuid,
|
|
status: PresenceStatus,
|
|
custom_status_text: Option<String>,
|
|
custom_status_emoji: Option<String>,
|
|
device_type: Option<DeviceType>,
|
|
ip_address: Option<String>,
|
|
last_active_at: DateTime<Utc>,
|
|
last_seen_at: Option<DateTime<Utc>>,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl From<UserPresenceRow> for UserPresence {
|
|
fn from(row: UserPresenceRow) -> Self {
|
|
Self {
|
|
id: row.id,
|
|
user_id: row.user_id,
|
|
status: row.status,
|
|
custom_status_text: row.custom_status_text,
|
|
custom_status_emoji: row.custom_status_emoji,
|
|
device_type: row.device_type,
|
|
ip_address: row.ip_address,
|
|
last_active_at: row.last_active_at,
|
|
last_seen_at: row.last_seen_at,
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl UserService {
|
|
pub async fn user_presence_get(&self, session: &Session) -> Result<UserPresence, AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
let row = sqlx::query_as::<_, UserPresenceRow>(
|
|
"SELECT id, user_id, status, custom_status_text, custom_status_emoji, device_type, ip_address, \
|
|
last_active_at, last_seen_at, created_at, updated_at \
|
|
FROM user_presence WHERE user_id = $1",
|
|
)
|
|
.bind(user_uid)
|
|
.fetch_optional(self.ctx.db.reader())
|
|
.await
|
|
.map_err(AppError::Database)?;
|
|
row.map(Into::into)
|
|
.ok_or_else(|| AppError::NotFound("presence not found".into()))
|
|
}
|
|
|
|
pub async fn user_presence_update(
|
|
&self,
|
|
session: &Session,
|
|
params: UpdatePresenceParams,
|
|
) -> Result<UserPresence, AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
let id = Uuid::now_v7();
|
|
let now = chrono::Utc::now();
|
|
|
|
let row = sqlx::query_as::<_, UserPresenceRow>(
|
|
"INSERT INTO user_presence (id, user_id, status, custom_status_text, custom_status_emoji, \
|
|
device_type, ip_address, last_active_at, last_seen_at, created_at, updated_at) \
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NULL, $9, $9) \
|
|
ON CONFLICT (user_id) DO UPDATE SET \
|
|
status = $3, custom_status_text = $4, custom_status_emoji = $5, \
|
|
device_type = $6, ip_address = $7, last_active_at = $8, updated_at = $9 \
|
|
RETURNING id, user_id, status, custom_status_text, custom_status_emoji, device_type, ip_address, \
|
|
last_active_at, last_seen_at, created_at, updated_at",
|
|
)
|
|
.bind(id)
|
|
.bind(user_uid)
|
|
.bind(¶ms.status)
|
|
.bind(¶ms.custom_status_text)
|
|
.bind(¶ms.custom_status_emoji)
|
|
.bind(¶ms.device_type)
|
|
.bind(¶ms.ip_address)
|
|
.bind(now)
|
|
.bind(now)
|
|
.fetch_one(self.ctx.db.writer())
|
|
.await
|
|
.map_err(AppError::Database)?;
|
|
Ok(row.into())
|
|
}
|
|
|
|
pub async fn user_blocks_list(
|
|
&self,
|
|
session: &Session,
|
|
limit: i64,
|
|
offset: i64,
|
|
) -> Result<Vec<UserBlock>, AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
let limit = limit.clamp(1, 100);
|
|
let offset = offset.max(0);
|
|
sqlx::query_as::<_, UserBlock>(
|
|
"SELECT blocker_id, blocked_id, reason, created_at \
|
|
FROM user_block WHERE blocker_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
|
)
|
|
.bind(user_uid)
|
|
.bind(limit)
|
|
.bind(offset)
|
|
.fetch_all(self.ctx.db.reader())
|
|
.await
|
|
.map_err(AppError::Database)
|
|
}
|
|
|
|
pub async fn user_block_create(
|
|
&self,
|
|
session: &Session,
|
|
target_user_id: Uuid,
|
|
reason: Option<String>,
|
|
) -> Result<UserBlock, AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
if user_uid == target_user_id {
|
|
return Err(AppError::BadRequest("cannot block yourself".into()));
|
|
}
|
|
let now = chrono::Utc::now();
|
|
sqlx::query_as::<_, UserBlock>(
|
|
"INSERT INTO user_block (blocker_id, blocked_id, reason, created_at) \
|
|
VALUES ($1, $2, $3, $4) \
|
|
RETURNING blocker_id, blocked_id, reason, created_at",
|
|
)
|
|
.bind(user_uid)
|
|
.bind(target_user_id)
|
|
.bind(&reason)
|
|
.bind(now)
|
|
.fetch_one(self.ctx.db.writer())
|
|
.await
|
|
.map_err(AppError::Database)
|
|
}
|
|
|
|
pub async fn user_block_delete(
|
|
&self,
|
|
session: &Session,
|
|
target_user_id: Uuid,
|
|
) -> Result<(), AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
let result =
|
|
sqlx::query("DELETE FROM user_block WHERE blocker_id = $1 AND blocked_id = $2")
|
|
.bind(user_uid)
|
|
.bind(target_user_id)
|
|
.execute(self.ctx.db.writer())
|
|
.await
|
|
.map_err(AppError::Database)?;
|
|
ensure_affected(result.rows_affected(), "block not found")
|
|
}
|
|
|
|
pub async fn user_follows_list(
|
|
&self,
|
|
session: &Session,
|
|
limit: i64,
|
|
offset: i64,
|
|
) -> Result<Vec<UserFollow>, AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
let limit = limit.clamp(1, 100);
|
|
let offset = offset.max(0);
|
|
sqlx::query_as::<_, UserFollow>(
|
|
"SELECT follower_id, following_id, created_at \
|
|
FROM user_follow WHERE follower_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
|
)
|
|
.bind(user_uid)
|
|
.bind(limit)
|
|
.bind(offset)
|
|
.fetch_all(self.ctx.db.reader())
|
|
.await
|
|
.map_err(AppError::Database)
|
|
}
|
|
|
|
pub async fn user_follow_create(
|
|
&self,
|
|
session: &Session,
|
|
target_user_id: Uuid,
|
|
) -> Result<UserFollow, AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
if user_uid == target_user_id {
|
|
return Err(AppError::BadRequest("cannot follow yourself".into()));
|
|
}
|
|
let now = chrono::Utc::now();
|
|
sqlx::query_as::<_, UserFollow>(
|
|
"INSERT INTO user_follow (follower_id, following_id, created_at) \
|
|
VALUES ($1, $2, $3) \
|
|
RETURNING follower_id, following_id, created_at",
|
|
)
|
|
.bind(user_uid)
|
|
.bind(target_user_id)
|
|
.bind(now)
|
|
.fetch_one(self.ctx.db.writer())
|
|
.await
|
|
.map_err(AppError::Database)
|
|
}
|
|
|
|
pub async fn user_follow_delete(
|
|
&self,
|
|
session: &Session,
|
|
target_user_id: Uuid,
|
|
) -> Result<(), AppError> {
|
|
let user_uid = session.user().ok_or(AppError::Unauthorized)?;
|
|
let result =
|
|
sqlx::query("DELETE FROM user_follow WHERE follower_id = $1 AND following_id = $2")
|
|
.bind(user_uid)
|
|
.bind(target_user_id)
|
|
.execute(self.ctx.db.writer())
|
|
.await
|
|
.map_err(AppError::Database)?;
|
|
ensure_affected(result.rows_affected(), "follow not found")
|
|
}
|
|
}
|