mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 08:01:09 +00:00
allow forking remote article to local instance, add search
This commit is contained in:
parent
782ddd30f3
commit
d0e163ed61
6 changed files with 170 additions and 9 deletions
86
src/api.rs
86
src/api.rs
|
@ -25,11 +25,13 @@ pub fn api_routes() -> Router {
|
|||
"/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))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
@ -42,6 +44,16 @@ async fn create_article(
|
|||
data: Data<DatabaseHandle>,
|
||||
Form(create_article): Form<CreateArticleData>,
|
||||
) -> MyResult<Json<DbArticle>> {
|
||||
{
|
||||
let articles = data.articles.lock().unwrap();
|
||||
let title_exists = articles
|
||||
.iter()
|
||||
.any(|a| a.1.local && a.1.title == create_article.title);
|
||||
if title_exists {
|
||||
return Err(anyhow!("A local article with this title already exists").into());
|
||||
}
|
||||
}
|
||||
|
||||
let local_instance_id = data.local_instance().ap_id;
|
||||
let ap_id = ObjectId::parse(&format!(
|
||||
"http://{}:{}/article/{}",
|
||||
|
@ -204,3 +216,77 @@ async fn edit_conflicts(data: Data<DatabaseHandle>) -> MyResult<Json<Vec<ApiConf
|
|||
.collect();
|
||||
Ok(Json(conflicts))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct SearchArticleData {
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn search_article(
|
||||
Query(query): Query<SearchArticleData>,
|
||||
data: Data<DatabaseHandle>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
let articles = data.articles.lock().unwrap();
|
||||
let article = articles
|
||||
.iter()
|
||||
.filter(|a| a.1.title == query.title)
|
||||
.map(|a| a.1)
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(Json(article))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ForkArticleData {
|
||||
// TODO: could add optional param new_title so there is no problem with title collision
|
||||
// in case local article with same title exists
|
||||
pub ap_id: ObjectId<DbArticle>,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn fork_article(
|
||||
data: Data<DatabaseHandle>,
|
||||
Form(fork_form): Form<ForkArticleData>,
|
||||
) -> MyResult<Json<DbArticle>> {
|
||||
let article = {
|
||||
let lock = data.articles.lock().unwrap();
|
||||
let article = lock.get(fork_form.ap_id.inner()).unwrap();
|
||||
article.clone()
|
||||
};
|
||||
if article.local {
|
||||
return Err(anyhow!("Cannot fork local article because there cant be multiple local articles with same title").into());
|
||||
}
|
||||
|
||||
let original_article = {
|
||||
let lock = data.articles.lock().unwrap();
|
||||
lock.get(fork_form.ap_id.inner())
|
||||
.expect("article exists")
|
||||
.clone()
|
||||
};
|
||||
|
||||
let local_instance_id = data.local_instance().ap_id;
|
||||
let ap_id = ObjectId::parse(&format!(
|
||||
"http://{}:{}/article/{}",
|
||||
local_instance_id.inner().domain().unwrap(),
|
||||
local_instance_id.inner().port().unwrap(),
|
||||
original_article.title
|
||||
))?;
|
||||
let forked_article = DbArticle {
|
||||
title: original_article.title.clone(),
|
||||
text: original_article.text.clone(),
|
||||
ap_id,
|
||||
latest_version: original_article.latest_version.clone(),
|
||||
edits: original_article.edits.clone(),
|
||||
instance: local_instance_id,
|
||||
local: true,
|
||||
};
|
||||
{
|
||||
let mut articles = data.articles.lock().unwrap();
|
||||
articles.insert(forked_article.ap_id.inner().clone(), forked_article.clone());
|
||||
}
|
||||
|
||||
CreateArticle::send_to_followers(forked_article.clone(), &data).await?;
|
||||
|
||||
Ok(Json(forked_article))
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ use activitypub_federation::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DbArticle {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
|
@ -91,7 +91,7 @@ impl Object for DbArticle {
|
|||
}
|
||||
|
||||
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
||||
let article = DbArticle {
|
||||
let mut article = DbArticle {
|
||||
title: json.name,
|
||||
text: json.content,
|
||||
ap_id: json.id,
|
||||
|
@ -107,7 +107,10 @@ impl Object for DbArticle {
|
|||
lock.insert(article.ap_id.inner().clone(), article.clone());
|
||||
}
|
||||
|
||||
json.edits.dereference(&article, data).await?;
|
||||
let edits = json.edits.dereference(&article, data).await?;
|
||||
|
||||
// include edits in return value (they are already written to db, no need to do that here)
|
||||
article.edits = edits.0;
|
||||
|
||||
Ok(article)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ impl Default for EditVersion {
|
|||
}
|
||||
|
||||
/// Represents a single change to the article.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DbEdit {
|
||||
pub id: ObjectId<DbEdit>,
|
||||
pub diff: String,
|
||||
|
|
|
@ -23,7 +23,7 @@ pub struct ApubEditCollection {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DbEditCollection(Vec<DbEdit>);
|
||||
pub struct DbEditCollection(pub Vec<DbEdit>);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Collection for DbEditCollection {
|
||||
|
|
|
@ -140,7 +140,7 @@ where
|
|||
Ok(alpha_instance)
|
||||
}
|
||||
|
||||
async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
|
||||
pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
|
||||
where
|
||||
R: for<'de> Deserialize<'de>,
|
||||
{
|
||||
|
|
|
@ -4,16 +4,17 @@ mod common;
|
|||
|
||||
use crate::common::{
|
||||
create_article, edit_article, edit_article_with_conflict, follow_instance, get_article,
|
||||
get_query, TestData, TEST_ARTICLE_DEFAULT_TEXT,
|
||||
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
|
||||
};
|
||||
use common::get;
|
||||
use fediwiki::api::{ApiConflict, EditArticleData, ResolveObject};
|
||||
use fediwiki::api::{
|
||||
ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData,
|
||||
};
|
||||
use fediwiki::error::MyResult;
|
||||
use fediwiki::federation::objects::article::DbArticle;
|
||||
use fediwiki::federation::objects::edit::ApubEdit;
|
||||
use fediwiki::federation::objects::instance::DbInstance;
|
||||
use serial_test::serial;
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -48,6 +49,31 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
|
|||
assert_eq!(edit_form.new_text, edit_res.text);
|
||||
assert_eq!(2, edit_res.edits.len());
|
||||
|
||||
let search_form = SearchArticleData {
|
||||
title: title.clone(),
|
||||
};
|
||||
let search_res: Vec<DbArticle> =
|
||||
get_query(data.hostname_alpha, "search", Some(search_form)).await?;
|
||||
assert_eq!(1, search_res.len());
|
||||
assert_eq!(edit_res, search_res[0]);
|
||||
|
||||
data.stop()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_create_duplicate_article() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
|
||||
// create article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(data.hostname_alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.title);
|
||||
assert!(create_res.local);
|
||||
|
||||
let create_res = create_article(data.hostname_alpha, title.clone()).await;
|
||||
assert!(create_res.is_err());
|
||||
|
||||
data.stop()
|
||||
}
|
||||
|
||||
|
@ -378,3 +404,49 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
|||
|
||||
data.stop()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_fork_article() -> MyResult<()> {
|
||||
let data = TestData::start();
|
||||
|
||||
// create article
|
||||
let title = "Manu_Chao".to_string();
|
||||
let create_res = create_article(data.hostname_alpha, title.clone()).await?;
|
||||
assert_eq!(title, create_res.title);
|
||||
assert!(create_res.local);
|
||||
|
||||
// fetch on beta
|
||||
let resolve_object = ResolveObject {
|
||||
id: create_res.ap_id.into_inner(),
|
||||
};
|
||||
let resolved_article =
|
||||
get_query::<DbArticle, _>(data.hostname_beta, "resolve_article", Some(resolve_object))
|
||||
.await?;
|
||||
assert_eq!(create_res.edits.len(), resolved_article.edits.len());
|
||||
|
||||
// fork the article to local instance
|
||||
let fork_form = ForkArticleData {
|
||||
ap_id: resolved_article.ap_id.clone(),
|
||||
};
|
||||
let fork_res: DbArticle = post(data.hostname_beta, "article/fork", &fork_form).await?;
|
||||
assert_eq!(resolved_article.title, fork_res.title);
|
||||
assert_eq!(resolved_article.text, fork_res.text);
|
||||
assert_eq!(resolved_article.edits, fork_res.edits);
|
||||
assert_eq!(resolved_article.latest_version, fork_res.latest_version);
|
||||
assert_ne!(resolved_article.ap_id, fork_res.ap_id);
|
||||
assert!(fork_res.local);
|
||||
|
||||
let beta_instance: DbInstance = get(data.hostname_beta, "instance").await?;
|
||||
assert_eq!(fork_res.instance, beta_instance.ap_id);
|
||||
|
||||
// now search returns two articles for this title (original and forked)
|
||||
let search_form = SearchArticleData {
|
||||
title: title.clone(),
|
||||
};
|
||||
let search_res: Vec<DbArticle> =
|
||||
get_query(data.hostname_beta, "search", Some(search_form)).await?;
|
||||
assert_eq!(2, search_res.len());
|
||||
|
||||
data.stop()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue