add basic guild create + message create endpoints

This commit is contained in:
sam 2024-01-20 16:43:03 +01:00
parent 5b23095520
commit e57bff00c2
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
27 changed files with 367 additions and 36 deletions

View file

@ -31,6 +31,8 @@ create table guilds_users (
guild_id text not null references guilds (id) on delete cascade,
user_id text not null references users (id) on delete cascade,
joined_at timestamptz not null default now(),
primary key (guild_id, user_id)
);

47
chat/src/db/channel.rs Normal file
View file

@ -0,0 +1,47 @@
use eyre::Result;
use foxchat::FoxError;
use sqlx::PgExecutor;
use ulid::Ulid;
pub struct Channel {
pub id: String,
pub guild_id: String,
pub name: String,
pub topic: Option<String>,
}
pub async fn create_channel(
executor: impl PgExecutor<'_>,
guild_id: &str,
name: &str,
topic: Option<String>,
) -> Result<Channel> {
let channel = sqlx::query_as!(
Channel,
"insert into channels (id, guild_id, name, topic) values ($1, $2, $3, $4) returning *",
Ulid::new().to_string(),
guild_id,
name,
topic
)
.fetch_one(executor)
.await?;
Ok(channel)
}
pub async fn get_channel(executor: impl PgExecutor<'_>, channel_id: &str) -> Result<Channel, FoxError> {
let channel = sqlx::query_as!(Channel, "select * from channels where id = $1", channel_id)
.fetch_one(executor)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => FoxError::NotInGuild,
_ => {
tracing::error!("database error: {}", e);
return FoxError::DatabaseError;
}
})?;
Ok(channel)
}

71
chat/src/db/guild.rs Normal file
View file

@ -0,0 +1,71 @@
use eyre::Result;
use foxchat::FoxError;
use sqlx::PgExecutor;
use ulid::Ulid;
pub struct Guild {
pub id: String,
pub owner_id: String,
pub name: String,
}
pub async fn create_guild(
executor: impl PgExecutor<'_>,
owner_id: &str,
name: &str,
) -> Result<Guild> {
let guild = sqlx::query_as!(
Guild,
"insert into guilds (id, owner_id, name) values ($1, $2, $3) returning *",
Ulid::new().to_string(),
owner_id,
name
)
.fetch_one(executor)
.await?;
Ok(guild)
}
pub async fn join_guild(
executor: impl PgExecutor<'_>,
guild_id: &str,
user_id: &str,
) -> Result<()> {
sqlx::query!(
"insert into guilds_users (guild_id, user_id) values ($1, $2) on conflict (guild_id, user_id) do nothing",
guild_id,
user_id
)
.execute(executor)
.await?;
Ok(())
}
pub async fn get_guild(
executor: impl PgExecutor<'_>,
guild_id: &str,
user_id: &str,
) -> Result<Guild, FoxError> {
println!("guild id: {}, user id: {}", guild_id, user_id);
let guild = sqlx::query_as!(
Guild,
"select g.* from guilds_users u join guilds g on u.guild_id = g.id where u.guild_id = $1 and u.user_id = $2",
guild_id,
user_id
)
.fetch_one(executor)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => FoxError::NotInGuild,
_ => {
tracing::error!("database error: {}", e);
return FoxError::DatabaseError;
}
})?;
Ok(guild)
}

33
chat/src/db/message.rs Normal file
View file

@ -0,0 +1,33 @@
use chrono::{DateTime, Utc};
use eyre::Result;
use foxchat::model::http::channel::CreateMessageParams;
use sqlx::PgExecutor;
use ulid::Ulid;
pub struct Message {
pub id: String,
pub channel_id: String,
pub author_id: String,
pub updated_at: DateTime<Utc>,
pub content: String,
}
pub async fn create_message(
executor: impl PgExecutor<'_>,
channel_id: &str,
user_id: &str,
params: CreateMessageParams,
) -> Result<Message> {
let message = sqlx::query_as!(
Message,
"insert into messages (id, channel_id, author_id, content) values ($1, $2, $3, $4) returning *",
Ulid::new().to_string(),
channel_id,
user_id,
params.content,
)
.fetch_one(executor)
.await?;
Ok(message)
}

View file

@ -1,3 +1,7 @@
pub mod guild;
pub mod channel;
pub mod message;
use eyre::{OptionExt, Result};
use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey, LineEnding};
use rsa::{RsaPrivateKey, RsaPublicKey};

View file

@ -0,0 +1,48 @@
use std::sync::Arc;
use axum::{extract::Path, Extension, Json};
use eyre::Result;
use foxchat::{
http::ApiError,
model::{http::channel::CreateMessageParams, Message, user::PartialUser},
FoxError, ulid_timestamp,
};
use crate::{
app_state::AppState,
db::{channel::get_channel, guild::get_guild, message::create_message},
fed::FoxRequestData,
model::user::User,
};
pub async fn post_messages(
Extension(state): Extension<Arc<AppState>>,
request: FoxRequestData,
Path(channel_id): Path<String>,
Json(params): Json<CreateMessageParams>,
) -> Result<Json<Message>, ApiError> {
let user_id = request.user_id.ok_or(FoxError::MissingUser)?;
let user = User::get(&state, &request.instance, &user_id).await?;
let mut tx = state.pool.begin().await?;
let channel = get_channel(&mut *tx, &channel_id).await?;
let _guild = get_guild(&mut *tx, &channel.guild_id, &user.id).await?;
let message = create_message(&mut *tx, &channel.id, &user.id, params).await?;
tx.commit().await?;
// TODO: dispatch message create event
Ok(Json(Message {
id: message.id.clone(),
channel_id: channel.id,
author: PartialUser {
id: user.id,
username: user.username,
instance: request.instance.domain,
},
content: Some(message.content),
created_at: ulid_timestamp(&message.id),
}))
}

View file

@ -0,0 +1,7 @@
mod messages;
use axum::{routing::post, Router};
pub fn router() -> Router {
Router::new().route("/_fox/chat/channels/:id/messages", post(messages::post_messages))
}

View file

@ -3,30 +3,32 @@ use std::sync::Arc;
use axum::{Extension, Json};
use foxchat::{
http::ApiError,
model::{http::guild::CreateGuildParams, Guild, user::PartialUser},
model::{http::guild::CreateGuildParams, user::PartialUser, Guild, channel::PartialChannel},
FoxError,
};
use ulid::Ulid;
use crate::{app_state::AppState, fed::FoxRequestData, model::user::User};
use crate::{
app_state::AppState,
db::{channel::create_channel, guild::{create_guild, join_guild}},
fed::FoxRequestData,
model::user::User,
};
pub async fn create_guild(
pub async fn post_guilds(
Extension(state): Extension<Arc<AppState>>,
request: FoxRequestData,
Json(params): Json<CreateGuildParams>,
) -> Result<Json<Guild>, ApiError> {
let user_id = request.user_id.ok_or(FoxError::MissingUser)?;
let user = User::get(&state, &request.instance, &user_id).await?;
let user = User::get(&state, &request.instance, user_id).await?;
let mut tx = state.pool.begin().await?;
let guild = create_guild(&mut *tx, &user.id, &params.name).await?;
let channel = create_channel(&mut *tx, &guild.id, "general", None).await?;
let guild = sqlx::query!(
"insert into guilds (id, owner_id, name) values ($1, $2, $3) returning *",
Ulid::new().to_string(),
user.id,
params.name
)
.fetch_one(&state.pool)
.await?;
join_guild(&mut *tx, &guild.id, &user.id).await?;
tx.commit().await?;
Ok(Json(Guild {
id: guild.id,
@ -34,7 +36,11 @@ pub async fn create_guild(
owner: PartialUser {
id: user.id,
username: user.username,
instance: request.instance.domain,
instance: user.instance.domain,
},
default_channel: PartialChannel {
id: channel.id,
name: channel.name,
}
}))
}

View file

@ -4,5 +4,5 @@ use axum::{Router, routing::post};
pub fn router() -> Router {
Router::new()
.route("/_fox/chat/guilds", post(create_guild::create_guild))
.route("/_fox/chat/guilds", post(create_guild::post_guilds))
}

View file

@ -1,7 +1,10 @@
use axum::Router;
pub mod channels;
pub mod guilds;
pub fn router() -> Router {
Router::new().merge(guilds::router())
Router::new()
.merge(guilds::router())
.merge(channels::router())
}

View file

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::app_state::AppState;
#[derive(Serialize)]
#[derive(Serialize, Clone, Debug)]
pub struct IdentityInstance {
pub id: String,
pub domain: String,
@ -17,7 +17,7 @@ pub struct IdentityInstance {
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, sqlx::Type)]
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)]
#[sqlx(type_name = "instance_status", rename_all = "lowercase")]
pub enum InstanceStatus {
Active,

View file

@ -11,6 +11,7 @@ use super::identity_instance::IdentityInstance;
pub struct User {
pub id: String,
pub instance_id: String,
pub instance: IdentityInstance,
pub remote_user_id: String,
pub username: String,
pub avatar: Option<String>,
@ -20,10 +21,9 @@ impl User {
pub async fn get(
state: &Arc<AppState>,
instance: &IdentityInstance,
remote_id: String,
remote_id: &str,
) -> Result<User> {
if let Some(user) = sqlx::query_as!(
User,
if let Some(user) = sqlx::query!(
"select * from users where instance_id = $1 and remote_user_id = $2",
instance.id,
remote_id
@ -31,7 +31,14 @@ impl User {
.fetch_optional(&state.pool)
.await?
{
return Ok(user);
return Ok(User {
id: user.id,
username: user.username,
instance_id: user.instance_id,
instance: instance.to_owned(),
remote_user_id: user.remote_user_id,
avatar: user.avatar,
});
}
let http_user = fed::get::<HttpUser>(
@ -43,8 +50,7 @@ impl User {
)
.await?;
let user = sqlx::query_as!(
User,
let user = sqlx::query!(
"insert into users (id, instance_id, remote_user_id, username, avatar) values ($1, $2, $3, $4, $5) returning *",
Ulid::new().to_string(),
instance.id,
@ -55,6 +61,13 @@ impl User {
.fetch_one(&state.pool)
.await?;
Ok(user)
Ok(User {
id: user.id,
username: user.username,
instance_id: user.instance_id,
instance: instance.to_owned(),
remote_user_id: user.remote_user_id,
avatar: user.avatar,
})
}
}