License federation (#343)

* Federate license

* Make it possible to use no license
This commit is contained in:
Baptiste Gelez 2018-12-09 18:43:34 +01:00 committed by GitHub
parent e9f2f769be
commit b73fbd3768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 126 additions and 45 deletions

View File

@ -220,3 +220,12 @@ pub struct Source {
} }
impl Object for Source {} impl Object for Source {}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")]
pub struct Licensed {
#[activitystreams(concrete(String), functional)]
pub license: Option<serde_json::Value>,
}
impl Object for Licensed {}

View File

@ -1,4 +1,5 @@
use activitypub::{ use activitypub::{
CustomObject,
activity::{Create, Delete, Update}, activity::{Create, Delete, Update},
link, link,
object::{Article, Image, Tombstone}, object::{Article, Image, Tombstone},
@ -18,7 +19,7 @@ use plume_api::posts::PostEndpoint;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{Deletable, FromActivity}, inbox::{Deletable, FromActivity},
Hashtag, Id, IntoId, Source, PUBLIC_VISIBILTY, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
}, },
utils::md_to_html, utils::md_to_html,
}; };
@ -32,6 +33,8 @@ use tags::Tag;
use users::User; use users::User;
use {ap_url, Connection, BASE_URL}; use {ap_url, Connection, BASE_URL};
pub type LicensedArticle = CustomObject<Licensed, Article>;
#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)] #[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")] #[changeset_options(treat_none_as_null = "true")]
pub struct Post { pub struct Post {
@ -418,7 +421,7 @@ impl Post {
}) })
} }
pub fn to_activity(&self, conn: &Connection) -> Article { pub fn to_activity(&self, conn: &Connection) -> LicensedArticle {
let mut to = self.get_receivers_urls(conn); let mut to = self.get_receivers_urls(conn);
to.push(PUBLIC_VISIBILTY.to_string()); to.push(PUBLIC_VISIBILTY.to_string());
@ -516,7 +519,9 @@ impl Post {
.object_props .object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])
.expect("Post::to_activity: cc error"); .expect("Post::to_activity: cc error");
article let mut license = Licensed::default();
license.set_license_string(self.license.clone()).expect("Post::to_activity: license error");
LicensedArticle::new(article, license)
} }
pub fn create_activity(&self, conn: &Connection) -> Create { pub fn create_activity(&self, conn: &Connection) -> Create {
@ -527,7 +532,7 @@ impl Post {
.expect("Post::create_activity: id error"); .expect("Post::create_activity: id error");
act.object_props act.object_props
.set_to_link_vec::<Id>( .set_to_link_vec::<Id>(
article article.object
.object_props .object_props
.to_link_vec() .to_link_vec()
.expect("Post::create_activity: Couldn't copy 'to'"), .expect("Post::create_activity: Couldn't copy 'to'"),
@ -535,7 +540,7 @@ impl Post {
.expect("Post::create_activity: to error"); .expect("Post::create_activity: to error");
act.object_props act.object_props
.set_cc_link_vec::<Id>( .set_cc_link_vec::<Id>(
article article.object
.object_props .object_props
.cc_link_vec() .cc_link_vec()
.expect("Post::create_activity: Couldn't copy 'cc'"), .expect("Post::create_activity: Couldn't copy 'cc'"),
@ -558,7 +563,7 @@ impl Post {
.expect("Post::update_activity: id error"); .expect("Post::update_activity: id error");
act.object_props act.object_props
.set_to_link_vec::<Id>( .set_to_link_vec::<Id>(
article article.object
.object_props .object_props
.to_link_vec() .to_link_vec()
.expect("Post::update_activity: Couldn't copy 'to'"), .expect("Post::update_activity: Couldn't copy 'to'"),
@ -566,7 +571,7 @@ impl Post {
.expect("Post::update_activity: to error"); .expect("Post::update_activity: to error");
act.object_props act.object_props
.set_cc_link_vec::<Id>( .set_cc_link_vec::<Id>(
article article.object
.object_props .object_props
.cc_link_vec() .cc_link_vec()
.expect("Post::update_activity: Couldn't copy 'cc'"), .expect("Post::update_activity: Couldn't copy 'cc'"),
@ -577,44 +582,48 @@ impl Post {
.expect("Post::update_activity: actor error"); .expect("Post::update_activity: actor error");
act.update_props act.update_props
.set_object_object(article) .set_object_object(article)
.expect("Article::update_activity: object error"); .expect("Post::update_activity: object error");
act act
} }
pub fn handle_update(conn: &Connection, updated: &Article, searcher: &Searcher) { pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) {
let id = updated let id = updated.object
.object_props .object_props
.id_string() .id_string()
.expect("Post::handle_update: id error"); .expect("Post::handle_update: id error");
let mut post = Post::find_by_ap_url(conn, &id).expect("Post::handle_update: finding error"); let mut post = Post::find_by_ap_url(conn, &id).expect("Post::handle_update: finding error");
if let Ok(title) = updated.object_props.name_string() { if let Ok(title) = updated.object.object_props.name_string() {
post.slug = title.to_kebab_case(); post.slug = title.to_kebab_case();
post.title = title; post.title = title;
} }
if let Ok(content) = updated.object_props.content_string() { if let Ok(content) = updated.object.object_props.content_string() {
post.content = SafeString::new(&content); post.content = SafeString::new(&content);
} }
if let Ok(subtitle) = updated.object_props.summary_string() { if let Ok(subtitle) = updated.object.object_props.summary_string() {
post.subtitle = subtitle; post.subtitle = subtitle;
} }
if let Ok(ap_url) = updated.object_props.url_string() { if let Ok(ap_url) = updated.object.object_props.url_string() {
post.ap_url = ap_url; post.ap_url = ap_url;
} }
if let Ok(source) = updated.ap_object_props.source_object::<Source>() { if let Ok(source) = updated.object.ap_object_props.source_object::<Source>() {
post.source = source.content; post.source = source.content;
} }
if let Ok(license) = updated.custom_props.license_string() {
post.license = license;
}
let mut txt_hashtags = md_to_html(&post.source) let mut txt_hashtags = md_to_html(&post.source)
.2 .2
.into_iter() .into_iter()
.map(|s| s.to_camel_case()) .map(|s| s.to_camel_case())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(mention_tags)) = updated.object_props.tag.clone() { if let Some(serde_json::Value::Array(mention_tags)) = updated.object.object_props.tag.clone() {
let mut mentions = vec![]; let mut mentions = vec![];
let mut tags = vec![]; let mut tags = vec![];
let mut hashtags = vec![]; let mut hashtags = vec![];
@ -782,8 +791,10 @@ impl Post {
} }
} }
impl<'a> FromActivity<Article, (&'a Connection, &'a Searcher)> for Post { impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post {
fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: Article, _actor: Id) -> Post { fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: LicensedArticle, _actor: Id) -> Post {
let license = article.custom_props.license_string().unwrap_or_default();
let article = article.object;
if let Some(post) = Post::find_by_ap_url( if let Some(post) = Post::find_by_ap_url(
conn, conn,
&article.object_props.id_string().unwrap_or_default(), &article.object_props.id_string().unwrap_or_default(),
@ -829,7 +840,7 @@ impl<'a> FromActivity<Article, (&'a Connection, &'a Searcher)> for Post {
.expect("Post::from_activity: content error"), .expect("Post::from_activity: content error"),
), ),
published: true, published: true,
license: String::from("CC-BY-SA"), // TODO license: license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article.object_props.url_string().unwrap_or_else(|_| ap_url: article.object_props.url_string().unwrap_or_else(|_|
article article

View File

@ -609,6 +609,13 @@ msgstr "Administration"
msgid "None" msgid "None"
msgstr "" msgstr ""
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "Falls es dies nicht gibt, lass es leer"
msgid "All rights reserved."
msgstr ""
#~ msgid "Home to" #~ msgid "Home to"
#~ msgstr "Heimat von" #~ msgstr "Heimat von"

View File

@ -593,3 +593,9 @@ msgstr ""
msgid "None" msgid "None"
msgstr "" msgstr ""
msgid "Let it empty reserve all rights"
msgstr ""
msgid "All rights reserved."
msgstr ""

View File

@ -609,3 +609,10 @@ msgstr "Illustration"
msgid "None" msgid "None"
msgstr "Aucun" msgstr "Aucun"
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "Laisser vide sil ny en a pas"
msgid "All rights reserved."
msgstr ""

View File

@ -600,6 +600,13 @@ msgstr "Administración"
msgid "None" msgid "None"
msgstr "" msgstr ""
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "Deixar baldeiro si non hai ningunha"
msgid "All rights reserved."
msgstr ""
#~ msgid "Home to" #~ msgid "Home to"
#~ msgstr "Fogar de" #~ msgstr "Fogar de"

View File

@ -603,6 +603,13 @@ msgstr "Amministrazione"
msgid "None" msgid "None"
msgstr "" msgstr ""
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "Lascialo vuoto se non è presente nessuno"
msgid "All rights reserved."
msgstr ""
#~ msgid "Home to" #~ msgid "Home to"
#~ msgstr "Casa di" #~ msgstr "Casa di"

View File

@ -595,6 +595,13 @@ msgstr "図"
msgid "None" msgid "None"
msgstr "なし" msgstr "なし"
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "不要な場合は空にしてください"
msgid "All rights reserved."
msgstr ""
#~ msgid "Welcome to {{ instance_name | escape }}" #~ msgid "Welcome to {{ instance_name | escape }}"
#~ msgstr "{{ instance_name | escape }} へようこそ" #~ msgstr "{{ instance_name | escape }} へようこそ"

View File

@ -615,6 +615,12 @@ msgstr "Administrasjon"
msgid "None" msgid "None"
msgstr "" msgstr ""
msgid "Let it empty reserve all rights"
msgstr ""
msgid "All rights reserved."
msgstr ""
#~ msgid "Home to" #~ msgid "Home to"
#~ msgstr "Hjem for" #~ msgstr "Hjem for"

View File

@ -609,6 +609,13 @@ msgstr "Ilustracja"
msgid "None" msgid "None"
msgstr "Brak" msgstr "Brak"
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "Pozostaw puste, jeżeli niepotrzebne"
msgid "All rights reserved."
msgstr ""
#~ msgid "Home to" #~ msgid "Home to"
#~ msgstr "Dom dla" #~ msgstr "Dom dla"

View File

@ -579,3 +579,9 @@ msgstr ""
msgid "None" msgid "None"
msgstr "" msgstr ""
msgid "Let it empty reserve all rights"
msgstr ""
msgid "All rights reserved."
msgstr ""

View File

@ -620,6 +620,13 @@ msgstr "Иллюстрация"
msgid "None" msgid "None"
msgstr "Нет" msgstr "Нет"
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "Оставьте пустым если нет"
msgid "All rights reserved."
msgstr ""
#~ msgid "Home to" #~ msgid "Home to"
#~ msgstr "Дом для" #~ msgstr "Дом для"

View File

@ -1,4 +1,3 @@
use activitypub::object::Article;
use chrono::Utc; use chrono::Utc;
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
use rocket::request::LenientForm; use rocket::request::LenientForm;
@ -89,7 +88,7 @@ pub fn details_response(blog: String, slug: String, conn: DbConn, user: Option<U
} }
#[get("/~/<blog>/<slug>", rank = 3)] #[get("/~/<blog>/<slug>", rank = 3)]
pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<Article>, Option<String>> { pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
let blog = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?; let blog = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?; let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
if post.published { if post.published {
@ -123,11 +122,13 @@ pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe>
&(&*conn, &intl.catalog, Some(user)), &(&*conn, &intl.catalog, Some(user)),
b, b,
false, false,
&NewPostForm::default(), &NewPostForm {
license: Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or_else(||String::from("CC-BY-SA")),
..NewPostForm::default()
},
true, true,
None, None,
ValidationErrors::default(), ValidationErrors::default(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
medias medias
))) )))
} }
@ -171,7 +172,6 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) ->
!post.published, !post.published,
Some(post), Some(post),
ValidationErrors::default(), ValidationErrors::default(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
medias medias
))) )))
} }
@ -209,12 +209,6 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
} else { } else {
let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref()); let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref());
let license = if !form.license.is_empty() {
form.license.to_string()
} else {
Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or_else(|| String::from("CC-BY-SA"))
};
// update publication date if when this article is no longer a draft // update publication date if when this article is no longer a draft
let newly_published = if !post.published && !form.draft { let newly_published = if !post.published && !form.draft {
post.published = true; post.published = true;
@ -229,7 +223,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
post.subtitle = form.subtitle.clone(); post.subtitle = form.subtitle.clone();
post.content = SafeString::new(&content); post.content = SafeString::new(&content);
post.source = form.content.clone(); post.source = form.content.clone();
post.license = license; post.license = form.license.clone();
post.cover_id = form.cover; post.cover_id = form.cover;
post.update(&*conn, &searcher); post.update(&*conn, &searcher);
let post = post.update_ap_url(&*conn); let post = post.update_ap_url(&*conn);
@ -270,7 +264,6 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
form.draft.clone(), form.draft.clone(),
Some(post), Some(post),
errors.clone(), errors.clone(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
medias.clone() medias.clone()
)); ));
Err(Some(temp)) Err(Some(temp))
@ -330,11 +323,7 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
title: form.title.to_string(), title: form.title.to_string(),
content: SafeString::new(&content), content: SafeString::new(&content),
published: !form.draft, published: !form.draft,
license: if !form.license.is_empty() { license: form.license.clone(),
form.license.to_string()
} else {
Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or_else(||String::from("CC-BY-SA"))
},
ap_url: "".to_string(), ap_url: "".to_string(),
creation_date: None, creation_date: None,
subtitle: form.subtitle.clone(), subtitle: form.subtitle.clone(),
@ -390,7 +379,6 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
form.draft, form.draft,
None, None,
errors.clone(), errors.clone(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
medias medias
)))) ))))
} }

View File

@ -1,4 +1,4 @@
use activitypub::{activity::Create, collection::OrderedCollection, object::Article}; use activitypub::{activity::Create, collection::OrderedCollection};
use atom_syndication::{Entry, FeedBuilder}; use atom_syndication::{Entry, FeedBuilder};
use rocket::{ use rocket::{
http::{ContentType, Cookies}, http::{ContentType, Cookies},
@ -18,7 +18,7 @@ use plume_common::activity_pub::{
}; };
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::Post, blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::{LicensedArticle, Post},
reshares::Reshare, users::*, reshares::Reshare, users::*,
}; };
use routes::Page; use routes::Page;
@ -56,7 +56,7 @@ pub fn details(
let searcher = searcher.clone(); let searcher = searcher.clone();
worker.execute(move || { worker.execute(move || {
for create_act in user_clone.fetch_outbox::<Create>() { for create_act in user_clone.fetch_outbox::<Create>() {
match create_act.create_props.object_object::<Article>() { match create_act.create_props.object_object::<LicensedArticle>() {
Ok(article) => { Ok(article) => {
Post::from_activity( Post::from_activity(
&(&*fetch_articles_conn, &searcher), &(&*fetch_articles_conn, &searcher),

View File

@ -53,7 +53,13 @@
@Html(&article.content) @Html(&article.content)
</article> </article>
<div class="article-meta"> <div class="article-meta">
<p>@i18n!(ctx.1, "This article is under the {0} license."; &article.license)</p> <p>
@if article.license.is_empty() {
@i18n!(ctx.1, "All rights reserved."; &article.license)
} else {
@i18n!(ctx.1, "This article is under the {0} license."; &article.license)
}
</p>
<ul class="tags"> <ul class="tags">
@for tag in tags { @for tag in tags {
@if !tag.is_hashtag { @if !tag.is_hashtag {

View File

@ -8,7 +8,7 @@
@use routes::posts::NewPostForm; @use routes::posts::NewPostForm;
@use routes::*; @use routes::*;
@(ctx: BaseContext, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, default_license: String, medias: Vec<Media>) @(ctx: BaseContext, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>)
@:base(ctx, &i18n!(ctx.1, if editing { "Edit {0}" } else { "New post" }; &form.title), {}, {}, { @:base(ctx, &i18n!(ctx.1, if editing { "Edit {0}" } else { "New post" }; &form.title), {}, {}, {
<h1> <h1>
@ -35,7 +35,7 @@
@input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "") @input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "")
@input!(ctx.1, license (optional text), "License", &i18n!(ctx.1, "Default license will be {0}"; &default_license), form, errors, "") @input!(ctx.1, license (optional text), "License", "Let it empty reserve all rights", form, errors, "")
<label for="cover">@i18n!(ctx.1, "Illustration")<small>@i18n!(ctx.1, "Optional")</small></label> <label for="cover">@i18n!(ctx.1, "Illustration")<small>@i18n!(ctx.1, "Optional")</small></label>
<select id="cover" name="cover"> <select id="cover" name="cover">