Merge branch 'master' of https://github.com/Plume-org/Plume into icons

This commit is contained in:
Madeorsk 2018-09-04 21:55:44 +02:00
commit a30b99f93e
25 changed files with 299 additions and 108 deletions

22
docs/ENV-VARS.md Normal file
View File

@ -0,0 +1,22 @@
# Useful Environment Variables
Plume relies on some environment variables for some configuration options. You can either set them before
starting the app with `cargo run` or write them in a `.env` file to have automatically loaded.
Here are the variables that Plume uses:
- `BASE_URL`: the domain name, or IP and port on which Plume is listening. It is used in all federation-related code.
- `DB_URL`: the URL of the PostgreSQL database, used by Plume (`postgres://plume:plume@localhost/plume` by default).
- `POSTGRES_USER`: if you just want to use a different PostgreSQL user name, and keep the rest of the default URL.
- `POSTGRES_PASSWORD`: same as `POSTGRES_USER`, but for the password.
- `USE_HTTPS`: if it is `0`, federation and medias will be using HTTP by default (`1` by default).
- `ROCKET_ADDRESS`: the adress on which Plume should listen (`0.0.0.0` by default).
- `ROCKET_PORT`: the port on which Plume should listen ([`7878` by default](https://twitter.com/ag_dubs/status/852559264510070784))
- `ROCKET_SECRET_KEY`: key used to sign private cookies and for CSRF protection. If it is not set, it will be regenerated everytime you restart Plume,
meaning that all your users will get disconnected. You can generate one with `openssl rand -base64 32`.
## Diesel
Diesel, the tool we use to run migrations may be configured with the `DATABASE_URL` which should contain the URL of the
PostgreSQL database. Otherwise, you can specify `--database-url YOUR-URL` everytime you run a `diesel` command.

View File

@ -2,5 +2,6 @@
- [Installing Plume (for development or production)](INSTALL.md) - [Installing Plume (for development or production)](INSTALL.md)
- [Updating your instance](UPDATE.md) - [Updating your instance](UPDATE.md)
- [Useful Environment Variables](ENV-VARS.md)
- [Development Guide](DEVELOPMENT.md) - [Development Guide](DEVELOPMENT.md)
- [Making Plume available in your language](INTERNATIONALIZATION.md) - [Making Plume available in your language](INTERNATIONALIZATION.md)

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE follows DROP COLUMN ap_url;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE follows ADD COLUMN ap_url TEXT NOT NULL DEFAULT '';

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE posts DROP COLUMN subtitle;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE posts ADD COLUMN subtitle TEXT NOT NULL DEFAULT '';

View File

@ -1,7 +1,7 @@
use activitypub::{Actor, activity::{Accept, Follow as FollowAct}, actor::Person}; use activitypub::{Actor, activity::{Accept, Follow as FollowAct, Undo}, actor::Person};
use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox}, sign::Signer}; use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
use blogs::Blog; use blogs::Blog;
use notifications::*; use notifications::*;
use users::User; use users::User;
@ -12,19 +12,42 @@ use schema::follows;
pub struct Follow { pub struct Follow {
pub id: i32, pub id: i32,
pub follower_id: i32, pub follower_id: i32,
pub following_id: i32 pub following_id: i32,
pub ap_url: String,
} }
#[derive(Insertable)] #[derive(Insertable)]
#[table_name = "follows"] #[table_name = "follows"]
pub struct NewFollow { pub struct NewFollow {
pub follower_id: i32, pub follower_id: i32,
pub following_id: i32 pub following_id: i32,
pub ap_url: String,
} }
impl Follow { impl Follow {
insert!(follows, NewFollow); insert!(follows, NewFollow);
get!(follows); get!(follows);
find_by!(follows, find_by_ap_url, ap_url as String);
pub fn find(conn: &PgConnection, from: i32, to: i32) -> Option<Follow> {
follows::table.filter(follows::follower_id.eq(from))
.filter(follows::following_id.eq(to))
.get_result(conn)
.ok()
}
pub fn into_activity(&self, conn: &PgConnection) -> FollowAct {
let user = User::get(conn, self.follower_id).unwrap();
let target = User::get(conn, self.following_id).unwrap();
let mut act = FollowAct::default();
act.follow_props.set_actor_link::<Id>(user.clone().into_id()).expect("Follow::into_activity: actor error");
act.follow_props.set_object_object(user.into_activity(&*conn)).unwrap();
act.object_props.set_id_string(self.ap_url.clone()).unwrap();
act.object_props.set_to_link(target.clone().into_id()).expect("New Follow error while setting 'to'");
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("New Follow error while setting 'cc'");
act
}
/// from -> The one sending the follow request /// from -> The one sending the follow request
/// target -> The target of the request, responding with Accept /// target -> The target of the request, responding with Accept
@ -36,9 +59,12 @@ impl Follow {
from_id: i32, from_id: i32,
target_id: i32 target_id: i32
) -> Follow { ) -> Follow {
let from_url: String = from.clone().into_id().into();
let target_url: String = target.clone().into_id().into();
let res = Follow::insert(conn, NewFollow { let res = Follow::insert(conn, NewFollow {
follower_id: from_id, follower_id: from_id,
following_id: target_id following_id: target_id,
ap_url: format!("{}/follow/{}", from_url, target_url),
}); });
let mut accept = Accept::default(); let mut accept = Accept::default();
@ -77,3 +103,21 @@ impl Notify<PgConnection> for Follow {
}); });
} }
} }
impl Deletable<PgConnection, Undo> for Follow {
fn delete(&self, conn: &PgConnection) -> Undo {
diesel::delete(self).execute(conn).expect("Coudn't delete follow");
let mut undo = Undo::default();
undo.undo_props.set_actor_link(User::get(conn, self.follower_id).unwrap().into_id()).expect("Follow::delete: actor error");
undo.object_props.set_id_string(format!("{}/undo", self.ap_url)).expect("Follow::delete: id error");
undo.undo_props.set_object_object(self.into_activity(conn)).expect("Follow::delete: object error");
undo
}
fn delete_id(id: String, conn: &PgConnection) {
if let Some(follow) = Follow::find_by_ap_url(conn, id) {
follow.delete(conn);
}
}
}

View File

@ -33,7 +33,8 @@ pub struct Post {
pub published: bool, pub published: bool,
pub license: String, pub license: String,
pub creation_date: NaiveDateTime, pub creation_date: NaiveDateTime,
pub ap_url: String pub ap_url: String,
pub subtitle: String,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -46,7 +47,8 @@ pub struct NewPost {
pub published: bool, pub published: bool,
pub license: String, pub license: String,
pub creation_date: Option<NaiveDateTime>, pub creation_date: Option<NaiveDateTime>,
pub ap_url: String pub ap_url: String,
pub subtitle: String,
} }
impl Post { impl Post {
@ -185,6 +187,7 @@ impl Post {
article.object_props.set_attributed_to_link_vec::<Id>(authors).expect("Article::into_activity: attributedTo error"); article.object_props.set_attributed_to_link_vec::<Id>(authors).expect("Article::into_activity: attributedTo error");
article.object_props.set_content_string(self.content.get().clone()).expect("Article::into_activity: content error"); article.object_props.set_content_string(self.content.get().clone()).expect("Article::into_activity: content error");
article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Article::into_activity: published error"); article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Article::into_activity: published error");
article.object_props.set_summary_string(self.subtitle.clone()).expect("Article::into_activity: summary error");
article.object_props.set_tag_link_vec(mentions).expect("Article::into_activity: tag error"); article.object_props.set_tag_link_vec(mentions).expect("Article::into_activity: tag error");
article.object_props.set_url_string(self.ap_url.clone()).expect("Article::into_activity: url error"); article.object_props.set_url_string(self.ap_url.clone()).expect("Article::into_activity: url error");
article.object_props.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect()).expect("Article::into_activity: to error"); article.object_props.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect()).expect("Article::into_activity: to error");
@ -250,7 +253,8 @@ impl FromActivity<Article, PgConnection> for Post {
license: String::from("CC-0"), // TODO license: String::from("CC-0"), // TODO
// 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(article.object_props.id_string().expect("Post::from_activity: url + id error")), ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error")),
creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc()) creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc()),
subtitle: article.object_props.summary_string().expect("Post::from_activity: summary error")
}); });
for author in authors.into_iter() { for author in authors.into_iter() {

View File

@ -42,6 +42,7 @@ table! {
id -> Int4, id -> Int4,
follower_id -> Int4, follower_id -> Int4,
following_id -> Int4, following_id -> Int4,
ap_url -> Text,
} }
} }
@ -124,6 +125,7 @@ table! {
license -> Varchar, license -> Varchar,
creation_date -> Timestamp, creation_date -> Timestamp,
ap_url -> Varchar, ap_url -> Varchar,
subtitle -> Text,
} }
} }

View File

@ -567,7 +567,7 @@ impl WithInbox for User {
} }
fn is_local(&self) -> bool { fn is_local(&self) -> bool {
self.instance_id == 0 self.instance_id == 1
} }
} }

View File

@ -508,5 +508,15 @@ msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one" "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "" msgstr ""
#, fuzzy
msgid "Subtitle"
msgstr "Titel"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""
#~ msgid "Your password should be at least 8 characters long" #~ msgid "Your password should be at least 8 characters long"
#~ msgstr "Das Passwort sollte mindestens 8 Zeichen lang sein" #~ msgstr "Das Passwort sollte mindestens 8 Zeichen lang sein"

View File

@ -497,3 +497,12 @@ msgstr ""
msgid "" msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one" "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "" msgstr ""
msgid "Subtitle"
msgstr ""
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -503,4 +503,16 @@ msgstr "Envoyer"
msgid "" msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one" "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "Désolé, mais les inscriptions sont fermées sur cette instance. Essayez d'en trouver une autre." msgstr ""
"Désolé, mais les inscriptions sont fermées sur cette instance. Essayez d'en "
"trouver une autre."
#, fuzzy
msgid "Subtitle"
msgstr "Titre"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -496,4 +496,15 @@ msgstr "Enviar"
msgid "" msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one" "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "" msgstr ""
"Lamentámolo, pero o rexistro en esta instancia está pechado. Inténteo en outra instancia" "Lamentámolo, pero o rexistro en esta instancia está pechado. Inténteo en "
"outra instancia"
#, fuzzy
msgid "Subtitle"
msgstr "Título"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -512,6 +512,16 @@ msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one" "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "" msgstr ""
#, fuzzy
msgid "Subtitle"
msgstr "Tittel"
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""
#~ msgid "One reshare" #~ msgid "One reshare"
#~ msgid_plural "{{ count }} reshares" #~ msgid_plural "{{ count }} reshares"
#~ msgstr[0] "Én deling" #~ msgstr[0] "Én deling"

108
po/pl.po
View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-07-28 14:56+0200\n" "PO-Revision-Date: 2018-09-04 17:35+0200\n"
"Last-Translator: Marcin Mikołajczak <me@m4sk.in>\n" "Last-Translator: Marcin Mikołajczak <me@m4sk.in>\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: pl\n" "Language: pl\n"
@ -12,7 +12,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n" "|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 2.0.9\n" "X-Generator: Poedit 2.1.1\n"
msgid "Latest articles" msgid "Latest articles"
msgstr "Najnowsze artykuły" msgstr "Najnowsze artykuły"
@ -92,16 +92,15 @@ msgstr "Dodaj swoje"
msgid "One Boost" msgid "One Boost"
msgid_plural "{{ count }} Boosts" msgid_plural "{{ count }} Boosts"
msgstr[0] "" msgstr[0] "Jedno podbicie"
msgstr[1] "" msgstr[1] "{{ count }} podbicia"
msgstr[2] "" msgstr[2] "{{ count }} podbić"
#, fuzzy
msgid "I don&#x27;t want to boost this anymore" msgid "I don&#x27;t want to boost this anymore"
msgstr "Cofnij udostępnienie" msgstr "Cofnij podbicie"
msgid "Boost" msgid "Boost"
msgstr "" msgstr "Podbij"
msgid "Comments" msgid "Comments"
msgstr "Komentarze" msgstr "Komentarze"
@ -165,9 +164,8 @@ msgstr "Obserwuj"
msgid "Unfollow" msgid "Unfollow"
msgstr "Przestań obserwować" msgstr "Przestań obserwować"
#, fuzzy
msgid "Recently boosted" msgid "Recently boosted"
msgstr "Ostatnio udostępniono" msgstr "Ostatnio podbite"
msgid "One follower" msgid "One follower"
msgid_plural "{{ count }} followers" msgid_plural "{{ count }} followers"
@ -247,9 +245,8 @@ msgstr "Musisz się zalogować, aby zobaczyć swoje powiadomienia"
msgid "You need to be logged in order to write a new post" msgid "You need to be logged in order to write a new post"
msgstr "Musisz się zalogować, aby utworzyć wpis" msgstr "Musisz się zalogować, aby utworzyć wpis"
#, fuzzy
msgid "You need to be logged in order to boost a post" msgid "You need to be logged in order to boost a post"
msgstr "Musisz się zalogować, aby polubić wpis" msgstr "Musisz się zalogować, aby podbić wpis"
msgid "Invalid username or password" msgid "Invalid username or password"
msgstr "Nieprawidłowa nazwa użytkownika lub hasło" msgstr "Nieprawidłowa nazwa użytkownika lub hasło"
@ -268,18 +265,17 @@ msgstr ""
"Napisano przez {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}" "Napisano przez {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}"
"{{ link_3 }}" "{{ link_3 }}"
#, fuzzy
msgid "{{ data }} boosted your article" msgid "{{ data }} boosted your article"
msgstr "{{ data }} skomentował Twój artykuł" msgstr "{{ data }} podbił(a) Twój artykuł"
msgid "{{ data }} started following you" msgid "{{ data }} started following you"
msgstr "{{ data }} zaczął Cię obserwować" msgstr "{{ data }} zaczął(-ęła) Cię obserwować"
msgid "{{ data }} liked your article" msgid "{{ data }} liked your article"
msgstr "{{ data }} polubił Twój artykuł" msgstr "{{ data }} polubił(a) Twój artykuł"
msgid "{{ data }} commented your article" msgid "{{ data }} commented your article"
msgstr "{{ data }} skomentował Twój artykuł" msgstr "{{ data }} skomentował(a) Twój artykuł"
msgid "We couldn&#x27;t find this page." msgid "We couldn&#x27;t find this page."
msgstr "Nie udało się odnaleźć tej strony." msgstr "Nie udało się odnaleźć tej strony."
@ -294,7 +290,7 @@ msgid "You are not author in this blog."
msgstr "Nie jesteś autorem tego bloga." msgstr "Nie jesteś autorem tego bloga."
msgid "{{ data }} mentioned you." msgid "{{ data }} mentioned you."
msgstr "{{ data }} wspomniał o Tobie." msgstr "{{ data }} wspomniał(a) o Tobie."
msgid "Your comment" msgid "Your comment"
msgstr "Twój komentarz" msgstr "Twój komentarz"
@ -318,9 +314,8 @@ msgid "We need an email or a username to identify you"
msgstr "" msgstr ""
"Potrzebujemy nazwy użytkownika lub adresu e-mail, aby Cię zidentyfikować" "Potrzebujemy nazwy użytkownika lub adresu e-mail, aby Cię zidentyfikować"
#, fuzzy
msgid "Your password can't be empty" msgid "Your password can't be empty"
msgstr "Twój komentarz nie może być pusty" msgstr "Twoje hasło nie może być puste"
msgid "Passwords are not matching" msgid "Passwords are not matching"
msgstr "Hasła nie pasują do siebie" msgstr "Hasła nie pasują do siebie"
@ -361,20 +356,19 @@ msgid "Next page"
msgstr "Następna strona" msgstr "Następna strona"
msgid "{{ user }} mentioned you." msgid "{{ user }} mentioned you."
msgstr "{{ user }} wspomniał o Tobie." msgstr "{{ user }} wspomniał(a) o Tobie."
msgid "{{ user }} commented your article." msgid "{{ user }} commented your article."
msgstr "{{ user }} skomentował Twój artykuł." msgstr "{{ user }} skomentował(a) Twój artykuł."
msgid "{{ user }} is now following you." msgid "{{ user }} is now following you."
msgstr "{{ user }} zaczął Cię obserwować." msgstr "{{ user }} zaczął(-ęła) Cię obserwować."
msgid "{{ user }} liked your article." msgid "{{ user }} liked your article."
msgstr "{{ user }} polubił Twój artykuł." msgstr "{{ user }} polubił(a) Twój artykuł."
#, fuzzy
msgid "{{ user }} boosted your article." msgid "{{ user }} boosted your article."
msgstr "{{ user }} skomentował Twój artykuł." msgstr "{{ user }} podbił(a) Twój artykuł."
msgid "Source code" msgid "Source code"
msgstr "Kod źródłowy" msgstr "Kod źródłowy"
@ -441,7 +435,7 @@ msgid "people"
msgstr "osób" msgstr "osób"
msgid "Who wrote" msgid "Who wrote"
msgstr "Którzy napisali" msgstr "Które napisały"
msgid "articles" msgid "articles"
msgstr "artykuły" msgstr "artykuły"
@ -449,79 +443,83 @@ msgstr "artykuły"
msgid "Read the detailed rules" msgid "Read the detailed rules"
msgstr "Przeczytaj szczegółowe zasady" msgstr "Przeczytaj szczegółowe zasady"
#, fuzzy
msgid "Delete this article" msgid "Delete this article"
msgstr "Najnowsze artykuły" msgstr "Usuń ten artykuł"
msgid "And connected to" msgid "And connected to"
msgstr "" msgstr "Połączony z"
#, fuzzy
msgid "other instances" msgid "other instances"
msgstr "O tej instancji" msgstr "innych instancji"
#, fuzzy
msgid "Administred by" msgid "Administred by"
msgstr "Administracja" msgstr "Administrowany przez"
msgid "Runs Plume {{ version }}" msgid "Runs Plume {{ version }}"
msgstr "" msgstr "Działa na Plume {{ version }}"
#, fuzzy
msgid "Your media" msgid "Your media"
msgstr "Twój komentarz" msgstr "Twoja zawartość multimedialna"
msgid "Go to your gallery" msgid "Go to your gallery"
msgstr "" msgstr "Przejdź do swojej galerii"
msgid "{{ name}}'s avatar'" msgid "{{ name}}'s avatar'"
msgstr "" msgstr "Awatar {{name}}"
msgid "Media details" msgid "Media details"
msgstr "" msgstr "Szczegóły zawartości multimedialnej"
msgid "Go back to the gallery" msgid "Go back to the gallery"
msgstr "" msgstr "Powróć do galerii"
#, fuzzy
msgid "Markdown code" msgid "Markdown code"
msgstr "Markdown jest obsługiwany" msgstr "Kod Markdown"
msgid "Copy it in your articles to insert this media." msgid "Copy it in your articles to insert this media."
msgstr "" msgstr "Skopiuj do swoich artykułów, aby wstawić tę zawartość multimedialną."
msgid "Use as avatar" msgid "Use as avatar"
msgstr "" msgstr "Użyj jako awataru"
msgid "Delete" msgid "Delete"
msgstr "" msgstr "Usuń"
msgid "Upload" msgid "Upload"
msgstr "" msgstr "Wyślij"
msgid "You don't have any media yet." msgid "You don't have any media yet."
msgstr "" msgstr "Nie masz żadnej zawartości multimedialnej."
msgid "Media upload" msgid "Media upload"
msgstr "" msgstr "Wysyłanie zawartości multimedialnej"
#, fuzzy
msgid "Description" msgid "Description"
msgstr "Szczegółowy opis" msgstr "Opis"
#, fuzzy
msgid "Content warning" msgid "Content warning"
msgstr "Zawartość" msgstr "Ostrzeżenie o zawartości"
msgid "File" msgid "File"
msgstr "" msgstr "Plik"
msgid "Send" msgid "Send"
msgstr "" msgstr "Wyślij"
msgid "" msgid ""
"Sorry, but registrations are closed on this instance. Try to find another one" "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "" msgstr ""
"Przepraszamy, rejestracja jest zamknięta na tej instancji. Spróbuj znaleźć "
"inną"
msgid "Subtitle"
msgstr "Podtytuł"
msgid "Login to like"
msgstr "Zaloguj się aby polubić"
msgid "Login to boost"
msgstr "Zaloguj się aby podbić"
#~ msgid "One reshare" #~ msgid "One reshare"
#~ msgid_plural "{{ count }} reshares" #~ msgid_plural "{{ count }} reshares"

View File

@ -486,3 +486,12 @@ msgstr ""
msgid "Sorry, but registrations are closed on this instance. Try to find another one" msgid "Sorry, but registrations are closed on this instance. Try to find another one"
msgstr "" msgstr ""
msgid "Subtitle"
msgstr ""
msgid "Login to like"
msgstr ""
msgid "Login to boost"
msgstr ""

View File

@ -55,6 +55,10 @@ pub trait Inbox {
"Announce" => { "Announce" => {
Reshare::delete_id(act.undo_props.object_object::<Announce>()?.object_props.id_string()?, conn); Reshare::delete_id(act.undo_props.object_object::<Announce>()?.object_props.id_string()?, conn);
Ok(()) Ok(())
},
"Follow" => {
Follow::delete_id(act.undo_props.object_object::<Like>()?.object_props.id_string()?, conn);
Ok(())
} }
_ => Err(InboxError::CantUndo)? _ => Err(InboxError::CantUndo)?
} }

View File

@ -57,7 +57,8 @@ fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>
"date": &post.creation_date.timestamp(), "date": &post.creation_date.timestamp(),
"previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn, &vec![]))), "previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn, &vec![]))),
"user_fqn": user.clone().map(|u| u.get_fqn(&*conn)).unwrap_or(String::new()), "user_fqn": user.clone().map(|u| u.get_fqn(&*conn)).unwrap_or(String::new()),
"is_author": user.map(|u| post.get_authors(&*conn).into_iter().any(|a| u.id == a.id)).unwrap_or(false) "is_author": user.clone().map(|u| post.get_authors(&*conn).into_iter().any(|a| u.id == a.id)).unwrap_or(false),
"is_following": user.map(|u| u.is_following(&*conn, post.get_authors(&*conn)[0].id)).unwrap_or(false)
})) }))
}) })
}) })
@ -98,6 +99,7 @@ fn new(blog: String, user: User, conn: DbConn) -> Template {
struct NewPostForm { struct NewPostForm {
#[validate(custom(function = "valid_slug", message = "Invalid title"))] #[validate(custom(function = "valid_slug", message = "Invalid title"))]
pub title: String, pub title: String,
pub subtitle: String,
pub content: String, pub content: String,
pub license: String pub license: String
} }
@ -150,7 +152,8 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or(String::from("CC-0")) Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or(String::from("CC-0"))
}, },
ap_url: "".to_string(), ap_url: "".to_string(),
creation_date: None creation_date: None,
subtitle: form.subtitle.clone()
}); });
let post = post.update_ap_url(&*conn); let post = post.update_ap_url(&*conn);
PostAuthor::insert(&*conn, NewPostAuthor { PostAuthor::insert(&*conn, NewPostAuthor {

View File

@ -1,5 +1,5 @@
use activitypub::{ use activitypub::{
activity::{Create, Follow}, activity::Create,
collection::OrderedCollection, collection::OrderedCollection,
object::Article object::Article
}; };
@ -16,7 +16,7 @@ use workerpool::thunk::*;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
ActivityStream, broadcast, Id, IntoId, ApRequest, ActivityStream, broadcast, Id, IntoId, ApRequest,
inbox::{FromActivity, Notify} inbox::{FromActivity, Notify, Deletable}
}; };
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
@ -71,7 +71,8 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
.unwrap_or_else(|| User::fetch_from_url(&*fecth_followers_conn, user_id).expect("Couldn't fetch follower")); .unwrap_or_else(|| User::fetch_from_url(&*fecth_followers_conn, user_id).expect("Couldn't fetch follower"));
follows::Follow::insert(&*fecth_followers_conn, follows::NewFollow { follows::Follow::insert(&*fecth_followers_conn, follows::NewFollow {
follower_id: follower.id, follower_id: follower.id,
following_id: user_clone.id following_id: user_clone.id,
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
}); });
} }
})); }));
@ -116,20 +117,20 @@ fn dashboard_auth() -> Flash<Redirect> {
#[get("/@/<name>/follow")] #[get("/@/<name>/follow")]
fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Redirect { fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Redirect {
let target = User::find_by_fqn(&*conn, name.clone()).unwrap(); let target = User::find_by_fqn(&*conn, name.clone()).unwrap();
let f = follows::Follow::insert(&*conn, follows::NewFollow { if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
follower_id: user.id, let delete_act = follow.delete(&*conn);
following_id: target.id worker.execute(Thunk::of(move || broadcast(&user, delete_act, vec![target])));
}); } else {
f.notify(&*conn); let f = follows::Follow::insert(&*conn, follows::NewFollow {
follower_id: user.id,
following_id: target.id,
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
});
f.notify(&*conn);
let mut act = Follow::default(); let act = f.into_activity(&*conn);
act.follow_props.set_actor_link::<Id>(user.clone().into_id()).unwrap(); worker.execute(Thunk::of(move || broadcast(&user, act, vec![target])));
act.follow_props.set_object_object(user.into_activity(&*conn)).unwrap(); }
act.object_props.set_id_string(format!("{}/follow/{}", user.ap_url, target.ap_url)).unwrap();
act.object_props.set_to_link(target.clone().into_id()).expect("New Follow error while setting 'to'");
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("New Follow error while setting 'cc'");
worker.execute(Thunk::of(move || broadcast(&user, act, vec![target])));
Redirect::to(uri!(details: name = name)) Redirect::to(uri!(details: name = name))
} }

View File

@ -192,9 +192,18 @@
} }
article img { article img {
display: block;
margin: 3em auto;
max-width: 100%; max-width: 100%;
} }
article pre {
padding: 1em;
background: #DADADA;
overflow: auto;
border-radius: 5px;
}
/* Article.Meta */ /* Article.Meta */
main .article-meta, main .article-meta button { main .article-meta, main .article-meta button {
@ -235,8 +244,8 @@
font-size: 1.5em; font-size: 1.5em;
} }
main .article-meta .likes button, main .article-meta .likes .action,
main .article-meta .reshares button { main .article-meta .reshares .action {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -246,15 +255,16 @@
background: none; background: none;
color: #242424; color: #242424;
border: none; border: none;
font-size: 1.1em;
} }
main .article-meta .likes > p, main .article-meta .likes > p,
main .article-meta .likes button:hover { color: #E92F2F; } main .article-meta .likes .action:hover { color: #E92F2F; }
main .article-meta .reshares > p, main .article-meta .reshares > p,
main .article-meta .reshares button:hover { color: #7765E3; } main .article-meta .reshares .action:hover { color: #7765E3; }
main .article-meta .likes button svg.feather, main .article-meta .likes .action svg.feather,
main .article-meta .reshares button i { main .article-meta .reshares .action i {
transition: background 0.1s ease-in; transition: background 0.1s ease-in;
display: flex; display: flex;
align-items: center; align-items: center;
@ -267,39 +277,39 @@
border-radius: 50%; border-radius: 50%;
} }
main .article-meta .likes button svg.feather { main .article-meta .likes .action svg.feather {
padding: 0.7em; padding: 0.7em;
box-sizing: border-box; box-sizing: border-box;
color: #E92F2F; color: #E92F2F;
fill: none; fill: none;
border: solid #E92F2F thin; border: solid #E92F2F thin;
} }
main .article-meta .likes button:hover svg.feather { main .article-meta .likes .action:hover svg.feather {
background: rgba(233, 47, 47, 0.15); background: rgba(233, 47, 47, 0.15);
} }
main .article-meta .reshares button i { main .article-meta .reshares .action i {
color: #7765E3; color: #7765E3;
border: solid #7765E3 thin; border: solid #7765E3 thin;
font-weight: 600; font-weight: 600;
} }
main .article-meta .reshares button:hover i { main .article-meta .reshares .action:hover i {
background: rgba(119, 101, 227, 0.15); background: rgba(119, 101, 227, 0.15);
} }
main .article-meta .likes button.liked svg.feather { background: #E92F2F; fill: currentColor; } main .article-meta .likes .action.liked svg.feather { background: #E92F2F; fill: currentColor; }
main .article-meta .likes button.liked:hover svg.feather { main .article-meta .likes .action.liked:hover svg.feather {
background: rgba(233, 47, 47, 0.25); background: rgba(233, 47, 47, 0.25);
color: #E92F2F; color: #E92F2F;
} }
main .article-meta .reshares button.reshared i { background: #7765E3; } main .article-meta .reshares .action.reshared i { background: #7765E3; }
main .article-meta .reshares button.reshared:hover i { main .article-meta .reshares .action.reshared:hover i {
background: rgba(119, 101, 227, 0.25); background: rgba(119, 101, 227, 0.25);
color: #7765E3; color: #7765E3;
} }
main .article-meta .likes button.liked svg.feather, main .article-meta .likes .action.liked svg.feather,
main .article-meta .reshares button.reshared i { main .article-meta .reshares .action.reshared i {
color: #F4F4F4; color: #F4F4F4;
font-weight: 900; font-weight: 900;
} }
@ -796,5 +806,5 @@
} }
.avatar.padded { .avatar.padded {
margin-right: 1em; margin-right: 2rem;
} }

View File

@ -30,7 +30,7 @@
<span class="mobile-label">{{ "Notifications" | _ }}</span> <span class="mobile-label">{{ "Notifications" | _ }}</span>
</a> </a>
<a href="/logout"> <a href="/logout">
<i class="icon icon-log-out aria-label="{{ "Log Out" | _ }}"></i> <i class="icon icon-log-out" aria-label="{{ "Log Out" | _ }}"></i>
<span class="mobile-label">{{ "Log Out" | _ }}</span> <span class="mobile-label">{{ "Log Out" | _ }}</span>
</a> </a>
<a href="/me"> <a href="/me">

View File

@ -2,7 +2,13 @@
<div class="card"> <div class="card">
<h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3> <h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3>
<main <main
<p>{{ article.post.content | safe | striptags | truncate(length=200) }}</p> <p>
{% if article.post.subtitle | length > 0 %}
{{ article.post.subtitle }}
{% else %}
{{ article.post.content | safe | striptags | truncate(length=200) }}
{% endif %}
</p>
</main> </main>
<p class="author"> <p class="author">
{{ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" | _( {{ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name | escape }}{{ link_4 }}" | _(

View File

@ -11,6 +11,7 @@
{% block content %} {% block content %}
<h1 class="article">{{ article.post.title }}</h1> <h1 class="article">{{ article.post.title }}</h1>
<h2 class="article">{{ article.post.subtitle }}</h2>
<p class="article-info"> <p class="article-info">
<span class="author">{{ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" | _( <span class="author">{{ "Written by {{ link_1 }}{{ url }}{{ link_2 }}{{ name | escape }}{{ link_3 }}" | _(
link_1='<a href="/@/', link_1='<a href="/@/',
@ -33,6 +34,20 @@
<div class="article-meta"> <div class="article-meta">
<p>{{ "This article is under the {{ license }} license." | _(license=article.post.license) }}</p> <p>{{ "This article is under the {{ license }} license." | _(license=article.post.license) }}</p>
<div class="flex">
<img src="{{ author.avatar }}" alt="{{ author.name }}" class="avatar medium padded">
<div class="grow">
<h2><a href="/@/{{ author.fqn }}">{{ author.name }}</a></h2>
<p>{{ author.summary | safe }}</h2>
</div>
<a href="/@/{{ author.fqn }}/follow" class="button">
{% if is_following %}
{{ "Unfollow" | _ }}
{% else %}
{{ "Follow" | _ }}
{% endif %}
</a>
</div>
{% if account %} {% if account %}
<div class="actions"> <div class="actions">
@ -40,23 +55,34 @@
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p> <p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
{% if has_liked %} {% if has_liked %}
<button type="submit" class="liked">{{ macros::feather(name="heart") }}{{ "I don't like this anymore" | _ }}</button> <button type="submit" class="action liked">{{ macros::feather(name="heart") }}{{ "I don't like this anymore" | _ }}</button>
{% else %} {% else %}
<button type="submit">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</button> <button type="submit" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</button>
{% endif %} {% endif %}
</form> </form>
<form class="reshares" action="{{ article.url }}reshare" method="POST"> <form class="reshares" action="{{ article.url }}reshare" method="POST">
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p> <p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
{% if has_reshared %} {% if has_reshared %}
<button type="submit" class="reshared"><i class="icon icon-repeat"></i>{{ "I don't want to boost this anymore" | _ }}</button> <button type="submit" class="action reshared"><i class="icon icon-repeat"></i>{{ "I don't want to boost this anymore" | _ }}</button>
{% else %} {% else %}
<button type="submit"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</button> <button type="submit" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</button>
{% endif %} {% endif %}
</form> </form>
</div> </div>
{% else %} {% else %}
<p class="center">{{ "Login or use your Fediverse account to interact with this article" | _ }}</p> <p class="center">{{ "Login or use your Fediverse account to interact with this article" | _ }}</p>
<div class="actions">
<div class="likes">
<p aria-label="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}" title="{{ "{{ count }} likes" | _n(singular="One like", count=n_likes) }}">{{ n_likes }}</p>
<a href="/login?m=Login%20to%20like" class="action">{{ macros::feather(name="heart") }}{{ "Add yours" | _ }}</a>
</div>
<div class="reshares">
<p aria-label="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}" title="{{ "{{ count }} Boosts" | _n(singular="One Boost", count=n_reshares) }}">{{ n_reshares }}</p>
<a href="/login?m=Login%20to%20boost" class="action"><i class="icon icon-repeat"></i>{{ "Boost" | _ }}</a>
</div>
</div>
{% endif %} {% endif %}
<div class="comments"> <div class="comments">

View File

@ -9,6 +9,7 @@
<h1>{{ "Create a post" | _ }}</h1> <h1>{{ "Create a post" | _ }}</h1>
<form class="new-post" method="post"> <form class="new-post" method="post">
{{ macros::input(name="title", label="Title", errors=errors, form=form, props="required") }} {{ macros::input(name="title", label="Title", errors=errors, form=form, props="required") }}
{{ macros::input(name="subtitle", label="Subtitle", errors=errors, form=form, optional=true) }}
{% if errors is defined and errors.content %} {% if errors is defined and errors.content %}
{% for err in errors.content %} {% for err in errors.content %}