mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 08:51:09 +00:00
Use single api endpoint for notifications (conflicts and new articles)
This commit is contained in:
parent
7531476066
commit
21c77c2f24
18 changed files with 231 additions and 158 deletions
|
@ -42,7 +42,7 @@ use diffy::create_patch;
|
||||||
/// Create a new article with empty text, and federate it to followers.
|
/// Create a new article with empty text, and federate it to followers.
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub(in crate::backend::api) async fn create_article(
|
pub(in crate::backend::api) async fn create_article(
|
||||||
Extension(user): Extension<LocalUserView>,
|
Extension(mut user): Extension<LocalUserView>,
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
Form(create_article): Form<CreateArticleForm>,
|
Form(create_article): Form<CreateArticleForm>,
|
||||||
) -> MyResult<Json<ArticleView>> {
|
) -> MyResult<Json<ArticleView>> {
|
||||||
|
@ -79,9 +79,13 @@ pub(in crate::backend::api) async fn create_article(
|
||||||
previous_version_id: article.latest_edit_version(&data)?,
|
previous_version_id: article.latest_edit_version(&data)?,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// workaround so the edit goes through
|
||||||
|
user.local_user.admin = true;
|
||||||
let _ = edit_article(Extension(user), data.reset_request_count(), Form(edit_data)).await?;
|
let _ = edit_article(Extension(user), data.reset_request_count(), Form(edit_data)).await?;
|
||||||
|
|
||||||
let article_view = DbArticle::read_view(article.id, &data)?;
|
// allow reading unapproved article here
|
||||||
|
let article_view = DbArticle::read_view(article.id, true, &data)?;
|
||||||
CreateArticle::send_to_followers(article_view.article.clone(), &data).await?;
|
CreateArticle::send_to_followers(article_view.article.clone(), &data).await?;
|
||||||
|
|
||||||
Ok(Json(article_view))
|
Ok(Json(article_view))
|
||||||
|
@ -102,11 +106,12 @@ pub(in crate::backend::api) async fn edit_article(
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
Form(mut edit_form): Form<EditArticleForm>,
|
Form(mut edit_form): Form<EditArticleForm>,
|
||||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||||
|
let is_admin = check_is_admin(&user).is_ok();
|
||||||
// resolve conflict if any
|
// resolve conflict if any
|
||||||
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
|
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
|
||||||
DbConflict::delete(resolve_conflict_id, &data)?;
|
DbConflict::delete(resolve_conflict_id, &data)?;
|
||||||
}
|
}
|
||||||
let original_article = DbArticle::read_view(edit_form.article_id, &data)?;
|
let original_article = DbArticle::read_view(edit_form.article_id, is_admin, &data)?;
|
||||||
if edit_form.new_text == original_article.article.text {
|
if edit_form.new_text == original_article.article.text {
|
||||||
return Err(anyhow!("Edit contains no changes").into());
|
return Err(anyhow!("Edit contains no changes").into());
|
||||||
}
|
}
|
||||||
|
@ -148,7 +153,7 @@ pub(in crate::backend::api) async fn edit_article(
|
||||||
previous_version_id: previous_version.hash,
|
previous_version_id: previous_version.hash,
|
||||||
};
|
};
|
||||||
let conflict = DbConflict::create(&form, &data)?;
|
let conflict = DbConflict::create(&form, &data)?;
|
||||||
Ok(Json(conflict.to_api_conflict(&data).await?))
|
Ok(Json(conflict.to_api_conflict(is_admin, &data).await?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,19 +161,23 @@ pub(in crate::backend::api) async fn edit_article(
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub(in crate::backend::api) async fn get_article(
|
pub(in crate::backend::api) async fn get_article(
|
||||||
Query(query): Query<GetArticleForm>,
|
Query(query): Query<GetArticleForm>,
|
||||||
|
Extension(user): Extension<LocalUserView>,
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
) -> MyResult<Json<ArticleView>> {
|
) -> MyResult<Json<ArticleView>> {
|
||||||
|
let is_admin = check_is_admin(&user).is_ok();
|
||||||
match (query.title, query.id) {
|
match (query.title, query.id) {
|
||||||
(Some(title), None) => Ok(Json(DbArticle::read_view_title(
|
(Some(title), None) => Ok(Json(DbArticle::read_view_title(
|
||||||
&title,
|
&title,
|
||||||
query.domain,
|
query.domain,
|
||||||
|
is_admin,
|
||||||
&data,
|
&data,
|
||||||
)?)),
|
)?)),
|
||||||
(None, Some(id)) => {
|
(None, Some(id)) => {
|
||||||
if query.domain.is_some() {
|
if query.domain.is_some() {
|
||||||
return Err(anyhow!("Cant combine id and instance_domain").into());
|
return Err(anyhow!("Cant combine id and instance_domain").into());
|
||||||
}
|
}
|
||||||
Ok(Json(DbArticle::read_view(id, &data)?))
|
let article = DbArticle::read_view(id, is_admin, &data)?;
|
||||||
|
Ok(Json(article))
|
||||||
}
|
}
|
||||||
_ => Err(anyhow!("Must pass exactly one of title, id").into()),
|
_ => Err(anyhow!("Must pass exactly one of title, id").into()),
|
||||||
}
|
}
|
||||||
|
@ -190,12 +199,13 @@ pub(in crate::backend::api) async fn list_articles(
|
||||||
/// how an article should be edited.
|
/// how an article should be edited.
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub(in crate::backend::api) async fn fork_article(
|
pub(in crate::backend::api) async fn fork_article(
|
||||||
Extension(_user): Extension<LocalUserView>,
|
Extension(user): Extension<LocalUserView>,
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
Form(fork_form): Form<ForkArticleForm>,
|
Form(fork_form): Form<ForkArticleForm>,
|
||||||
) -> MyResult<Json<ArticleView>> {
|
) -> MyResult<Json<ArticleView>> {
|
||||||
|
let is_admin = check_is_admin(&user).is_ok();
|
||||||
// TODO: lots of code duplicated from create_article(), can move it into helper
|
// TODO: lots of code duplicated from create_article(), can move it into helper
|
||||||
let original_article = DbArticle::read(fork_form.article_id, &data)?;
|
let original_article = DbArticle::read_view(fork_form.article_id, is_admin, &data)?;
|
||||||
|
|
||||||
let local_instance = DbInstance::read_local_instance(&data)?;
|
let local_instance = DbInstance::read_local_instance(&data)?;
|
||||||
let ap_id = ObjectId::parse(&format!(
|
let ap_id = ObjectId::parse(&format!(
|
||||||
|
@ -206,18 +216,19 @@ pub(in crate::backend::api) async fn fork_article(
|
||||||
))?;
|
))?;
|
||||||
let form = DbArticleForm {
|
let form = DbArticleForm {
|
||||||
title: fork_form.new_title,
|
title: fork_form.new_title,
|
||||||
text: original_article.text.clone(),
|
text: original_article.article.text.clone(),
|
||||||
ap_id,
|
ap_id,
|
||||||
instance_id: local_instance.id,
|
instance_id: local_instance.id,
|
||||||
local: true,
|
local: true,
|
||||||
protected: false,
|
protected: false,
|
||||||
approved: data.config.article_approval,
|
approved: !data.config.article_approval,
|
||||||
};
|
};
|
||||||
let article = DbArticle::create(form, &data)?;
|
let article = DbArticle::create(form, &data)?;
|
||||||
|
|
||||||
// copy edits to new article
|
// copy edits to new article
|
||||||
// this could also be done in sql
|
// this could also be done in sql
|
||||||
let edits = DbEdit::read_for_article(&original_article, &data)?
|
let edits = original_article
|
||||||
|
.edits
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|e| e.edit)
|
.map(|e| e.edit)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -238,7 +249,7 @@ pub(in crate::backend::api) async fn fork_article(
|
||||||
|
|
||||||
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
||||||
|
|
||||||
Ok(Json(DbArticle::read_view(article.id, &data)?))
|
Ok(Json(DbArticle::read_view(article.id, is_admin, &data)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||||
|
@ -288,17 +299,6 @@ pub(in crate::backend::api) async fn protect_article(
|
||||||
Ok(Json(article))
|
Ok(Json(article))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a list of all unresolved edit conflicts.
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn list_approval_required(
|
|
||||||
Extension(user): Extension<LocalUserView>,
|
|
||||||
data: Data<IbisData>,
|
|
||||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
|
||||||
check_is_admin(&user)?;
|
|
||||||
let articles = DbArticle::list_approval_required(&data)?;
|
|
||||||
Ok(Json(articles))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a list of all unresolved edit conflicts.
|
/// Get a list of all unresolved edit conflicts.
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn approve_article(
|
pub async fn approve_article(
|
||||||
|
|
|
@ -22,28 +22,25 @@ use crate::{
|
||||||
AUTH_COOKIE,
|
AUTH_COOKIE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
database::{conflict::DbConflict, IbisData},
|
database::IbisData,
|
||||||
error::MyResult,
|
error::MyResult,
|
||||||
},
|
},
|
||||||
common::{ApiConflict, LocalUserView},
|
common::LocalUserView,
|
||||||
};
|
};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use article::{approve_article, list_approval_required};
|
use article::approve_article;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::Response,
|
response::Response,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Extension,
|
|
||||||
Json,
|
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use axum_macros::debug_handler;
|
|
||||||
use futures::future::try_join_all;
|
|
||||||
use instance::list_remote_instances;
|
use instance::list_remote_instances;
|
||||||
|
use user::{count_notifications, list_notifications};
|
||||||
|
|
||||||
pub mod article;
|
pub mod article;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
|
@ -59,18 +56,15 @@ pub fn api_routes() -> Router<()> {
|
||||||
.route("/article/fork", post(fork_article))
|
.route("/article/fork", post(fork_article))
|
||||||
.route("/article/resolve", get(resolve_article))
|
.route("/article/resolve", get(resolve_article))
|
||||||
.route("/article/protect", post(protect_article))
|
.route("/article/protect", post(protect_article))
|
||||||
.route(
|
|
||||||
"/article/list/approval_required",
|
|
||||||
get(list_approval_required),
|
|
||||||
)
|
|
||||||
.route("/article/approve", post(approve_article))
|
.route("/article/approve", post(approve_article))
|
||||||
.route("/edit_conflicts", get(edit_conflicts))
|
|
||||||
.route("/instance", get(get_instance))
|
.route("/instance", get(get_instance))
|
||||||
.route("/instance/follow", post(follow_instance))
|
.route("/instance/follow", post(follow_instance))
|
||||||
.route("/instance/resolve", get(resolve_instance))
|
.route("/instance/resolve", get(resolve_instance))
|
||||||
.route("/instance/list", get(list_remote_instances))
|
.route("/instance/list", get(list_remote_instances))
|
||||||
.route("/search", get(search_article))
|
.route("/search", get(search_article))
|
||||||
.route("/user", get(get_user))
|
.route("/user", get(get_user))
|
||||||
|
.route("/user/notifications/list", get(list_notifications))
|
||||||
|
.route("/user/notifications/count", get(count_notifications))
|
||||||
.route("/account/register", post(register_user))
|
.route("/account/register", post(register_user))
|
||||||
.route("/account/login", post(login_user))
|
.route("/account/login", post(login_user))
|
||||||
.route("/account/my_profile", get(my_profile))
|
.route("/account/my_profile", get(my_profile))
|
||||||
|
@ -99,21 +93,3 @@ fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a list of all unresolved edit conflicts.
|
|
||||||
#[debug_handler]
|
|
||||||
async fn edit_conflicts(
|
|
||||||
Extension(user): Extension<LocalUserView>,
|
|
||||||
data: Data<IbisData>,
|
|
||||||
) -> MyResult<Json<Vec<ApiConflict>>> {
|
|
||||||
let conflicts = DbConflict::list(&user.person, &data)?;
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
|
use super::check_is_admin;
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{
|
backend::{
|
||||||
database::{read_jwt_secret, IbisData},
|
database::{conflict::DbConflict, read_jwt_secret, IbisData},
|
||||||
error::MyResult,
|
error::MyResult,
|
||||||
},
|
},
|
||||||
common::{DbPerson, GetUserForm, LocalUserView, LoginUserForm, RegisterUserForm},
|
common::{
|
||||||
|
DbArticle,
|
||||||
|
DbPerson,
|
||||||
|
GetUserForm,
|
||||||
|
LocalUserView,
|
||||||
|
LoginUserForm,
|
||||||
|
Notification,
|
||||||
|
RegisterUserForm,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use axum::{extract::Query, Form, Json};
|
use axum::{extract::Query, Extension, Form, Json};
|
||||||
use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite};
|
use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
use bcrypt::verify;
|
use bcrypt::verify;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use futures::future::try_join_all;
|
||||||
use jsonwebtoken::{
|
use jsonwebtoken::{
|
||||||
decode,
|
decode,
|
||||||
encode,
|
encode,
|
||||||
|
@ -145,3 +155,49 @@ pub(in crate::backend::api) async fn get_user(
|
||||||
&data,
|
&data,
|
||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub(crate) async fn list_notifications(
|
||||||
|
Extension(user): Extension<LocalUserView>,
|
||||||
|
data: Data<IbisData>,
|
||||||
|
) -> MyResult<Json<Vec<Notification>>> {
|
||||||
|
let is_admin = check_is_admin(&user).is_ok();
|
||||||
|
let conflicts = DbConflict::list(&user.person, &data)?;
|
||||||
|
let conflicts: Vec<_> = try_join_all(conflicts.into_iter().map(|c| {
|
||||||
|
let data = data.reset_request_count();
|
||||||
|
async move { c.to_api_conflict(is_admin, &data).await }
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
let mut notifications: Vec<_> = conflicts
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(Notification::EditConflict)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if check_is_admin(&user).is_ok() {
|
||||||
|
let articles = DbArticle::list_approval_required(&data)?;
|
||||||
|
notifications.extend(
|
||||||
|
articles
|
||||||
|
.into_iter()
|
||||||
|
.map(Notification::ArticleApprovalRequired),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(notifications))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub(crate) async fn count_notifications(
|
||||||
|
Extension(user): Extension<LocalUserView>,
|
||||||
|
data: Data<IbisData>,
|
||||||
|
) -> MyResult<Json<usize>> {
|
||||||
|
let mut count = 0;
|
||||||
|
let conflicts = DbConflict::list(&user.person, &data)?;
|
||||||
|
count += conflicts.len();
|
||||||
|
if check_is_admin(&user).is_ok() {
|
||||||
|
let articles = DbArticle::list_approval_required(&data)?;
|
||||||
|
count += articles.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(count))
|
||||||
|
}
|
||||||
|
|
|
@ -87,14 +87,13 @@ impl DbArticle {
|
||||||
.get_result::<Self>(conn.deref_mut())?)
|
.get_result::<Self>(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(id: ArticleId, data: &IbisData) -> MyResult<Self> {
|
pub fn read_view(id: ArticleId, is_admin: bool, data: &IbisData) -> MyResult<ArticleView> {
|
||||||
let mut conn = data.db_pool.get()?;
|
let mut conn = data.db_pool.get()?;
|
||||||
Ok(article::table.find(id).get_result(conn.deref_mut())?)
|
let mut query = article::table.find(id).into_boxed();
|
||||||
}
|
if !is_admin {
|
||||||
|
query = query.filter(article::dsl::approved.eq(true));
|
||||||
pub fn read_view(id: ArticleId, data: &IbisData) -> MyResult<ArticleView> {
|
}
|
||||||
let mut conn = data.db_pool.get()?;
|
let article: DbArticle = query.get_result(conn.deref_mut())?;
|
||||||
let article: DbArticle = { article::table.find(id).get_result(conn.deref_mut())? };
|
|
||||||
let latest_version = article.latest_edit_version(data)?;
|
let latest_version = article.latest_edit_version(data)?;
|
||||||
let edits = DbEdit::read_for_article(&article, data)?;
|
let edits = DbEdit::read_for_article(&article, data)?;
|
||||||
Ok(ArticleView {
|
Ok(ArticleView {
|
||||||
|
@ -107,6 +106,7 @@ impl DbArticle {
|
||||||
pub fn read_view_title(
|
pub fn read_view_title(
|
||||||
title: &str,
|
title: &str,
|
||||||
domain: Option<String>,
|
domain: Option<String>,
|
||||||
|
admin: bool,
|
||||||
data: &IbisData,
|
data: &IbisData,
|
||||||
) -> MyResult<ArticleView> {
|
) -> MyResult<ArticleView> {
|
||||||
let mut conn = data.db_pool.get()?;
|
let mut conn = data.db_pool.get()?;
|
||||||
|
@ -115,11 +115,14 @@ impl DbArticle {
|
||||||
.inner_join(instance::table)
|
.inner_join(instance::table)
|
||||||
.filter(article::dsl::title.eq(title))
|
.filter(article::dsl::title.eq(title))
|
||||||
.into_boxed();
|
.into_boxed();
|
||||||
let query = if let Some(domain) = domain {
|
let mut query = if let Some(domain) = domain {
|
||||||
query.filter(instance::dsl::domain.eq(domain))
|
query.filter(instance::dsl::domain.eq(domain))
|
||||||
} else {
|
} else {
|
||||||
query.filter(article::dsl::local.eq(true))
|
query.filter(article::dsl::local.eq(true))
|
||||||
};
|
};
|
||||||
|
if !admin {
|
||||||
|
query = query.filter(article::dsl::approved.eq(true));
|
||||||
|
}
|
||||||
query
|
query
|
||||||
.select(article::all_columns)
|
.select(article::all_columns)
|
||||||
.get_result(conn.deref_mut())?
|
.get_result(conn.deref_mut())?
|
||||||
|
@ -140,14 +143,6 @@ impl DbArticle {
|
||||||
.get_result(conn.deref_mut())?)
|
.get_result(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_local_title(title: &str, data: &IbisData) -> MyResult<Self> {
|
|
||||||
let mut conn = data.db_pool.get()?;
|
|
||||||
Ok(article::table
|
|
||||||
.filter(article::dsl::title.eq(title))
|
|
||||||
.filter(article::dsl::local.eq(true))
|
|
||||||
.get_result(conn.deref_mut())?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read all articles, ordered by most recently edited first.
|
/// Read all articles, ordered by most recently edited first.
|
||||||
///
|
///
|
||||||
/// TODO: Should get rid of only_local param and rely on instance_id
|
/// TODO: Should get rid of only_local param and rely on instance_id
|
||||||
|
|
|
@ -76,10 +76,14 @@ impl DbConflict {
|
||||||
Ok(delete(conflict::table.find(id)).get_result(conn.deref_mut())?)
|
Ok(delete(conflict::table.find(id)).get_result(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn to_api_conflict(&self, data: &Data<IbisData>) -> MyResult<Option<ApiConflict>> {
|
pub async fn to_api_conflict(
|
||||||
let article = DbArticle::read(self.article_id, data)?;
|
&self,
|
||||||
|
is_admin: bool,
|
||||||
|
data: &Data<IbisData>,
|
||||||
|
) -> MyResult<Option<ApiConflict>> {
|
||||||
|
let article = DbArticle::read_view(self.article_id, is_admin, data)?;
|
||||||
// Make sure to get latest version from origin so that all conflicts can be resolved
|
// Make sure to get latest version from origin so that all conflicts can be resolved
|
||||||
let original_article = article.ap_id.dereference_forced(data).await?;
|
let original_article = article.article.ap_id.dereference_forced(data).await?;
|
||||||
|
|
||||||
// create common ancestor version
|
// create common ancestor version
|
||||||
let edits = DbEdit::read_for_article(&original_article, data)?;
|
let edits = DbEdit::read_for_article(&original_article, data)?;
|
||||||
|
|
|
@ -52,7 +52,7 @@ impl Object for DbEdit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||||
let article = DbArticle::read(self.article_id, data)?;
|
let article = DbArticle::read_view(self.article_id, false, data)?;
|
||||||
let creator = DbPerson::read(self.creator_id, data)?;
|
let creator = DbPerson::read(self.creator_id, data)?;
|
||||||
Ok(ApubEdit {
|
Ok(ApubEdit {
|
||||||
kind: PatchType::Patch,
|
kind: PatchType::Patch,
|
||||||
|
@ -61,7 +61,7 @@ impl Object for DbEdit {
|
||||||
summary: self.summary,
|
summary: self.summary,
|
||||||
version: self.hash,
|
version: self.hash,
|
||||||
previous_version: self.previous_version_id,
|
previous_version: self.previous_version_id,
|
||||||
object: article.ap_id,
|
object: article.article.ap_id,
|
||||||
attributed_to: creator.ap_id,
|
attributed_to: creator.ap_id,
|
||||||
published: self.created,
|
published: self.created,
|
||||||
})
|
})
|
||||||
|
|
|
@ -35,7 +35,7 @@ impl Collection for DbEditCollection {
|
||||||
owner: &Self::Owner,
|
owner: &Self::Owner,
|
||||||
data: &Data<Self::DataType>,
|
data: &Data<Self::DataType>,
|
||||||
) -> Result<Self::Kind, Self::Error> {
|
) -> Result<Self::Kind, Self::Error> {
|
||||||
let article = DbArticle::read_view(owner.id, data)?;
|
let article = DbArticle::read_view(owner.id, false, data)?;
|
||||||
|
|
||||||
let edits = future::try_join_all(
|
let edits = future::try_join_all(
|
||||||
article
|
article
|
||||||
|
|
|
@ -94,8 +94,8 @@ async fn http_get_article(
|
||||||
Path(title): Path<String>,
|
Path(title): Path<String>,
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
) -> MyResult<FederationJson<WithContext<ApubArticle>>> {
|
) -> MyResult<FederationJson<WithContext<ApubArticle>>> {
|
||||||
let article = DbArticle::read_local_title(&title, &data)?;
|
let article = DbArticle::read_view_title(&title, None, false, &data)?;
|
||||||
let json = article.into_json(&data).await?;
|
let json = article.article.into_json(&data).await?;
|
||||||
Ok(FederationJson(WithContext::new_default(json)))
|
Ok(FederationJson(WithContext::new_default(json)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +104,8 @@ async fn http_get_article_edits(
|
||||||
Path(title): Path<String>,
|
Path(title): Path<String>,
|
||||||
data: Data<IbisData>,
|
data: Data<IbisData>,
|
||||||
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> {
|
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> {
|
||||||
let article = DbArticle::read_local_title(&title, &data)?;
|
let article = DbArticle::read_view_title(&title, None, false, &data)?;
|
||||||
let json = DbEditCollection::read_local(&article, &data).await?;
|
let json = DbEditCollection::read_local(&article.article, &data).await?;
|
||||||
Ok(FederationJson(WithContext::new_default(json)))
|
Ok(FederationJson(WithContext::new_default(json)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,6 +250,12 @@ pub struct ApiConflict {
|
||||||
pub previous_version_id: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Notification {
|
||||||
|
EditConflict(ApiConflict),
|
||||||
|
ArticleApprovalRequired(DbArticle),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
|
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
|
||||||
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]
|
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]
|
||||||
|
|
|
@ -19,6 +19,7 @@ use crate::{
|
||||||
ListArticlesForm,
|
ListArticlesForm,
|
||||||
LocalUserView,
|
LocalUserView,
|
||||||
LoginUserForm,
|
LoginUserForm,
|
||||||
|
Notification,
|
||||||
ProtectArticleForm,
|
ProtectArticleForm,
|
||||||
RegisterUserForm,
|
RegisterUserForm,
|
||||||
ResolveObject,
|
ResolveObject,
|
||||||
|
@ -132,10 +133,17 @@ impl ApiClient {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_articles_approval_required(&self) -> MyResult<Vec<DbArticle>> {
|
pub async fn notifications_list(&self) -> MyResult<Vec<Notification>> {
|
||||||
let req = self
|
let req = self
|
||||||
.client
|
.client
|
||||||
.get(self.request_endpoint("/api/v1/article/list/approval_required"));
|
.get(self.request_endpoint("/api/v1/user/notifications/list"));
|
||||||
|
handle_json_res(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn notifications_count(&self) -> MyResult<usize> {
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.get(self.request_endpoint("/api/v1/user/notifications/count"));
|
||||||
handle_json_res(req).await
|
handle_json_res(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,13 +238,6 @@ impl ApiClient {
|
||||||
handle_json_res(req).await
|
handle_json_res(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_conflicts(&self) -> MyResult<Vec<ApiConflict>> {
|
|
||||||
let req = self
|
|
||||||
.client
|
|
||||||
.get(self.request_endpoint("/api/v1/edit_conflicts"));
|
|
||||||
Ok(handle_json_res(req).await.unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> {
|
pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> {
|
||||||
let resolve_object = ResolveObject { id };
|
let resolve_object = ResolveObject { id };
|
||||||
self.get_query("/api/v1/article/resolve", Some(resolve_object))
|
self.get_query("/api/v1/article/resolve", Some(resolve_object))
|
||||||
|
|
|
@ -13,10 +13,10 @@ use crate::{
|
||||||
list::ListArticles,
|
list::ListArticles,
|
||||||
read::ReadArticle,
|
read::ReadArticle,
|
||||||
},
|
},
|
||||||
conflicts::Conflicts,
|
|
||||||
diff::EditDiff,
|
diff::EditDiff,
|
||||||
instance::{details::InstanceDetails, list::ListInstances},
|
instance::{details::InstanceDetails, list::ListInstances},
|
||||||
login::Login,
|
login::Login,
|
||||||
|
notifications::Notifications,
|
||||||
register::Register,
|
register::Register,
|
||||||
search::Search,
|
search::Search,
|
||||||
user_profile::UserProfile,
|
user_profile::UserProfile,
|
||||||
|
@ -125,7 +125,7 @@ pub fn App() -> impl IntoView {
|
||||||
<Route path="/login" view=Login />
|
<Route path="/login" view=Login />
|
||||||
<Route path="/register" view=Register />
|
<Route path="/register" view=Register />
|
||||||
<Route path="/search" view=Search />
|
<Route path="/search" view=Search />
|
||||||
<Route path="/conflicts" view=Conflicts />
|
<Route path="/notifications" view=Notifications />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
@ -19,6 +19,15 @@ pub fn Nav() -> impl IntoView {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
let notification_count = create_resource(
|
||||||
|
|| (),
|
||||||
|
move |_| async move {
|
||||||
|
GlobalState::api_client()
|
||||||
|
.notifications_count()
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let (search_query, set_search_query) = create_signal(String::new());
|
let (search_query, set_search_query) = create_signal(String::new());
|
||||||
let mut dark_mode = expect_context::<DarkMode>();
|
let mut dark_mode = expect_context::<DarkMode>();
|
||||||
|
@ -57,7 +66,7 @@ pub fn Nav() -> impl IntoView {
|
||||||
<A href="/article/create">"Create Article"</A>
|
<A href="/article/create">"Create Article"</A>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<A href="/conflicts">"Edit Conflicts"</A>
|
<A href="/notifications">"Notifications "<span class="indicator-item indicator-end badge badge-neutral">{notification_count}</span></A>
|
||||||
</li>
|
</li>
|
||||||
</Show>
|
</Show>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{newtypes::ConflictId, ApiConflict, ArticleView, EditArticleForm},
|
common::{newtypes::ConflictId, ApiConflict, ArticleView, EditArticleForm, Notification},
|
||||||
frontend::{
|
frontend::{
|
||||||
app::GlobalState,
|
app::GlobalState,
|
||||||
components::{
|
components::{
|
||||||
|
@ -35,10 +35,14 @@ pub fn EditArticle() -> impl IntoView {
|
||||||
let conflict_id = ConflictId(conflict_id.parse().unwrap());
|
let conflict_id = ConflictId(conflict_id.parse().unwrap());
|
||||||
async move {
|
async move {
|
||||||
let conflict = GlobalState::api_client()
|
let conflict = GlobalState::api_client()
|
||||||
.get_conflicts()
|
.notifications_list()
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter_map(|n| match n {
|
||||||
|
Notification::EditConflict(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
.find(|c| c.id == conflict_id)
|
.find(|c| c.id == conflict_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
set_edit_response.set(EditResponse::Conflict(conflict));
|
set_edit_response.set(EditResponse::Conflict(conflict));
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
use crate::frontend::{app::GlobalState, article_link, article_title};
|
|
||||||
use leptos::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Conflicts() -> impl IntoView {
|
|
||||||
let conflicts = create_resource(
|
|
||||||
move || {},
|
|
||||||
|_| async move { GlobalState::api_client().get_conflicts().await.unwrap() },
|
|
||||||
);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<h1>Your unresolved edit conflicts</h1>
|
|
||||||
<Suspense fallback=|| view! { "Loading..." }>
|
|
||||||
<ul>
|
|
||||||
{move || {
|
|
||||||
conflicts
|
|
||||||
.get()
|
|
||||||
.map(|c| {
|
|
||||||
c.into_iter()
|
|
||||||
.map(|c| {
|
|
||||||
let link = format!(
|
|
||||||
"{}/edit/{}",
|
|
||||||
article_link(&c.article),
|
|
||||||
c.id.0,
|
|
||||||
);
|
|
||||||
view! {
|
|
||||||
<li>
|
|
||||||
<a href=link>
|
|
||||||
{article_title(&c.article)} " - " {c.summary}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,10 +6,10 @@ use leptos::{create_resource, Resource, SignalGet};
|
||||||
use leptos_router::use_params_map;
|
use leptos_router::use_params_map;
|
||||||
|
|
||||||
pub(crate) mod article;
|
pub(crate) mod article;
|
||||||
pub(crate) mod conflicts;
|
|
||||||
pub(crate) mod diff;
|
pub(crate) mod diff;
|
||||||
pub(crate) mod instance;
|
pub(crate) mod instance;
|
||||||
pub(crate) mod login;
|
pub(crate) mod login;
|
||||||
|
pub(crate) mod notifications;
|
||||||
pub(crate) mod register;
|
pub(crate) mod register;
|
||||||
pub(crate) mod search;
|
pub(crate) mod search;
|
||||||
pub(crate) mod user_profile;
|
pub(crate) mod user_profile;
|
||||||
|
|
55
src/frontend/pages/notifications.rs
Normal file
55
src/frontend/pages/notifications.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::{
|
||||||
|
common::Notification,
|
||||||
|
frontend::{app::GlobalState, article_link, article_title},
|
||||||
|
};
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Notifications() -> impl IntoView {
|
||||||
|
let notifications = create_local_resource(
|
||||||
|
move || {},
|
||||||
|
|_| async move {
|
||||||
|
GlobalState::api_client()
|
||||||
|
.notifications_list()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h1 class="text-4xl font-bold font-serif my-6 grow flex-auto">Notifications</h1>
|
||||||
|
<Suspense fallback=|| view! { "Loading..." }>
|
||||||
|
<ul>
|
||||||
|
{move || {
|
||||||
|
notifications
|
||||||
|
.get()
|
||||||
|
.map(|n| {
|
||||||
|
n.into_iter()
|
||||||
|
.map(|n| {
|
||||||
|
use Notification::*;
|
||||||
|
let (link, title) = match n {
|
||||||
|
EditConflict(c) => (format!(
|
||||||
|
"{}/edit/{}",
|
||||||
|
article_link(&c.article),
|
||||||
|
c.id.0)
|
||||||
|
, format!("Conflict: {} - {}", article_title(&c.article), c.summary)),
|
||||||
|
ArticleApprovalRequired(a) => (article_link(&a), format!("Approval required: {}", a.title)),
|
||||||
|
|
||||||
|
};
|
||||||
|
// TODO: need buttons to approve/reject new article, also makes sense to discard edit conflict
|
||||||
|
view! {
|
||||||
|
<li>
|
||||||
|
<a class="link text-lg" href=link>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use ibis::{
|
use ibis::{
|
||||||
backend::{
|
backend::{
|
||||||
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation, IbisConfigSetup},
|
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
|
||||||
start,
|
start,
|
||||||
},
|
},
|
||||||
common::RegisterUserForm,
|
common::RegisterUserForm,
|
||||||
|
|
|
@ -14,6 +14,7 @@ use ibis::{
|
||||||
GetUserForm,
|
GetUserForm,
|
||||||
ListArticlesForm,
|
ListArticlesForm,
|
||||||
LoginUserForm,
|
LoginUserForm,
|
||||||
|
Notification,
|
||||||
ProtectArticleForm,
|
ProtectArticleForm,
|
||||||
RegisterUserForm,
|
RegisterUserForm,
|
||||||
SearchArticleForm,
|
SearchArticleForm,
|
||||||
|
@ -377,9 +378,12 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome\nexample\ntext\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
|
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome\nexample\ntext\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
|
||||||
|
|
||||||
let conflicts = data.alpha.get_conflicts().await?;
|
let notifications = data.alpha.notifications_list().await?;
|
||||||
assert_eq!(1, conflicts.len());
|
assert_eq!(1, notifications.len());
|
||||||
assert_eq!(conflicts[0], edit_res);
|
let Notification::EditConflict(conflict) = ¬ifications[0] else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(conflict, &edit_res);
|
||||||
|
|
||||||
let edit_form = EditArticleForm {
|
let edit_form = EditArticleForm {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
|
@ -391,8 +395,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
||||||
let edit_res = data.alpha.edit_article(&edit_form).await?;
|
let edit_res = data.alpha.edit_article(&edit_form).await?;
|
||||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||||
|
|
||||||
let conflicts = data.alpha.get_conflicts().await?;
|
assert_eq!(0, data.alpha.notifications_count().await?);
|
||||||
assert_eq!(0, conflicts.len());
|
|
||||||
|
|
||||||
data.stop()
|
data.stop()
|
||||||
}
|
}
|
||||||
|
@ -463,23 +466,28 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||||
assert_eq!(1, edit_res.edits.len());
|
assert_eq!(1, edit_res.edits.len());
|
||||||
assert!(!edit_res.article.local);
|
assert!(!edit_res.article.local);
|
||||||
|
|
||||||
let conflicts = data.gamma.get_conflicts().await?;
|
assert_eq!(1, data.gamma.notifications_count().await?);
|
||||||
assert_eq!(1, conflicts.len());
|
let notifications = data.gamma.notifications_list().await?;
|
||||||
|
assert_eq!(1, notifications.len());
|
||||||
|
let Notification::EditConflict(conflict) = ¬ifications[0] else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
// resolve the conflict
|
// resolve the conflict
|
||||||
let edit_form = EditArticleForm {
|
let edit_form = EditArticleForm {
|
||||||
article_id: resolve_res.article.id,
|
article_id: resolve_res.article.id,
|
||||||
new_text: "aaaa\n".to_string(),
|
new_text: "aaaa\n".to_string(),
|
||||||
summary: "summary".to_string(),
|
summary: "summary".to_string(),
|
||||||
previous_version_id: conflicts[0].previous_version_id.clone(),
|
previous_version_id: conflict.previous_version_id.clone(),
|
||||||
resolve_conflict_id: Some(conflicts[0].id),
|
resolve_conflict_id: Some(conflict.id),
|
||||||
};
|
};
|
||||||
let edit_res = data.gamma.edit_article(&edit_form).await?;
|
let edit_res = data.gamma.edit_article(&edit_form).await?;
|
||||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||||
assert_eq!(3, edit_res.edits.len());
|
assert_eq!(3, edit_res.edits.len());
|
||||||
|
|
||||||
let conflicts = data.gamma.get_conflicts().await?;
|
assert_eq!(0, data.gamma.notifications_count().await?);
|
||||||
assert_eq!(0, conflicts.len());
|
let notifications = data.gamma.notifications_list().await?;
|
||||||
|
assert_eq!(0, notifications.len());
|
||||||
|
|
||||||
data.stop()
|
data.stop()
|
||||||
}
|
}
|
||||||
|
@ -519,8 +527,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = data.alpha.edit_article(&edit_form).await?;
|
let edit_res = data.alpha.edit_article(&edit_form).await?;
|
||||||
let conflicts = data.alpha.get_conflicts().await?;
|
assert_eq!(0, data.alpha.notifications_count().await?);
|
||||||
assert_eq!(0, conflicts.len());
|
|
||||||
assert_eq!(3, edit_res.edits.len());
|
assert_eq!(3, edit_res.edits.len());
|
||||||
assert_eq!("my\nexample\narticle\n", edit_res.article.text);
|
assert_eq!("my\nexample\narticle\n", edit_res.article.text);
|
||||||
|
|
||||||
|
@ -752,14 +759,15 @@ async fn test_article_approval_required() -> MyResult<()> {
|
||||||
};
|
};
|
||||||
data.alpha.login(form).await?;
|
data.alpha.login(form).await?;
|
||||||
|
|
||||||
let list_approval_required = data.alpha.list_articles_approval_required().await?;
|
assert_eq!(1, data.alpha.notifications_count().await?);
|
||||||
assert_eq!(1, list_approval_required.len());
|
let notifications = data.alpha.notifications_list().await?;
|
||||||
assert_eq!(create_res.article.id, list_approval_required[0].id);
|
assert_eq!(1, notifications.len());
|
||||||
|
let Notification::ArticleApprovalRequired(notif) = ¬ifications[0] else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(create_res.article.id, notif.id);
|
||||||
|
|
||||||
let approve = data
|
let approve = data.alpha.approve_article(notif.id).await?;
|
||||||
.alpha
|
|
||||||
.approve_article(list_approval_required[0].id)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(create_res.article.id, approve.id);
|
assert_eq!(create_res.article.id, approve.id);
|
||||||
assert!(approve.approved);
|
assert!(approve.approved);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue