diff --git a/server/migrations/2_init_traq.sql b/server/migrations/2_init_traq.sql new file mode 100644 index 00000000..155f3a91 --- /dev/null +++ b/server/migrations/2_init_traq.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `traq_users` ( + `id` BINARY(16) NOT NULL, + `user_id` BINARY(16) NOT NULL, + `bot` BOOLEAN NOT NULL, + `bio` TEXT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); diff --git a/server/src/traq.rs b/server/src/traq.rs index 92c6199d..de76a2ea 100644 --- a/server/src/traq.rs +++ b/server/src/traq.rs @@ -1,5 +1,19 @@ +use serde::{Deserialize, Serialize}; + pub mod auth; pub mod bot; pub mod channel; pub mod message; pub mod user; + +/// traQサーバーのホスト名 +/// ex. `q.trap.jp` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(transparent)] +pub struct TraqHost(pub String); + +impl std::fmt::Display for TraqHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} diff --git a/server/src/traq/user.rs b/server/src/traq/user.rs index 0ba8fac1..fd1be2e0 100644 --- a/server/src/traq/user.rs +++ b/server/src/traq/user.rs @@ -3,7 +3,12 @@ use futures::future::BoxFuture; use serde::{Deserialize, Serialize}; -use crate::prelude::IntoStatus; +use crate::prelude::{IntoStatus, Timestamp}; + +pub mod error; +mod r#impl; + +pub use error::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] @@ -15,6 +20,8 @@ pub struct TraqUser { pub inner: crate::user::User, pub bot: bool, pub bio: String, + pub created_at: Timestamp, + pub updated_at: Timestamp, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] @@ -22,6 +29,8 @@ pub struct GetTraqUserParams { pub id: TraqUserId, } +pub type FindTraqUserParams = GetTraqUserParams; + #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct RegisterTraqUserParams { pub id: TraqUserId, @@ -35,6 +44,11 @@ pub trait TraqUserService: Send + Sync + 'static { ctx: &'a Context, params: GetTraqUserParams, ) -> BoxFuture<'a, Result>; + fn find_traq_user<'a>( + &'a self, + ctx: &'a Context, + params: FindTraqUserParams, + ) -> BoxFuture<'a, Result, Self::Error>>; fn register_traq_user<'a>( &'a self, ctx: &'a Context, @@ -60,6 +74,16 @@ pub trait ProvideTraqUserService: Send + Sync + 'static { let ctx = self.context(); self.traq_user_service().get_traq_user(ctx, params) } + fn find_traq_user( + &self, + params: FindTraqUserParams, + ) -> BoxFuture< + '_, + Result, >::Error>, + > { + let ctx = self.context(); + self.traq_user_service().find_traq_user(ctx, params) + } fn register_traq_user( &self, params: RegisterTraqUserParams, @@ -71,3 +95,6 @@ pub trait ProvideTraqUserService: Send + Sync + 'static { self.traq_user_service().register_traq_user(ctx, params) } } + +#[derive(Debug, Clone, Copy, Default)] +pub struct TraqUserServiceImpl; diff --git a/server/src/traq/user/error.rs b/server/src/traq/user/error.rs new file mode 100644 index 00000000..3d75ad6c --- /dev/null +++ b/server/src/traq/user/error.rs @@ -0,0 +1,37 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Not found")] + NotFound, + #[error("Received unexpected response from traQ")] + UnexpectedResponseFromTraq, + #[error(transparent)] + Sqlx(#[from] sqlx::Error), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] + Status(#[from] tonic::Status), +} + +impl From for tonic::Status { + fn from(value: Error) -> Self { + match value { + Error::NotFound => tonic::Status::not_found("Not found"), + Error::UnexpectedResponseFromTraq => { + tracing::warn!("Received unexpected response from traQ"); + tonic::Status::unknown("Received unexpected response from traQ") + } + Error::Sqlx(e) => { + tracing::error!(error = &e as &dyn std::error::Error, "Database error"); + tonic::Status::internal("database error") + } + Error::Reqwest(e) => { + tracing::error!(error = &e as &dyn std::error::Error, "HTTP request error"); + tonic::Status::unknown("HTTP request error") + } + Error::Status(s) => { + tracing::error!(error = &s as &dyn std::error::Error, "Unexpected"); + s + } + } + } +} diff --git a/server/src/traq/user/impl.rs b/server/src/traq/user/impl.rs new file mode 100644 index 00000000..e1ec6647 --- /dev/null +++ b/server/src/traq/user/impl.rs @@ -0,0 +1,167 @@ +use chrono::{DateTime, Utc}; +use futures::future::{self, BoxFuture, FutureExt, TryFutureExt}; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, MySqlPool}; +use uuid::Uuid; + +use crate::prelude::IntoStatus; +use crate::traq::{bot::ProvideTraqBotService, TraqHost}; +use crate::user::ProvideUserService; + +impl super::TraqUserService for super::TraqUserServiceImpl +where + Context: AsRef + AsRef + ProvideUserService + ProvideTraqBotService, +{ + type Error = super::Error; + + fn get_traq_user<'a>( + &'a self, + ctx: &'a Context, + params: super::GetTraqUserParams, + ) -> BoxFuture<'a, Result> { + find_traq_user(ctx, ctx.as_ref(), params.id) + .and_then(|u| future::ready(u.ok_or(super::Error::NotFound))) + .boxed() + } + + fn find_traq_user<'a>( + &'a self, + ctx: &'a Context, + params: super::FindTraqUserParams, + ) -> BoxFuture<'a, Result, Self::Error>> { + find_traq_user(ctx, ctx.as_ref(), params.id).boxed() + } + + fn register_traq_user<'a>( + &'a self, + ctx: &'a Context, + params: super::RegisterTraqUserParams, + ) -> BoxFuture<'a, Result> { + register_traq_user(ctx, ctx, ctx.as_ref(), ctx.as_ref(), params).boxed() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, FromRow)] +struct TraqUserRow { + pub id: Uuid, + pub user_id: Uuid, + pub bot: bool, + pub bio: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[tracing::instrument(skip_all)] +async fn find_traq_user( + user_service: &U, + pool: &MySqlPool, + id: super::TraqUserId, +) -> Result, super::Error> { + let traq_user: Option = + sqlx::query_as(r#"SELECT * FROM `traq_users` WHERE id = ?"#) + .bind(id.0) + .fetch_optional(pool) + .await?; + let Some(traq_user) = traq_user else { + return Ok(None); + }; + let user_id = crate::user::UserId(traq_user.user_id); + let user = user_service + .get_user(crate::user::GetUserParams { id: user_id }) + .await + .map_err(IntoStatus::into_status)?; + let traq_user = super::TraqUser { + id: super::TraqUserId(traq_user.id), + inner: user, + bot: traq_user.bot, + bio: traq_user.bio, + created_at: traq_user.created_at.into(), + updated_at: traq_user.updated_at.into(), + }; + Ok(Some(traq_user)) +} + +#[tracing::instrument(skip_all)] +async fn register_traq_user( + user_service: &U, + traq_bot_service: &B, + pool: &MySqlPool, + traq_host: &TraqHost, + params: super::RegisterTraqUserParams, +) -> Result { + use super::Error::UnexpectedResponseFromTraq as FailJson; + + let super::RegisterTraqUserParams { + id: super::TraqUserId(traq_user_id), + } = params; + + let uri = format!("https://{traq_host}/api/v3/users/{traq_user_id}"); + let request = crate::traq::bot::BuildRequestAsBotParams { + method: http::Method::GET, + uri: &uri, + }; + let request = traq_bot_service + .build_request_as_bot(request) + .await + .map_err(IntoStatus::into_status)?; + let response: serde_json::Value = request.send().await?.error_for_status()?.json().await?; + tracing::trace!(value = ?response, "Received response from traQ"); + + let response = response.as_object().ok_or(FailJson)?; + let id: Uuid = response + .get("id") + .ok_or(FailJson)? + .as_str() + .ok_or(FailJson)? + .parse() + .map_err(|e| { + tracing::warn!(error = &e as &dyn std::error::Error, "Failed to parse UUID"); + FailJson + })?; + let name = response + .get("name") + .ok_or(FailJson)? + .as_str() + .ok_or(FailJson)? + .to_string(); + let display_name = response + .get("displayName") + .ok_or(FailJson)? + .as_str() + .ok_or(FailJson)? + .to_string(); + let bot = response + .get("bot") + .ok_or(FailJson)? + .as_bool() + .ok_or(FailJson)?; + let bio = response + .get("bio") + .ok_or(FailJson)? + .as_str() + .ok_or(FailJson)?; + + let create_user = crate::user::CreateUserParams { name, display_name }; + let user = user_service + .create_user(create_user) + .await + .map_err(IntoStatus::into_status)?; + + sqlx::query( + r#" + INSERT INTO `traq_users` (`id`, `user_id`, `bot`, `bio`) + VALUES (?, ?, ?, ?) + "#, + ) + .bind(id) + .bind(user.id.0) + .bind(bot) + .bind(bio) + .execute(pool) + .await?; + tracing::debug!(traq_user_id = %id, user_id = %user.id.0, "Registered a traQ user"); + + find_traq_user(user_service, pool, super::TraqUserId(id)) + .await? + .ok_or(super::Error::NotFound) +}