From c7cce959c7c6ce0bc83cac9eb2b23f1abdf8dab7 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 24 Jan 2025 22:52:37 +0900 Subject: [PATCH 1/7] :sparkles: Add `TraqUserService::find_traq_user` --- server/src/traq/user.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/server/src/traq/user.rs b/server/src/traq/user.rs index 0ba8fac1..03cf455f 100644 --- a/server/src/traq/user.rs +++ b/server/src/traq/user.rs @@ -22,6 +22,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 +37,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 +67,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, From 1ca587627c732f2b26f63943154730057790b691 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 24 Jan 2025 22:57:53 +0900 Subject: [PATCH 2/7] :construction: WIP impl `TraqUserService` --- server/src/traq/user.rs | 8 ++++++++ server/src/traq/user/error.rs | 25 +++++++++++++++++++++++++ server/src/traq/user/impl.rs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 server/src/traq/user/error.rs create mode 100644 server/src/traq/user/impl.rs diff --git a/server/src/traq/user.rs b/server/src/traq/user.rs index 03cf455f..83357bd9 100644 --- a/server/src/traq/user.rs +++ b/server/src/traq/user.rs @@ -5,6 +5,11 @@ use serde::{Deserialize, Serialize}; use crate::prelude::IntoStatus; +pub mod error; +mod r#impl; + +pub use error::Error; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] pub struct TraqUserId(pub uuid::Uuid); @@ -88,3 +93,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..9a629001 --- /dev/null +++ b/server/src/traq/user/error.rs @@ -0,0 +1,25 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Not found")] + NotFound, + #[error(transparent)] + Sqlx(#[from] sqlx::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::Sqlx(e) => { + tracing::error!(error = &e as &dyn std::error::Error, "Database error"); + tonic::Status::internal("database 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..7465fe2a --- /dev/null +++ b/server/src/traq/user/impl.rs @@ -0,0 +1,35 @@ +use sqlx::MySqlPool; + +use crate::prelude::IntoStatus; +use crate::user::ProvideUserService; + +impl super::TraqUserService for super::TraqUserServiceImpl +where + Context: AsRef + ProvideUserService, +{ + type Error = super::Error; + + fn get_traq_user<'a>( + &'a self, + ctx: &'a Context, + params: super::GetTraqUserParams, + ) -> futures::future::BoxFuture<'a, Result> { + todo!() + } + + fn find_traq_user<'a>( + &'a self, + ctx: &'a Context, + params: super::FindTraqUserParams, + ) -> futures::future::BoxFuture<'a, Result, Self::Error>> { + todo!() + } + + fn register_traq_user<'a>( + &'a self, + ctx: &'a Context, + params: super::RegisterTraqUserParams, + ) -> futures::future::BoxFuture<'a, Result> { + todo!() + } +} From 9f61d6e57051b84cb3a4b76d85d9208b059b21b2 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 24 Jan 2025 23:02:06 +0900 Subject: [PATCH 3/7] :recycle: Add `created_at`, `updated_at` in `TraqUser` --- server/src/traq/user.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/traq/user.rs b/server/src/traq/user.rs index 83357bd9..fd1be2e0 100644 --- a/server/src/traq/user.rs +++ b/server/src/traq/user.rs @@ -3,7 +3,7 @@ use futures::future::BoxFuture; use serde::{Deserialize, Serialize}; -use crate::prelude::IntoStatus; +use crate::prelude::{IntoStatus, Timestamp}; pub mod error; mod r#impl; @@ -20,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)] From 42f2b78371983c9cb991bf1d77b447d604268cb4 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 24 Jan 2025 23:02:37 +0900 Subject: [PATCH 4/7] :sparkles: Add migration `traq_users` --- server/migrations/2_init_traq.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 server/migrations/2_init_traq.sql 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`) +); From f70d2a48a2bd6dbec302c1257f78df8ea9d3a902 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 24 Jan 2025 23:21:11 +0900 Subject: [PATCH 5/7] :sparkles: Add `TraqHost` --- server/src/traq.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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) + } +} From 3c86a943ed4e5d951fb4d4a5cf30ab031e3c4fc9 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 24 Jan 2025 23:50:02 +0900 Subject: [PATCH 6/7] :sparkles: Add `traq::user::Error` variants --- server/src/traq/user/error.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/src/traq/user/error.rs b/server/src/traq/user/error.rs index 9a629001..3d75ad6c 100644 --- a/server/src/traq/user/error.rs +++ b/server/src/traq/user/error.rs @@ -2,9 +2,13 @@ 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), } @@ -12,10 +16,18 @@ 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 From 39d1711b1988bff52501e1c243ab37155b730a12 Mon Sep 17 00:00:00 2001 From: H1rono Date: Fri, 24 Jan 2025 23:50:49 +0900 Subject: [PATCH 7/7] :sparkles: Impl --- server/src/traq/user/impl.rs | 148 +++++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 8 deletions(-) diff --git a/server/src/traq/user/impl.rs b/server/src/traq/user/impl.rs index 7465fe2a..e1ec6647 100644 --- a/server/src/traq/user/impl.rs +++ b/server/src/traq/user/impl.rs @@ -1,11 +1,16 @@ -use sqlx::MySqlPool; +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 + ProvideUserService, + Context: AsRef + AsRef + ProvideUserService + ProvideTraqBotService, { type Error = super::Error; @@ -13,23 +18,150 @@ where &'a self, ctx: &'a Context, params: super::GetTraqUserParams, - ) -> futures::future::BoxFuture<'a, Result> { - todo!() + ) -> 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, - ) -> futures::future::BoxFuture<'a, Result, Self::Error>> { - todo!() + ) -> 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, - ) -> futures::future::BoxFuture<'a, Result> { - todo!() + ) -> 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) +}