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 {}
#[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::{
CustomObject,
activity::{Create, Delete, Update},
link,
object::{Article, Image, Tombstone},
@ -18,7 +19,7 @@ use plume_api::posts::PostEndpoint;
use plume_common::{
activity_pub::{
inbox::{Deletable, FromActivity},
Hashtag, Id, IntoId, Source, PUBLIC_VISIBILTY,
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
},
utils::md_to_html,
};
@ -32,6 +33,8 @@ use tags::Tag;
use users::User;
use {ap_url, Connection, BASE_URL};
pub type LicensedArticle = CustomObject<Licensed, Article>;
#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")]
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);
to.push(PUBLIC_VISIBILTY.to_string());
@ -516,7 +519,9 @@ impl Post {
.object_props
.set_cc_link_vec::<Id>(vec![])
.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 {
@ -527,7 +532,7 @@ impl Post {
.expect("Post::create_activity: id error");
act.object_props
.set_to_link_vec::<Id>(
article
article.object
.object_props
.to_link_vec()
.expect("Post::create_activity: Couldn't copy 'to'"),
@ -535,7 +540,7 @@ impl Post {
.expect("Post::create_activity: to error");
act.object_props
.set_cc_link_vec::<Id>(
article
article.object
.object_props
.cc_link_vec()
.expect("Post::create_activity: Couldn't copy 'cc'"),
@ -558,7 +563,7 @@ impl Post {
.expect("Post::update_activity: id error");
act.object_props
.set_to_link_vec::<Id>(
article
article.object
.object_props
.to_link_vec()
.expect("Post::update_activity: Couldn't copy 'to'"),
@ -566,7 +571,7 @@ impl Post {
.expect("Post::update_activity: to error");
act.object_props
.set_cc_link_vec::<Id>(
article
article.object
.object_props
.cc_link_vec()
.expect("Post::update_activity: Couldn't copy 'cc'"),
@ -577,44 +582,48 @@ impl Post {
.expect("Post::update_activity: actor error");
act.update_props
.set_object_object(article)
.expect("Article::update_activity: object error");
.expect("Post::update_activity: object error");
act
}
pub fn handle_update(conn: &Connection, updated: &Article, searcher: &Searcher) {
let id = updated
pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) {
let id = updated.object
.object_props
.id_string()
.expect("Post::handle_update: id 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.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);
}
if let Ok(subtitle) = updated.object_props.summary_string() {
if let Ok(subtitle) = updated.object.object_props.summary_string() {
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;
}
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;
}
if let Ok(license) = updated.custom_props.license_string() {
post.license = license;
}
let mut txt_hashtags = md_to_html(&post.source)
.2
.into_iter()
.map(|s| s.to_camel_case())
.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 tags = vec![];
let mut hashtags = vec![];
@ -782,8 +791,10 @@ impl Post {
}
}
impl<'a> FromActivity<Article, (&'a Connection, &'a Searcher)> for Post {
fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: Article, _actor: Id) -> Post {
impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for 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(
conn,
&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"),
),
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
ap_url: article.object_props.url_string().unwrap_or_else(|_|
article

View File

@ -609,6 +609,13 @@ msgstr "Administration"
msgid "None"
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"
#~ msgstr "Heimat von"

View File

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

View File

@ -609,3 +609,10 @@ msgstr "Illustration"
msgid "None"
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"
msgstr ""
#, fuzzy
msgid "Let it empty reserve all rights"
msgstr "Deixar baldeiro si non hai ningunha"
msgid "All rights reserved."
msgstr ""
#~ msgid "Home to"
#~ msgstr "Fogar de"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
use activitypub::object::Article;
use chrono::Utc;
use heck::{CamelCase, KebabCase};
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)]
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 post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
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)),
b,
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,
None,
ValidationErrors::default(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
medias
)))
}
@ -171,7 +172,6 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) ->
!post.published,
Some(post),
ValidationErrors::default(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
medias
)))
}
@ -209,12 +209,6 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
} else {
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
let newly_published = if !post.published && !form.draft {
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.content = SafeString::new(&content);
post.source = form.content.clone();
post.license = license;
post.license = form.license.clone();
post.cover_id = form.cover;
post.update(&*conn, &searcher);
let post = post.update_ap_url(&*conn);
@ -262,7 +256,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
}
} else {
let medias = Media::for_user(&*conn, user.id);
let temp = render!(posts::new(
let temp = render!(posts::new(
&(&*conn, &intl.catalog, Some(user)),
b,
true,
@ -270,7 +264,6 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
form.draft.clone(),
Some(post),
errors.clone(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
medias.clone()
));
Err(Some(temp))
@ -330,11 +323,7 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
title: form.title.to_string(),
content: SafeString::new(&content),
published: !form.draft,
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"))
},
license: form.license.clone(),
ap_url: "".to_string(),
creation_date: None,
subtitle: form.subtitle.clone(),
@ -390,7 +379,6 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
form.draft,
None,
errors.clone(),
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
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 rocket::{
http::{ContentType, Cookies},
@ -18,7 +18,7 @@ use plume_common::activity_pub::{
};
use plume_common::utils;
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::*,
};
use routes::Page;
@ -56,7 +56,7 @@ pub fn details(
let searcher = searcher.clone();
worker.execute(move || {
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) => {
Post::from_activity(
&(&*fetch_articles_conn, &searcher),

View File

@ -53,7 +53,13 @@
@Html(&article.content)
</article>
<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">
@for tag in tags {
@if !tag.is_hashtag {

View File

@ -8,7 +8,7 @@
@use routes::posts::NewPostForm;
@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), {}, {}, {
<h1>
@ -35,7 +35,7 @@
@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>
<select id="cover" name="cover">