mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 19:51:09 +00:00
split api into separate files
This commit is contained in:
parent
02db60ba15
commit
f8da5ae965
7 changed files with 234 additions and 198 deletions
|
@ -1,46 +1,21 @@
|
|||
use crate::database::article::{ArticleView, DbArticle, DbArticleForm};
|
||||
use crate::database::conflict::{ApiConflict, DbConflict, DbConflictForm};
|
||||
use crate::database::edit::{DbEdit, DbEditForm};
|
||||
use crate::database::instance::{DbInstance, InstanceView};
|
||||
use crate::database::user::{DbLocalUser, DbPerson};
|
||||
use crate::database::instance::DbInstance;
|
||||
use crate::database::version::EditVersion;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::activities::create_article::CreateArticle;
|
||||
use crate::federation::activities::follow::Follow;
|
||||
use crate::federation::activities::submit_article_update;
|
||||
use crate::utils::generate_article_version;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use anyhow::anyhow;
|
||||
use axum::extract::Query;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Form, Json, Router};
|
||||
use axum::Form;
|
||||
use axum::Json;
|
||||
use axum_macros::debug_handler;
|
||||
use bcrypt::verify;
|
||||
use chrono::Utc;
|
||||
use diffy::create_patch;
|
||||
use futures::future::try_join_all;
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
pub fn api_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/article",
|
||||
get(get_article).post(create_article).patch(edit_article),
|
||||
)
|
||||
.route("/article/fork", post(fork_article))
|
||||
.route("/edit_conflicts", get(edit_conflicts))
|
||||
.route("/resolve_instance", get(resolve_instance))
|
||||
.route("/resolve_article", get(resolve_article))
|
||||
.route("/instance", get(get_local_instance))
|
||||
.route("/instance/follow", post(follow_instance))
|
||||
.route("/search", get(search_article))
|
||||
.route("/user/register", post(register_user))
|
||||
.route("/user/login", post(login_user))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct CreateArticleData {
|
||||
|
@ -49,7 +24,7 @@ pub struct CreateArticleData {
|
|||
|
||||
/// Create a new article with empty text, and federate it to followers.
|
||||
#[debug_handler]
|
||||
async fn create_article(
|
||||
pub(in crate::api) async fn create_article(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(create_article): Form<CreateArticleData>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
|
@ -98,7 +73,7 @@ pub struct EditArticleData {
|
|||
///
|
||||
/// Conflicts are stored in the database so they can be retrieved later from `/api/v3/edit_conflicts`.
|
||||
#[debug_handler]
|
||||
async fn edit_article(
|
||||
pub(in crate::api) async fn edit_article(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(edit_form): Form<EditArticleData>,
|
||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||
|
@ -144,7 +119,7 @@ pub struct GetArticleData {
|
|||
|
||||
/// Retrieve an article by ID. It must already be stored in the local database.
|
||||
#[debug_handler]
|
||||
async fn get_article(
|
||||
pub(in crate::api) async fn get_article(
|
||||
Query(query): Query<GetArticleData>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
|
@ -154,97 +129,6 @@ async fn get_article(
|
|||
)?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ResolveObject {
|
||||
pub id: Url,
|
||||
}
|
||||
|
||||
/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to
|
||||
/// the local instance, and allows for interactions such as following.
|
||||
#[debug_handler]
|
||||
async fn resolve_instance(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?;
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||
/// article changes can only be received if we follow the instance, or if it is refetched manually.
|
||||
#[debug_handler]
|
||||
async fn resolve_article(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().hash.clone();
|
||||
Ok(Json(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
latest_version,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Retrieve the local instance info.
|
||||
#[debug_handler]
|
||||
async fn get_local_instance(data: Data<MyDataHandle>) -> MyResult<Json<InstanceView>> {
|
||||
let local_instance = DbInstance::read_local_view(&data.db_connection)?;
|
||||
Ok(Json(local_instance))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct FollowInstance {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
/// Make the local instance follow a given remote instance, to receive activities about new and
|
||||
/// updated articles.
|
||||
#[debug_handler]
|
||||
async fn follow_instance(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(query): Form<FollowInstance>,
|
||||
) -> MyResult<()> {
|
||||
let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
|
||||
let target = DbInstance::read(query.id, &data.db_connection)?;
|
||||
let pending = !target.local;
|
||||
DbInstance::follow(local_instance.id, target.id, pending, &data)?;
|
||||
let instance = DbInstance::read(query.id, &data.db_connection)?;
|
||||
Follow::send(local_instance, instance, &data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
|
||||
let conflicts = DbConflict::list(&data.db_connection)?;
|
||||
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = data.reset_request_count();
|
||||
async move { c.to_api_conflict(&data).await }
|
||||
}))
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
Ok(Json(conflicts))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct SearchArticleData {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
/// Search articles for matching title or body text.
|
||||
#[debug_handler]
|
||||
async fn search_article(
|
||||
Query(query): Query<SearchArticleData>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
let article = DbArticle::search(&query.query, &data.db_connection)?;
|
||||
Ok(Json(article))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ForkArticleData {
|
||||
// TODO: could add optional param new_title so there is no problem with title collision
|
||||
|
@ -256,7 +140,7 @@ pub struct ForkArticleData {
|
|||
/// Fork a remote article to local instance. This is useful if there are disagreements about
|
||||
/// how an article should be edited.
|
||||
#[debug_handler]
|
||||
async fn fork_article(
|
||||
pub(in crate::api) async fn fork_article(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(fork_form): Form<ForkArticleData>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
|
@ -298,69 +182,3 @@ async fn fork_article(
|
|||
|
||||
Ok(Json(DbArticle::read_view(article.id, &data.db_connection)?))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
/// local_user.id
|
||||
pub sub: String,
|
||||
/// hostname
|
||||
pub iss: String,
|
||||
/// Creation time as unix timestamp
|
||||
pub iat: i64,
|
||||
}
|
||||
|
||||
pub fn generate_login_token(
|
||||
local_user: DbLocalUser,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<LoginResponse> {
|
||||
let hostname = data.domain().to_string();
|
||||
let claims = Claims {
|
||||
sub: local_user.id.to_string(),
|
||||
iss: hostname,
|
||||
iat: Utc::now().timestamp(),
|
||||
};
|
||||
|
||||
// TODO: move to config
|
||||
let key = EncodingKey::from_secret("secret".as_bytes());
|
||||
let jwt = encode(&Header::default(), &claims, &key)?;
|
||||
Ok(LoginResponse { jwt })
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct RegisterUserData {
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub jwt: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn register_user(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(form): Form<RegisterUserData>,
|
||||
) -> MyResult<Json<LoginResponse>> {
|
||||
let user = DbPerson::create_local(form.name, form.password, &data)?;
|
||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginUserData {
|
||||
name: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn login_user(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(form): Form<LoginUserData>,
|
||||
) -> MyResult<Json<LoginResponse>> {
|
||||
let user = DbPerson::read_local_from_name(&form.name, &data)?;
|
||||
let valid = verify(&form.password, &user.local_user.password_encrypted)?;
|
||||
if !valid {
|
||||
return Err(anyhow!("Invalid login").into());
|
||||
}
|
||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
||||
}
|
38
src/api/instance.rs
Normal file
38
src/api/instance.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use crate::database::instance::{DbInstance, InstanceView};
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::activities::follow::Follow;
|
||||
use activitypub_federation::config::Data;
|
||||
use axum::{Form, Json};
|
||||
use axum_macros::debug_handler;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Retrieve the local instance info.
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn get_local_instance(
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<InstanceView>> {
|
||||
let local_instance = DbInstance::read_local_view(&data.db_connection)?;
|
||||
Ok(Json(local_instance))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct FollowInstance {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
/// Make the local instance follow a given remote instance, to receive activities about new and
|
||||
/// updated articles.
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn follow_instance(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(query): Form<FollowInstance>,
|
||||
) -> MyResult<()> {
|
||||
let local_instance = DbInstance::read_local_instance(&data.db_connection)?;
|
||||
let target = DbInstance::read(query.id, &data.db_connection)?;
|
||||
let pending = !target.local;
|
||||
DbInstance::follow(local_instance.id, target.id, pending, &data)?;
|
||||
let instance = DbInstance::read(query.id, &data.db_connection)?;
|
||||
Follow::send(local_instance, instance, &data).await?;
|
||||
Ok(())
|
||||
}
|
105
src/api/mod.rs
Normal file
105
src/api/mod.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use crate::api::article::create_article;
|
||||
use crate::api::article::{edit_article, fork_article, get_article};
|
||||
use crate::api::instance::follow_instance;
|
||||
use crate::api::instance::get_local_instance;
|
||||
use crate::api::user::login_user;
|
||||
use crate::api::user::register_user;
|
||||
use crate::database::article::{ArticleView, DbArticle};
|
||||
use crate::database::conflict::{ApiConflict, DbConflict};
|
||||
use crate::database::edit::DbEdit;
|
||||
use crate::database::instance::DbInstance;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use activitypub_federation::config::Data;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use axum::extract::Query;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use axum_macros::debug_handler;
|
||||
use futures::future::try_join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
pub mod article;
|
||||
pub mod instance;
|
||||
pub mod user;
|
||||
|
||||
pub fn api_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/article",
|
||||
get(get_article).post(create_article).patch(edit_article),
|
||||
)
|
||||
.route("/article/fork", post(fork_article))
|
||||
.route("/edit_conflicts", get(edit_conflicts))
|
||||
.route("/resolve_instance", get(resolve_instance))
|
||||
.route("/resolve_article", get(resolve_article))
|
||||
.route("/instance", get(get_local_instance))
|
||||
.route("/instance/follow", post(follow_instance))
|
||||
.route("/search", get(search_article))
|
||||
.route("/user/register", post(register_user))
|
||||
.route("/user/login", post(login_user))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ResolveObject {
|
||||
pub id: Url,
|
||||
}
|
||||
|
||||
/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to
|
||||
/// the local instance, and allows for interactions such as following.
|
||||
#[debug_handler]
|
||||
async fn resolve_instance(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?;
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||
/// article changes can only be received if we follow the instance, or if it is refetched manually.
|
||||
#[debug_handler]
|
||||
async fn resolve_article(
|
||||
Query(query): Query<ResolveObject>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().hash.clone();
|
||||
Ok(Json(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
latest_version,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
|
||||
let conflicts = DbConflict::list(&data.db_connection)?;
|
||||
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = data.reset_request_count();
|
||||
async move { c.to_api_conflict(&data).await }
|
||||
}))
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
Ok(Json(conflicts))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct SearchArticleData {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
/// Search articles for matching title or body text.
|
||||
#[debug_handler]
|
||||
async fn search_article(
|
||||
Query(query): Query<SearchArticleData>,
|
||||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
let article = DbArticle::search(&query.query, &data.db_connection)?;
|
||||
Ok(Json(article))
|
||||
}
|
77
src/api/user.rs
Normal file
77
src/api/user.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::database::user::{DbLocalUser, DbPerson};
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use activitypub_federation::config::Data;
|
||||
use anyhow::anyhow;
|
||||
use axum::{Form, Json};
|
||||
use axum_macros::debug_handler;
|
||||
use bcrypt::verify;
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
/// local_user.id
|
||||
pub sub: String,
|
||||
/// hostname
|
||||
pub iss: String,
|
||||
/// Creation time as unix timestamp
|
||||
pub iat: i64,
|
||||
}
|
||||
|
||||
pub(in crate::api) fn generate_login_token(
|
||||
local_user: DbLocalUser,
|
||||
data: &Data<MyDataHandle>,
|
||||
) -> MyResult<LoginResponse> {
|
||||
let hostname = data.domain().to_string();
|
||||
let claims = Claims {
|
||||
sub: local_user.id.to_string(),
|
||||
iss: hostname,
|
||||
iat: Utc::now().timestamp(),
|
||||
};
|
||||
|
||||
// TODO: move to config
|
||||
let key = EncodingKey::from_secret("secret".as_bytes());
|
||||
let jwt = encode(&Header::default(), &claims, &key)?;
|
||||
Ok(LoginResponse { jwt })
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct RegisterUserData {
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub jwt: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn register_user(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(form): Form<RegisterUserData>,
|
||||
) -> MyResult<Json<LoginResponse>> {
|
||||
let user = DbPerson::create_local(form.name, form.password, &data)?;
|
||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginUserData {
|
||||
name: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub(in crate::api) async fn login_user(
|
||||
data: Data<MyDataHandle>,
|
||||
Form(form): Form<LoginUserData>,
|
||||
) -> MyResult<Json<LoginResponse>> {
|
||||
let user = DbPerson::read_local_from_name(&form.name, &data)?;
|
||||
let valid = verify(&form.password, &user.local_user.password_encrypted)?;
|
||||
if !valid {
|
||||
return Err(anyhow!("Invalid login").into());
|
||||
}
|
||||
Ok(Json(generate_login_token(user.local_user, &data)?))
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
use crate::api::api_routes;
|
||||
use crate::database::instance::{DbInstance, DbInstanceForm};
|
||||
use crate::database::MyData;
|
||||
use crate::error::MyResult;
|
||||
|
@ -8,6 +7,7 @@ use activitypub_federation::config::{FederationConfig, FederationMiddleware};
|
|||
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use activitypub_federation::http_signatures::generate_actor_keypair;
|
||||
use api::api_routes;
|
||||
use axum::{Router, Server};
|
||||
use chrono::Local;
|
||||
use diesel::Connection;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::anyhow;
|
||||
use fediwiki::api::{
|
||||
CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
|
||||
};
|
||||
use fediwiki::api::article::{CreateArticleData, EditArticleData, GetArticleData};
|
||||
use fediwiki::api::instance::FollowInstance;
|
||||
use fediwiki::api::ResolveObject;
|
||||
use fediwiki::database::article::ArticleView;
|
||||
use fediwiki::database::conflict::ApiConflict;
|
||||
use fediwiki::database::instance::DbInstance;
|
||||
|
@ -20,7 +20,6 @@ use std::thread::{sleep, spawn};
|
|||
use std::time::Duration;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::log::LevelFilter;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
pub static CLIENT: Lazy<Client> = Lazy::new(Client::new);
|
||||
|
|
|
@ -7,10 +7,9 @@ use crate::common::{
|
|||
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
|
||||
};
|
||||
use common::get;
|
||||
use fediwiki::api::{
|
||||
EditArticleData, ForkArticleData, LoginResponse, RegisterUserData, ResolveObject,
|
||||
SearchArticleData,
|
||||
};
|
||||
use fediwiki::api::article::{EditArticleData, ForkArticleData};
|
||||
use fediwiki::api::user::{LoginResponse, RegisterUserData};
|
||||
use fediwiki::api::{ResolveObject, SearchArticleData};
|
||||
use fediwiki::database::article::{ArticleView, DbArticle};
|
||||
use fediwiki::database::conflict::ApiConflict;
|
||||
use fediwiki::database::instance::{DbInstance, InstanceView};
|
||||
|
|
Loading…
Reference in a new issue