split api into separate files

This commit is contained in:
Felix Ableitner 2023-12-13 13:32:44 +01:00
parent 02db60ba15
commit f8da5ae965
7 changed files with 234 additions and 198 deletions

View File

@ -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
View 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
View 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
View 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)?))
}

View File

@ -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;

View File

@ -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);

View File

@ -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};