Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TraqUserService書いた #70

Merged
merged 7 commits into from
Jan 24, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions server/migrations/2_init_traq.sql
Original file line number Diff line number Diff line change
@@ -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`)
);
14 changes: 14 additions & 0 deletions server/src/traq.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
29 changes: 28 additions & 1 deletion server/src/traq/user.rs
Original file line number Diff line number Diff line change
@@ -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,13 +20,17 @@ 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)]
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<Context>: Send + Sync + 'static {
ctx: &'a Context,
params: GetTraqUserParams,
) -> BoxFuture<'a, Result<TraqUser, Self::Error>>;
fn find_traq_user<'a>(
&'a self,
ctx: &'a Context,
params: FindTraqUserParams,
) -> BoxFuture<'a, Result<Option<TraqUser>, 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<Option<TraqUser>, <Self::TraqUserService as TraqUserService<Self::Context>>::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;
37 changes: 37 additions & 0 deletions server/src/traq/user/error.rs
Original file line number Diff line number Diff line change
@@ -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<Error> 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
}
}
}
}
167 changes: 167 additions & 0 deletions server/src/traq/user/impl.rs
Original file line number Diff line number Diff line change
@@ -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<Context> super::TraqUserService<Context> for super::TraqUserServiceImpl
where
Context: AsRef<MySqlPool> + AsRef<TraqHost> + ProvideUserService + ProvideTraqBotService,
{
type Error = super::Error;

fn get_traq_user<'a>(
&'a self,
ctx: &'a Context,
params: super::GetTraqUserParams,
) -> BoxFuture<'a, Result<super::TraqUser, Self::Error>> {
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<Option<super::TraqUser>, 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<super::TraqUser, Self::Error>> {
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<Utc>,
pub updated_at: DateTime<Utc>,
}

#[tracing::instrument(skip_all)]
async fn find_traq_user<U: ProvideUserService>(
user_service: &U,
pool: &MySqlPool,
id: super::TraqUserId,
) -> Result<Option<super::TraqUser>, super::Error> {
let traq_user: Option<TraqUserRow> =
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<U: ProvideUserService, B: ProvideTraqBotService>(
user_service: &U,
traq_bot_service: &B,
pool: &MySqlPool,
traq_host: &TraqHost,
params: super::RegisterTraqUserParams,
) -> Result<super::TraqUser, super::Error> {
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)
}
Loading