This commit is contained in:
Didier Link 2018-06-22 00:00:15 +02:00
commit feff837313
35 changed files with 532 additions and 488 deletions

2
Cargo.lock generated
View File

@ -949,6 +949,8 @@ name = "plume"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"activitypub 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "activitypub 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"activitystreams-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"activitystreams-traits 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"array_tool 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "array_tool 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -4,6 +4,8 @@ name = "plume"
version = "0.1.0" version = "0.1.0"
[dependencies] [dependencies]
activitypub = "0.1.1" activitypub = "0.1.1"
activitystreams-derive = "0.1.0"
activitystreams-traits = "0.1.0"
ammonia = "1.1.0" ammonia = "1.1.0"
array_tool = "1.0" array_tool = "1.0"
base64 = "0.9" base64 = "0.9"
@ -18,7 +20,6 @@ hex = "0.3"
hyper = "*" hyper = "*"
lazy_static = "*" lazy_static = "*"
openssl = "0.10.6" openssl = "0.10.6"
pulldown-cmark = { version = "0.1.2", default-features = false }
reqwest = "0.8" reqwest = "0.8"
rpassword = "2.0" rpassword = "2.0"
serde = "*" serde = "*"
@ -36,6 +37,10 @@ version = "0.4"
features = ["postgres", "r2d2", "chrono"] features = ["postgres", "r2d2", "chrono"]
version = "*" version = "*"
[dependencies.pulldown-cmark]
default-features = false
version = "0.1.2"
[dependencies.rocket] [dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket" git = "https://github.com/SergioBenitez/Rocket"
rev = "df7111143e466c18d1f56377a8d9530a5a306aba" rev = "df7111143e466c18d1f56377a8d9530a5a306aba"

View File

@ -6,6 +6,10 @@
All commands are run in the Mac Terminal or terminal emulator of your choice, such as iTerm2. First, you will need [Git](https://git-scm.com/download/mac), [Homebrew](https://brew.sh/), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Follow the instructions to install Homebrew before continuing if you don't already have it. All commands are run in the Mac Terminal or terminal emulator of your choice, such as iTerm2. First, you will need [Git](https://git-scm.com/download/mac), [Homebrew](https://brew.sh/), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Follow the instructions to install Homebrew before continuing if you don't already have it.
### Linux
Similar to Mac OSX all commands should be run from a terminal (a.k.a command line). First, you will need [Git](https://git-scm.com/download/mac), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Step-by-step instructions are also available here: [Installing Prerequisites](/doc/PREREQUISITES.md)
#### Download the Repository #### Download the Repository
Navigate to the directory on your machine where you would like to install the repository, such as in `~/dev` by running `cd dev`. Now, clone the remote repository by running `git clone https://github.com/Plume-org/Plume.git`. This will install the codebase to the `Plume` subdirectory. Navigate into that directory by running `cd Plume`. Navigate to the directory on your machine where you would like to install the repository, such as in `~/dev` by running `cd dev`. Now, clone the remote repository by running `git clone https://github.com/Plume-org/Plume.git`. This will install the codebase to the `Plume` subdirectory. Navigate into that directory by running `cd Plume`.

86
doc/PREREQUISITES.md Normal file
View File

@ -0,0 +1,86 @@
# Installing Software Prerequisites
These instructions have been adapted from the Aardwolf documentation, and may not be accurate.
As such, this notification should be updated once verified for Plume installs.
> NOTE: These instructions may help in installing a production version, but are
intended for developers to be able to build and test their changes. If in doubt,
seek out documentation from your distribution package or from [the `doc` folder](doc).
## Installing Requirements
### Installing PostgreSQL
In order to run the Plume backend, you will need to have access to a
[PostgreSQL](https://www.postgresql.org/) database. There are a few options for doing this, but for
this guide were going to assume you are running the database on your
development machine.
#### Linux/OSX Instructions
If you're on an Ubuntu-like machine, you should be able to install
PostgreSQL like this:
$ sudo apt-get update
$ sudo apt-get install postgresql postgresql-contrib
If you see an error like:
= note: /usr/bin/ld: cannot find -lpq
collect2: error: ld returned 1 exit statusb
Then you may need to install the libpq (PostgreSQL C-library) package as well :
$ sudo apt-get install libpq-dev
If you're on OSX and using `brew`, do
$ brew update
$ brew install postgres
For Gentoo (eselect-postgresql is optional),
# emerge --sync
# emerge -av postgresql eselect-postgresql
For Fedora/CentOS/RHEL, do
# dnf install postgresql-server postgresql-contrib
#### Windows Instructions
For Windows, just download the installer [here](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads#windows) and run it. After installing, make sure to add the <POSTGRES INSTALL PATH>/lib directory to your PATH system variable.
### Installing rustup
> Note: Rustup managed installations do appear to co-exist with system
installations on Gentoo, and should work on most other distributions.
If not, please file an issue with the Rust and Rustup teams or your distributions
managers.
Next, youll need to have the [Rust](https://rust-lang.org/) toolchain
installed. The best way to do this is to install
[rustup](https://rustup.rs), which is a Rust toolchain manager.
#### Linux/OSX Instructions
Open your terminal and run the following command:
$ curl https://sh.rustup.rs -sSf | sh
For those who are (understandably) uncomfortable with piping a shell
script from the internet directly into `sh`, you can also
[use an alternate installation method](https://github.com/rust-lang-nursery/rustup.rs/#other-installation-methods).
#### Windows Instructions
If you don't already have them, download and install the [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools).
Then, download the [rustup installer](https://www.rust-lang.org/en-US/install.html) and run it. That's it!
### Installing Rust Toolchain
Once you have `rustup` installed, make sure you have the `nightly` rust
toolchain installed:
$ rustup toolchain install nightly

View File

@ -283,3 +283,6 @@ msgstr ""
msgid "{{ data }} mentioned you." msgid "{{ data }} mentioned you."
msgstr "" msgstr ""
msgid "Your comment"
msgstr ""

View File

@ -282,3 +282,7 @@ msgstr "Vous n'êtes pas auteur dans ce blog."
msgid "{{ data }} mentioned you." msgid "{{ data }} mentioned you."
msgstr "" msgstr ""
#, fuzzy
msgid "Your comment"
msgstr "Envoyer le commentaire"

View File

@ -289,5 +289,9 @@ msgstr ""
msgid "{{ data }} mentioned you." msgid "{{ data }} mentioned you."
msgstr "{{ data }} skomentował Twój artykuł" msgstr "{{ data }} skomentował Twój artykuł"
#, fuzzy
msgid "Your comment"
msgstr "Wyślij komentarz"
#~ msgid "Logowanie" #~ msgid "Logowanie"
#~ msgstr "Zaloguj się" #~ msgstr "Zaloguj się"

View File

@ -278,3 +278,6 @@ msgstr ""
msgid "{{ data }} mentioned you." msgid "{{ data }} mentioned you."
msgstr "" msgstr ""
msgid "Your comment"
msgstr ""

View File

@ -1,86 +0,0 @@
use diesel::PgConnection;
use serde_json;
use BASE_URL;
use activity_pub::{activity_pub, ActivityPub, context, ap_url};
use models::instance::Instance;
pub enum ActorType {
Person,
Blog
}
impl ToString for ActorType {
fn to_string(&self) -> String {
String::from(match self {
ActorType::Person => "Person",
ActorType::Blog => "Blog"
})
}
}
pub trait Actor: Sized {
fn get_box_prefix() -> &'static str;
fn get_actor_id(&self) -> String;
fn get_display_name(&self) -> String;
fn get_summary(&self) -> String;
fn get_instance(&self, conn: &PgConnection) -> Instance;
fn get_actor_type() -> ActorType;
fn get_inbox_url(&self) -> String;
fn get_shared_inbox_url(&self) -> Option<String>;
fn custom_props(&self, _conn: &PgConnection) -> serde_json::Map<String, serde_json::Value> {
serde_json::Map::new()
}
fn as_activity_pub (&self, conn: &PgConnection) -> ActivityPub {
let mut repr = json!({
"@context": context(),
"id": self.compute_id(conn),
"type": Self::get_actor_type().to_string(),
"inbox": self.compute_inbox(conn),
"outbox": self.compute_outbox(conn),
"preferredUsername": self.get_actor_id(),
"name": self.get_display_name(),
"summary": self.get_summary(),
"url": self.compute_id(conn),
"endpoints": {
"sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str()))
}
});
self.custom_props(conn).iter().for_each(|p| repr[p.0] = p.1.clone());
activity_pub(repr)
}
fn compute_outbox(&self, conn: &PgConnection) -> String {
self.compute_box(conn, "outbox")
}
fn compute_inbox(&self, conn: &PgConnection) -> String {
self.compute_box(conn, "inbox")
}
fn compute_box(&self, conn: &PgConnection, box_name: &str) -> String {
format!("{id}/{name}", id = self.compute_id(conn), name = box_name)
}
fn compute_id(&self, conn: &PgConnection) -> String {
ap_url(format!(
"{instance}/{prefix}/{user}",
instance = self.get_instance(conn).public_domain,
prefix = Self::get_box_prefix(),
user = self.get_actor_id()
))
}
fn from_url(conn: &PgConnection, url: String) -> Option<Self>;
}

View File

@ -50,15 +50,7 @@ pub trait Deletable {
} }
pub trait Inbox { pub trait Inbox {
fn received(&self, conn: &PgConnection, act: serde_json::Value); fn received(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> {
fn unlike(&self, conn: &PgConnection, undo: Undo) -> Result<(), Error> {
let like = likes::Like::find_by_ap_url(conn, undo.undo_props.object_object::<Like>()?.object_props.id_string()?).unwrap();
like.delete(conn);
Ok(())
}
fn save(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> {
let actor_id = Id::new(act["actor"].as_str().unwrap()); let actor_id = Id::new(act["actor"].as_str().unwrap());
match act["type"].as_str() { match act["type"].as_str() {
Some(t) => { Some(t) => {

View File

@ -1,24 +1,19 @@
use activitypub::{Activity, Actor, Object, Link}; use activitypub::{Activity, Actor, Object, Link};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use diesel::PgConnection;
use reqwest::Client; use reqwest::Client;
use rocket::{ use rocket::{
http::{ContentType, Status}, http::Status,
response::{Response, Responder, Content}, response::{Response, Responder},
request::Request request::Request
}; };
use rocket_contrib::Json;
use serde_json; use serde_json;
use self::sign::Signable; use self::sign::Signable;
pub mod actor;
pub mod inbox; pub mod inbox;
pub mod request; pub mod request;
pub mod sign; pub mod sign;
pub type ActivityPub = Content<Json<serde_json::Value>>;
pub const CONTEXT_URL: &'static str = "https://www.w3.org/ns/activitystreams"; pub const CONTEXT_URL: &'static str = "https://www.w3.org/ns/activitystreams";
pub const PUBLIC_VISIBILTY: &'static str = "https://www.w3.org/ns/activitystreams#Public"; pub const PUBLIC_VISIBILTY: &'static str = "https://www.w3.org/ns/activitystreams#Public";
@ -56,10 +51,6 @@ pub fn context() -> serde_json::Value {
]) ])
} }
pub fn activity_pub(json: serde_json::Value) -> ActivityPub {
Content(ContentType::new("application", "activity+json"), Json(json))
}
pub struct ActivityStream<T> (T); pub struct ActivityStream<T> (T);
impl<T> ActivityStream<T> { impl<T> ActivityStream<T> {
@ -70,11 +61,15 @@ impl<T> ActivityStream<T> {
impl<'r, O: Object> Responder<'r> for ActivityStream<O> { impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> { fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
serde_json::to_string(&self.0).respond_to(request) let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context();
serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r)
.raw_header("Content-Type", "application/activity+json")
.finalize())
} }
} }
pub fn broadcast<A: Activity, S: sign::Signer, T: inbox::WithInbox + Actor>(conn: &PgConnection, sender: &S, act: A, to: Vec<T>) { pub fn broadcast<A: Activity, S: sign::Signer, T: inbox::WithInbox + Actor>(sender: &S, act: A, to: Vec<T>) {
let boxes = to.into_iter() let boxes = to.into_iter()
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url())) .map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
.collect::<Vec<String>>() .collect::<Vec<String>>()
@ -82,14 +77,14 @@ pub fn broadcast<A: Activity, S: sign::Signer, T: inbox::WithInbox + Actor>(conn
let mut act = serde_json::to_value(act).unwrap(); let mut act = serde_json::to_value(act).unwrap();
act["@context"] = context(); act["@context"] = context();
let signed = act.sign(sender, conn); let signed = act.sign(sender);
for inbox in boxes { for inbox in boxes {
// TODO: run it in Sidekiq or something like that // TODO: run it in Sidekiq or something like that
let res = Client::new() let res = Client::new()
.post(&inbox[..]) .post(&inbox[..])
.headers(request::headers()) .headers(request::headers())
.header(request::signature(sender, request::headers(), conn)) .header(request::signature(sender, request::headers()))
.header(request::digest(signed.to_string())) .header(request::digest(signed.to_string()))
.body(signed.to_string()) .body(signed.to_string())
.send(); .send();
@ -120,3 +115,27 @@ pub trait IntoId {
} }
impl Link for Id {} impl Link for Id {}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")]
pub struct ApSignature {
#[serde(skip_serializing_if = "Option::is_none")]
#[activitystreams(concrete(PublicKey), functional)]
pub public_key: Option<serde_json::Value>
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
#[serde(skip_serializing_if = "Option::is_none")]
#[activitystreams(concrete(String), functional)]
pub id: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
#[activitystreams(concrete(String), functional)]
pub owner: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
#[activitystreams(concrete(String), functional)]
pub public_key_pem: Option<serde_json::Value>
}

View File

@ -1,5 +1,4 @@
use base64; use base64;
use diesel::PgConnection;
use openssl::hash::{Hasher, MessageDigest}; use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{Date, Headers, UserAgent}; use reqwest::header::{Date, Headers, UserAgent};
use std::time::SystemTime; use std::time::SystemTime;
@ -23,7 +22,7 @@ pub fn headers() -> Headers {
headers headers
} }
pub fn signature<S: Signer>(signer: &S, headers: Headers, conn: &PgConnection) -> Signature { pub fn signature<S: Signer>(signer: &S, headers: Headers) -> Signature {
let signed_string = headers.iter().map(|h| format!("{}: {}", h.name().to_lowercase(), h.value_string())).collect::<Vec<String>>().join("\n"); let signed_string = headers.iter().map(|h| format!("{}: {}", h.name().to_lowercase(), h.value_string())).collect::<Vec<String>>().join("\n");
let signed_headers = headers.iter().map(|h| h.name().to_string()).collect::<Vec<String>>().join(" ").to_lowercase(); let signed_headers = headers.iter().map(|h| h.name().to_string()).collect::<Vec<String>>().join(" ").to_lowercase();
@ -32,7 +31,7 @@ pub fn signature<S: Signer>(signer: &S, headers: Headers, conn: &PgConnection) -
Signature(format!( Signature(format!(
"keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{signed_headers}\",signature=\"{signature}\"", "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{signed_headers}\",signature=\"{signature}\"",
key_id = signer.get_key_id(conn), key_id = signer.get_key_id(),
signed_headers = signed_headers, signed_headers = signed_headers,
signature = sign signature = sign
)) ))

View File

@ -1,6 +1,5 @@
use base64; use base64;
use chrono::Utc; use chrono::Utc;
use diesel::PgConnection;
use hex; use hex;
use openssl::{ use openssl::{
pkey::PKey, pkey::PKey,
@ -17,14 +16,14 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
} }
pub trait Signer { pub trait Signer {
fn get_key_id(&self, conn: &PgConnection) -> String; fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair /// Sign some data with the signer keypair
fn sign(&self, to_sign: String) -> Vec<u8>; fn sign(&self, to_sign: String) -> Vec<u8>;
} }
pub trait Signable { pub trait Signable {
fn sign<T>(&mut self, creator: &T, conn: &PgConnection) -> &mut Self where T: Signer; fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer;
fn hash(data: String) -> String { fn hash(data: String) -> String {
let bytes = data.into_bytes(); let bytes = data.into_bytes();
@ -33,11 +32,11 @@ pub trait Signable {
} }
impl Signable for serde_json::Value { impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T, conn: &PgConnection) -> &mut serde_json::Value { fn sign<T: Signer>(&mut self, creator: &T) -> &mut serde_json::Value {
let creation_date = Utc::now().to_rfc3339(); let creation_date = Utc::now().to_rfc3339();
let mut options = json!({ let mut options = json!({
"type": "RsaSignature2017", "type": "RsaSignature2017",
"creator": creator.get_key_id(conn), "creator": creator.get_key_id(),
"created": creation_date "created": creation_date
}); });

View File

@ -2,6 +2,9 @@
#![plugin(rocket_codegen)] #![plugin(rocket_codegen)]
extern crate activitypub; extern crate activitypub;
#[macro_use]
extern crate activitystreams_derive;
extern crate activitystreams_traits;
extern crate ammonia; extern crate ammonia;
extern crate array_tool; extern crate array_tool;
extern crate base64; extern crate base64;
@ -68,9 +71,8 @@ fn main() {
routes::blogs::new_auth, routes::blogs::new_auth,
routes::blogs::create, routes::blogs::create,
routes::comments::new,
routes::comments::new_auth,
routes::comments::create, routes::comments::create,
routes::comments::create_response,
routes::instance::index, routes::instance::index,
routes::instance::shared_inbox, routes::instance::shared_inbox,
@ -83,6 +85,7 @@ fn main() {
routes::notifications::notifications_auth, routes::notifications::notifications_auth,
routes::posts::details, routes::posts::details,
routes::posts::details_response,
routes::posts::activity_details, routes::posts::activity_details,
routes::posts::new, routes::posts::new,
routes::posts::new_auth, routes::posts::new_auth,

View File

@ -1,4 +1,4 @@
use activitypub::{Actor, Object, collection::OrderedCollection}; use activitypub::{Actor, Object, CustomObject, actor::Group, collection::OrderedCollection};
use reqwest::{ use reqwest::{
Client, Client,
header::{Accept, qitem}, header::{Accept, qitem},
@ -16,15 +16,16 @@ use openssl::{
}; };
use webfinger::*; use webfinger::*;
use BASE_URL;
use activity_pub::{ use activity_pub::{
ActivityStream, Id, IntoId, ApSignature, ActivityStream, Id, IntoId, PublicKey,
actor::{Actor as APActor, ActorType},
inbox::WithInbox, inbox::WithInbox,
sign sign
}; };
use models::instance::*; use models::instance::*;
use schema::blogs; use schema::blogs;
pub type CustomGroup = CustomObject<ApSignature, Group>;
#[derive(Queryable, Identifiable, Serialize, Deserialize, Clone)] #[derive(Queryable, Identifiable, Serialize, Deserialize, Clone)]
pub struct Blog { pub struct Blog {
@ -55,9 +56,17 @@ pub struct NewBlog {
pub public_key: String pub public_key: String
} }
const BLOG_PREFIX: &'static str = "~";
impl Blog { impl Blog {
insert!(blogs, NewBlog); insert!(blogs, NewBlog);
get!(blogs); get!(blogs);
find_by!(blogs, find_by_ap_url, ap_url as String);
find_by!(blogs, find_by_name, actor_id as String, instance_id as i32);
pub fn get_instance(&self, conn: &PgConnection) -> Instance {
Instance::get(conn, self.instance_id).expect("Couldn't find instance")
}
pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec<Blog> { pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec<Blog> {
use schema::blog_authors; use schema::blog_authors;
@ -67,8 +76,6 @@ impl Blog {
.expect("Couldn't load blogs ") .expect("Couldn't load blogs ")
} }
find_by!(blogs, find_by_name, actor_id as String, instance_id as i32);
pub fn find_local(conn: &PgConnection, name: String) -> Option<Blog> { pub fn find_local(conn: &PgConnection, name: String) -> Option<Blog> {
Blog::find_by_name(conn, name, Instance::local_id(conn)) Blog::find_by_name(conn, name, Instance::local_id(conn))
} }
@ -106,14 +113,14 @@ impl Blog {
.send(); .send();
match req { match req {
Ok(mut res) => { Ok(mut res) => {
let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); let json = serde_json::from_str(&res.text().unwrap()).unwrap();
Some(Blog::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string())) Some(Blog::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string()))
}, },
Err(_) => None Err(_) => None
} }
} }
fn from_activity(conn: &PgConnection, acct: serde_json::Value, inst: String) -> Blog { fn from_activity(conn: &PgConnection, acct: CustomGroup, inst: String) -> Blog {
let instance = match Instance::find_by_domain(conn, inst.clone()) { let instance = match Instance::find_by_domain(conn, inst.clone()) {
Some(instance) => instance, Some(instance) => instance,
None => { None => {
@ -125,34 +132,55 @@ impl Blog {
} }
}; };
Blog::insert(conn, NewBlog { Blog::insert(conn, NewBlog {
actor_id: acct["preferredUsername"].as_str().unwrap().to_string(), actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"),
title: acct["name"].as_str().unwrap().to_string(), title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"),
outbox_url: acct["outbox"].as_str().unwrap().to_string(), outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"),
inbox_url: acct["inbox"].as_str().unwrap().to_string(), inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"),
summary: acct["summary"].as_str().unwrap().to_string(), summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"),
instance_id: instance.id, instance_id: instance.id,
ap_url: acct["id"].as_str().unwrap().to_string(), ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"),
public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap_or("").to_string(), public_key: acct.custom_props.public_key_publickey().expect("Blog::from_activity: publicKey error")
.public_key_pem_string().expect("Blog::from_activity: publicKey.publicKeyPem error"),
private_key: None private_key: None
}) })
} }
pub fn into_activity(&self, _conn: &PgConnection) -> CustomGroup {
let mut blog = Group::default();
blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error");
blog.object_props.set_name_string(self.title.clone()).expect("Blog::into_activity: name error");
blog.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("Blog::into_activity: outbox error");
blog.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("Blog::into_activity: inbox error");
blog.object_props.set_summary_string(self.summary.clone()).expect("Blog::into_activity: summary error");
blog.object_props.set_id_string(self.ap_url.clone()).expect("Blog::into_activity: id error");
let mut public_key = PublicKey::default();
public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error");
public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error");
public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error");
let mut ap_signature = ApSignature::default();
ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error");
CustomGroup::new(blog, ap_signature)
}
pub fn update_boxes(&self, conn: &PgConnection) { pub fn update_boxes(&self, conn: &PgConnection) {
let instance = self.get_instance(conn);
if self.outbox_url.len() == 0 { if self.outbox_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(blogs::outbox_url.eq(self.compute_outbox(conn))) .set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox")))
.get_result::<Blog>(conn).expect("Couldn't update outbox URL"); .get_result::<Blog>(conn).expect("Couldn't update outbox URL");
} }
if self.inbox_url.len() == 0 { if self.inbox_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(blogs::inbox_url.eq(self.compute_inbox(conn))) .set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox")))
.get_result::<Blog>(conn).expect("Couldn't update inbox URL"); .get_result::<Blog>(conn).expect("Couldn't update inbox URL");
} }
if self.ap_url.len() == 0 { if self.ap_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(blogs::ap_url.eq(self.compute_id(conn))) .set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "")))
.get_result::<Blog>(conn).expect("Couldn't update AP URL"); .get_result::<Blog>(conn).expect("Couldn't update AP URL");
} }
} }
@ -175,26 +203,38 @@ impl Blog {
pub fn webfinger(&self, conn: &PgConnection) -> Webfinger { pub fn webfinger(&self, conn: &PgConnection) -> Webfinger {
Webfinger { Webfinger {
subject: format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain), subject: format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain),
aliases: vec![self.compute_id(conn)], aliases: vec![self.ap_url.clone()],
links: vec![ links: vec![
Link { Link {
rel: String::from("http://webfinger.net/rel/profile-page"), rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None, mime_type: None,
href: self.compute_id(conn) href: self.ap_url.clone()
}, },
Link { Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"), rel: String::from("http://schemas.google.com/g/2010#updates-from"),
mime_type: Some(String::from("application/atom+xml")), mime_type: Some(String::from("application/atom+xml")),
href: self.compute_box(conn, "feed.atom") href: self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")
}, },
Link { Link {
rel: String::from("self"), rel: String::from("self"),
mime_type: Some(String::from("application/activity+json")), mime_type: Some(String::from("application/activity+json")),
href: self.compute_id(conn) href: self.ap_url.clone()
} }
] ]
} }
} }
pub fn from_url(conn: &PgConnection, url: String) -> Option<Blog> {
Blog::find_by_ap_url(conn, url.clone()).or_else(|| {
// The requested user was not in the DB
// We try to fetch it if it is remote
if Url::parse(url.as_ref()).unwrap().host_str().unwrap() != BASE_URL.as_str() {
Some(Blog::fetch_from_url(conn, url).unwrap())
} else {
None
}
})
}
} }
impl IntoId for Blog { impl IntoId for Blog {
@ -216,51 +256,9 @@ impl WithInbox for Blog {
} }
} }
impl APActor for Blog {
fn get_box_prefix() -> &'static str {
"~"
}
fn get_actor_id(&self) -> String {
self.actor_id.to_string()
}
fn get_display_name(&self) -> String {
self.title.clone()
}
fn get_summary(&self) -> String {
self.summary.clone()
}
fn get_instance(&self, conn: &PgConnection) -> Instance {
Instance::get(conn, self.instance_id).unwrap()
}
fn get_actor_type () -> ActorType {
ActorType::Blog
}
fn get_inbox_url(&self) -> String {
self.inbox_url.clone()
}
fn get_shared_inbox_url(&self) -> Option<String> {
None
}
fn from_url(conn: &PgConnection, url: String) -> Option<Blog> {
blogs::table.filter(blogs::ap_url.eq(url))
.limit(1)
.load::<Blog>(conn)
.expect("Error loading blog from url")
.into_iter().nth(0)
}
}
impl sign::Signer for Blog { impl sign::Signer for Blog {
fn get_key_id(&self, conn: &PgConnection) -> String { fn get_key_id(&self) -> String {
format!("{}#main-key", self.compute_id(conn)) format!("{}#main-key", self.ap_url)
} }
fn sign(&self, to_sign: String) -> Vec<u8> { fn sign(&self, to_sign: String) -> Vec<u8> {

View File

@ -1,7 +1,7 @@
use activitypub::{ use activitypub::{
activity::Create, activity::Create,
link, link,
object::{Note, properties::ObjectProperties} object::{Note}
}; };
use chrono; use chrono;
use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl::any}; use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl::any};
@ -9,10 +9,10 @@ use serde_json;
use activity_pub::{ use activity_pub::{
ap_url, Id, IntoId, PUBLIC_VISIBILTY, ap_url, Id, IntoId, PUBLIC_VISIBILTY,
actor::Actor,
inbox::{FromActivity, Notify} inbox::{FromActivity, Notify}
}; };
use models::{ use models::{
get_next_id,
instance::Instance, instance::Instance,
mentions::Mention, mentions::Mention,
notifications::*, notifications::*,
@ -21,6 +21,7 @@ use models::{
}; };
use schema::comments; use schema::comments;
use safe_string::SafeString; use safe_string::SafeString;
use utils;
#[derive(Queryable, Identifiable, Serialize, Clone)] #[derive(Queryable, Identifiable, Serialize, Clone)]
pub struct Comment { pub struct Comment {
@ -35,7 +36,7 @@ pub struct Comment {
pub spoiler_text: String pub spoiler_text: String
} }
#[derive(Insertable)] #[derive(Insertable, Default)]
#[table_name = "comments"] #[table_name = "comments"]
pub struct NewComment { pub struct NewComment {
pub content: SafeString, pub content: SafeString,
@ -50,7 +51,7 @@ pub struct NewComment {
impl Comment { impl Comment {
insert!(comments, NewComment); insert!(comments, NewComment);
get!(comments); get!(comments);
find_by!(comments, find_by_post, post_id as i32); list_by!(comments, list_by_post, post_id as i32);
find_by!(comments, find_by_ap_url, ap_url as String); find_by!(comments, find_by_ap_url, ap_url as String);
pub fn get_author(&self, conn: &PgConnection) -> User { pub fn get_author(&self, conn: &PgConnection) -> User {
@ -61,37 +62,6 @@ impl Comment {
Post::get(conn, self.post_id).unwrap() Post::get(conn, self.post_id).unwrap()
} }
pub fn into_activity(&self, conn: &PgConnection) -> Note {
let mut to = self.get_author(conn).get_followers(conn).into_iter().map(|f| f.ap_url).collect::<Vec<String>>();
to.append(&mut self.get_post(conn).get_receivers_urls(conn));
to.push(PUBLIC_VISIBILTY.to_string());
let mut comment = Note::default();
comment.object_props = ObjectProperties {
id: Some(serde_json::to_value(self.ap_url.clone()).unwrap()),
summary: Some(serde_json::to_value(self.spoiler_text.clone()).unwrap()),
content: Some(serde_json::to_value(self.content.clone()).unwrap()),
in_reply_to: Some(serde_json::to_value(self.in_response_to_id.map_or_else(|| self.get_post(conn).ap_url, |id| {
let comm = Comment::get(conn, id).unwrap();
comm.ap_url.clone().unwrap_or(comm.compute_id(conn))
})).unwrap()),
published: Some(serde_json::to_value(self.creation_date).unwrap()),
attributed_to: Some(serde_json::to_value(self.get_author(conn).compute_id(conn)).unwrap()),
to: Some(serde_json::to_value(to).unwrap()),
cc: Some(serde_json::to_value(Vec::<serde_json::Value>::new()).unwrap()),
..ObjectProperties::default()
};
comment
}
pub fn create_activity(&self, conn: &PgConnection) -> Create {
let mut act = Create::default();
act.create_props.set_actor_link(self.get_author(conn).into_id()).unwrap();
act.create_props.set_object_object(self.into_activity(conn)).unwrap();
act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().unwrap())).unwrap();
act
}
pub fn count_local(conn: &PgConnection) -> usize { pub fn count_local(conn: &PgConnection) -> usize {
use schema::users; use schema::users;
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id); let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id);
@ -104,11 +74,16 @@ impl Comment {
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
let mut json = serde_json::to_value(self).unwrap(); let mut json = serde_json::to_value(self).unwrap();
json["author"] = self.get_author(conn).to_json(conn); json["author"] = self.get_author(conn).to_json(conn);
let mentions = Mention::list_for_comment(conn, self.id).into_iter()
.map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new()))
.collect::<Vec<String>>();
println!("{:?}", mentions);
json["mentions"] = serde_json::to_value(mentions).unwrap();
json json
} }
pub fn compute_id(&self, conn: &PgConnection) -> String { pub fn compute_id(&self, conn: &PgConnection) -> String {
ap_url(format!("{}#comment-{}", self.get_post(conn).compute_id(conn), self.id)) ap_url(format!("{}#comment-{}", self.get_post(conn).ap_url, self.id))
} }
} }
@ -117,15 +92,6 @@ impl FromActivity<Note> for Comment {
let previous_url = note.object_props.in_reply_to.clone().unwrap().as_str().unwrap().to_string(); let previous_url = note.object_props.in_reply_to.clone().unwrap().as_str().unwrap().to_string();
let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone()); let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone());
// save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag)
.map(|m| Mention::from_activity(conn, m, Id::new(note.clone().object_props.clone().url_string().unwrap_or(String::from("")))))
.ok();
}
}
let comm = Comment::insert(conn, NewComment { let comm = Comment::insert(conn, NewComment {
content: SafeString::new(&note.object_props.content_string().unwrap()), content: SafeString::new(&note.object_props.content_string().unwrap()),
spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")), spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")),
@ -137,6 +103,16 @@ impl FromActivity<Note> for Comment {
author_id: User::from_url(conn, actor.clone().into()).unwrap().id, author_id: User::from_url(conn, actor.clone().into()).unwrap().id,
sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
}); });
// save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag)
.map(|m| Mention::from_activity(conn, m, comm.id, false))
.ok();
}
}
comm.notify(conn); comm.notify(conn);
comm comm
} }
@ -155,3 +131,70 @@ impl Notify for Comment {
} }
} }
} }
impl NewComment {
pub fn build() -> Self {
NewComment::default()
}
pub fn content<T: AsRef<str>>(mut self, val: T) -> Self {
self.content = SafeString::new(val.as_ref());
self
}
pub fn in_response_to_id(mut self, val: Option<i32>) -> Self {
self.in_response_to_id = val;
self
}
pub fn post(mut self, post: Post) -> Self {
self.post_id = post.id;
self
}
pub fn author(mut self, author: User) -> Self {
self.author_id = author.id;
self
}
pub fn create(mut self, conn: &PgConnection) -> (Create, i32) {
let post = Post::get(conn, self.post_id).unwrap();
// We have to manually compute it since the new comment haven't been inserted yet, and it needs the activity we are building to be created
let next_id = get_next_id(conn, "comments_id_seq");
self.ap_url = Some(format!("{}#comment-{}", post.ap_url, next_id));
self.sensitive = false;
self.spoiler_text = String::new();
let (html, mentions) = utils::md_to_html(self.content.get().as_ref());
let author = User::get(conn, self.author_id).unwrap();
let mut note = Note::default();
let mut to = author.get_followers(conn).into_iter().map(User::into_id).collect::<Vec<Id>>();
to.append(&mut post
.get_authors(conn)
.into_iter()
.flat_map(|a| a.get_followers(conn))
.map(User::into_id)
.collect::<Vec<Id>>());
to.push(Id::new(PUBLIC_VISIBILTY.to_string()));
note.object_props.set_id_string(self.ap_url.clone().unwrap_or(String::new())).expect("NewComment::create: note.id error");
note.object_props.set_summary_string(self.spoiler_text.clone()).expect("NewComment::create: note.summary error");
note.object_props.set_content_string(html).expect("NewComment::create: note.content error");
note.object_props.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(|| Post::get(conn, self.post_id).unwrap().ap_url, |id| {
let comm = Comment::get(conn, id).unwrap();
comm.ap_url.clone().unwrap_or(comm.compute_id(conn))
}))).expect("NewComment::create: note.in_reply_to error");
note.object_props.set_published_string(chrono::Utc::now().to_rfc3339()).expect("NewComment::create: note.published error");
note.object_props.set_attributed_to_link(author.clone().into_id()).expect("NewComment::create: note.attributed_to error");
note.object_props.set_to_link_vec(to).expect("NewComment::create: note.to error");
note.object_props.set_tag_link_vec(mentions.into_iter().map(|m| Mention::build_activity(conn, m)).collect::<Vec<link::Mention>>())
.expect("NewComment::create: note.tag error");
let mut act = Create::default();
act.create_props.set_actor_link(author.into_id()).expect("NewComment::create: actor error");
act.create_props.set_object_object(note).expect("NewComment::create: object error");
act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().unwrap())).expect("NewComment::create: id error");
(act, next_id)
}
}

View File

@ -1,7 +1,7 @@
use activitypub::{Actor, activity::{Accept, Follow as FollowAct}}; use activitypub::{Actor, activity::{Accept, Follow as FollowAct}};
use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl};
use activity_pub::{broadcast, Id, IntoId, actor::Actor as ApActor, inbox::{FromActivity, Notify, WithInbox}, sign::Signer}; use activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox}, sign::Signer};
use models::{ use models::{
blogs::Blog, blogs::Blog,
notifications::*, notifications::*,
@ -44,7 +44,7 @@ impl Follow {
let mut accept = Accept::default(); let mut accept = Accept::default();
accept.accept_props.set_actor_link::<Id>(from.clone().into_id()).unwrap(); accept.accept_props.set_actor_link::<Id>(from.clone().into_id()).unwrap();
accept.accept_props.set_object_object(follow).unwrap(); accept.accept_props.set_object_object(follow).unwrap();
broadcast(conn, &*from, accept, vec![target.clone()]); broadcast(&*from, accept, vec![target.clone()]);
res res
} }
} }

View File

@ -1,9 +1,8 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection}; use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection};
use serde_json;
use std::iter::Iterator; use std::iter::Iterator;
use activity_pub::inbox::Inbox; use activity_pub::{ap_url, inbox::Inbox};
use models::users::User; use models::users::User;
use schema::{instances, users}; use schema::{instances, users};
@ -59,12 +58,16 @@ impl Instance {
.expect("Couldn't load admins") .expect("Couldn't load admins")
.len() > 0 .len() > 0
} }
}
impl Inbox for Instance { pub fn compute_box(&self, prefix: &'static str, name: String, box_name: &'static str) -> String {
fn received(&self, conn: &PgConnection, act: serde_json::Value) { ap_url(format!(
self.save(conn, act.clone()).expect("Shared Inbox: Couldn't save activity"); "{instance}/{prefix}/{name}/{box_name}",
instance = self.public_domain,
// TODO: add to stream, or whatever needs to be done prefix = prefix,
name = name,
box_name = box_name
))
} }
} }
impl Inbox for Instance {}

View File

@ -5,7 +5,6 @@ use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods};
use activity_pub::{ use activity_pub::{
Id, Id,
IntoId, IntoId,
actor::Actor,
inbox::{FromActivity, Deletable, Notify} inbox::{FromActivity, Deletable, Notify}
}; };
use models::{ use models::{
@ -70,8 +69,8 @@ impl Like {
pub fn compute_id(&self, conn: &PgConnection) -> String { pub fn compute_id(&self, conn: &PgConnection) -> String {
format!( format!(
"{}/like/{}", "{}/like/{}",
User::get(conn, self.user_id).unwrap().compute_id(conn), User::get(conn, self.user_id).unwrap().ap_url,
Post::get(conn, self.post_id).unwrap().compute_id(conn) Post::get(conn, self.post_id).unwrap().ap_url
) )
} }
} }

View File

@ -1,7 +1,7 @@
use activitypub::link; use activitypub::link;
use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods};
use activity_pub::{Id, inbox::Notify}; use activity_pub::inbox::Notify;
use models::{ use models::{
comments::Comment, comments::Comment,
notifications::*, notifications::*,
@ -10,13 +10,13 @@ use models::{
}; };
use schema::mentions; use schema::mentions;
#[derive(Queryable, Identifiable)] #[derive(Queryable, Identifiable, Serialize, Deserialize)]
pub struct Mention { pub struct Mention {
pub id: i32, pub id: i32,
pub mentioned_id: i32, pub mentioned_id: i32,
pub post_id: Option<i32>, pub post_id: Option<i32>,
pub comment_id: Option<i32>, pub comment_id: Option<i32>,
pub ap_url: String pub ap_url: String // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -34,6 +34,7 @@ impl Mention {
find_by!(mentions, find_by_ap_url, ap_url as String); find_by!(mentions, find_by_ap_url, ap_url as String);
list_by!(mentions, list_for_user, mentioned_id as i32); list_by!(mentions, list_for_user, mentioned_id as i32);
list_by!(mentions, list_for_post, post_id as i32); list_by!(mentions, list_for_post, post_id as i32);
list_by!(mentions, list_for_comment, comment_id as i32);
pub fn get_mentioned(&self, conn: &PgConnection) -> Option<User> { pub fn get_mentioned(&self, conn: &PgConnection) -> Option<User> {
User::get(conn, self.mentioned_id) User::get(conn, self.mentioned_id)
@ -44,12 +45,11 @@ impl Mention {
} }
pub fn get_comment(&self, conn: &PgConnection) -> Option<Comment> { pub fn get_comment(&self, conn: &PgConnection) -> Option<Comment> {
self.post_id.and_then(|id| Comment::get(conn, id)) self.comment_id.and_then(|id| Comment::get(conn, id))
} }
pub fn build_activity(conn: &PgConnection, ment: String) -> link::Mention { pub fn build_activity(conn: &PgConnection, ment: String) -> link::Mention {
let user = User::find_by_fqn(conn, ment.clone()); let user = User::find_by_fqn(conn, ment.clone());
println!("building act : {} -> {:?}", ment, user);
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Error setting mention's href"); mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Error setting mention's href");
mention.link_props.set_name_string(format!("@{}", ment)).expect("Error setting mention's name"); mention.link_props.set_name_string(format!("@{}", ment)).expect("Error setting mention's name");
@ -64,21 +64,23 @@ impl Mention {
mention mention
} }
pub fn from_activity(conn: &PgConnection, ment: link::Mention, inside: Id) -> Option<Self> { pub fn from_activity(conn: &PgConnection, ment: link::Mention, inside: i32, in_post: bool) -> Option<Self> {
let ap_url = ment.link_props.href_string().unwrap(); let ap_url = ment.link_props.href_string().unwrap();
let mentioned = User::find_by_ap_url(conn, ap_url).unwrap(); let mentioned = User::find_by_ap_url(conn, ap_url).unwrap();
if let Some(post) = Post::find_by_ap_url(conn, inside.clone().into()) { if in_post {
let res = Mention::insert(conn, NewMention { Post::get(conn, inside.clone().into()).map(|post| {
mentioned_id: mentioned.id, let res = Mention::insert(conn, NewMention {
post_id: Some(post.id), mentioned_id: mentioned.id,
comment_id: None, post_id: Some(post.id),
ap_url: ment.link_props.href_string().unwrap_or(String::new()) comment_id: None,
}); ap_url: ment.link_props.href_string().unwrap_or(String::new())
res.notify(conn); });
Some(res) res.notify(conn);
res
})
} else { } else {
if let Some(comment) = Comment::find_by_ap_url(conn, inside.into()) { Comment::get(conn, inside.into()).map(|comment| {
let res = Mention::insert(conn, NewMention { let res = Mention::insert(conn, NewMention {
mentioned_id: mentioned.id, mentioned_id: mentioned.id,
post_id: None, post_id: None,
@ -86,10 +88,8 @@ impl Mention {
ap_url: ment.link_props.href_string().unwrap_or(String::new()) ap_url: ment.link_props.href_string().unwrap_or(String::new())
}); });
res.notify(conn); res.notify(conn);
Some(res) res
} else { })
None
}
} }
} }
} }
@ -98,7 +98,7 @@ impl Notify for Mention {
fn notify(&self, conn: &PgConnection) { fn notify(&self, conn: &PgConnection) {
let author = self.get_comment(conn) let author = self.get_comment(conn)
.map(|c| c.get_author(conn).display_name.clone()) .map(|c| c.get_author(conn).display_name.clone())
.unwrap_or(self.get_post(conn).unwrap().get_authors(conn)[0].display_name.clone()); .unwrap_or_else(|| self.get_post(conn).unwrap().get_authors(conn)[0].display_name.clone());
self.get_mentioned(conn).map(|m| { self.get_mentioned(conn).map(|m| {
Notification::insert(conn, NewNotification { Notification::insert(conn, NewNotification {

View File

@ -1,3 +1,5 @@
use diesel::{PgConnection, RunQueryDsl, select};
macro_rules! find_by { macro_rules! find_by {
($table:ident, $fn:ident, $($col:ident as $type:ident),+) => { ($table:ident, $fn:ident, $($col:ident as $type:ident),+) => {
/// Try to find a $table with a given $col /// Try to find a $table with a given $col
@ -47,6 +49,16 @@ macro_rules! insert {
}; };
} }
sql_function!(nextval, nextval_t, (seq: ::diesel::sql_types::Text) -> ::diesel::sql_types::BigInt);
sql_function!(setval, setval_t, (seq: ::diesel::sql_types::Text, val: ::diesel::sql_types::BigInt) -> ::diesel::sql_types::BigInt);
fn get_next_id(conn: &PgConnection, seq: &str) -> i32 {
// We cant' use currval because it may fail if nextval have never been called before
let next = select(nextval(seq)).get_result::<i64>(conn).expect("Next ID fail");
select(setval(seq, next - 1)).get_result::<i64>(conn).expect("Reset ID fail");
next as i32
}
pub mod blog_authors; pub mod blog_authors;
pub mod blogs; pub mod blogs;
pub mod comments; pub mod comments;

View File

@ -155,7 +155,7 @@ impl Post {
content: Some(serde_json::to_value(self.content.clone()).unwrap()), content: Some(serde_json::to_value(self.content.clone()).unwrap()),
published: Some(serde_json::to_value(self.creation_date).unwrap()), published: Some(serde_json::to_value(self.creation_date).unwrap()),
tag: Some(serde_json::to_value(mentions).unwrap()), tag: Some(serde_json::to_value(mentions).unwrap()),
url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), url: Some(serde_json::to_value(self.ap_url.clone()).unwrap()),
to: Some(serde_json::to_value(to).unwrap()), to: Some(serde_json::to_value(to).unwrap()),
cc: Some(serde_json::to_value(Vec::<serde_json::Value>::new()).unwrap()), cc: Some(serde_json::to_value(Vec::<serde_json::Value>::new()).unwrap()),
..ObjectProperties::default() ..ObjectProperties::default()
@ -187,16 +187,7 @@ impl Post {
impl FromActivity<Article> for Post { impl FromActivity<Article> for Post {
fn from_activity(conn: &PgConnection, article: Article, _actor: Id) -> Post { fn from_activity(conn: &PgConnection, article: Article, _actor: Id) -> Post {
// save mentions let post = Post::insert(conn, NewPost {
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag)
.map(|m| Mention::from_activity(conn, m, Id::new(article.clone().object_props.clone().url_string().unwrap_or(String::from("")))))
.ok();
}
}
Post::insert(conn, NewPost {
blog_id: 0, // TODO blog_id: 0, // TODO
slug: String::from(""), // TODO slug: String::from(""), // TODO
title: article.object_props.name_string().unwrap(), title: article.object_props.name_string().unwrap(),
@ -204,7 +195,17 @@ impl FromActivity<Article> for Post {
published: true, published: true,
license: String::from("CC-0"), license: String::from("CC-0"),
ap_url: article.object_props.url_string().unwrap_or(String::from("")) ap_url: article.object_props.url_string().unwrap_or(String::from(""))
}) });
// save mentions
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
for tag in tags.into_iter() {
serde_json::from_value::<link::Mention>(tag)
.map(|m| Mention::from_activity(conn, m, post.id, true))
.ok();
}
}
post
} }
} }

View File

@ -2,7 +2,7 @@ use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods};
use activity_pub::{Id, IntoId, actor::Actor, inbox::{FromActivity, Notify, Deletable}}; use activity_pub::{Id, IntoId, inbox::{FromActivity, Notify, Deletable}};
use models::{notifications::*, posts::Post, users::User}; use models::{notifications::*, posts::Post, users::User};
use schema::reshares; use schema::reshares;
@ -34,8 +34,8 @@ impl Reshare {
diesel::update(self) diesel::update(self)
.set(reshares::ap_url.eq(format!( .set(reshares::ap_url.eq(format!(
"{}/reshare/{}", "{}/reshare/{}",
User::get(conn, self.user_id).unwrap().compute_id(conn), User::get(conn, self.user_id).unwrap().ap_url,
Post::get(conn, self.post_id).unwrap().compute_id(conn) Post::get(conn, self.post_id).unwrap().ap_url
))) )))
.get_result::<Reshare>(conn).expect("Couldn't update AP URL"); .get_result::<Reshare>(conn).expect("Couldn't update AP URL");
} }

View File

@ -1,8 +1,7 @@
use activitypub::{ use activitypub::{
Actor, Object, Actor, Object, Endpoint, CustomObject,
actor::{Person, properties::ApActorProperties}, actor::Person,
collection::OrderedCollection, collection::OrderedCollection
object::properties::ObjectProperties
}; };
use bcrypt; use bcrypt;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -28,8 +27,7 @@ use webfinger::*;
use BASE_URL; use BASE_URL;
use activity_pub::{ use activity_pub::{
ap_url, ActivityStream, Id, IntoId, ap_url, ActivityStream, Id, IntoId, ApSignature, PublicKey,
actor::{ActorType, Actor as APActor},
inbox::{Inbox, WithInbox}, inbox::{Inbox, WithInbox},
sign::{Signer, gen_keypair} sign::{Signer, gen_keypair}
}; };
@ -47,6 +45,8 @@ use safe_string::SafeString;
pub const AUTH_COOKIE: &'static str = "user_id"; pub const AUTH_COOKIE: &'static str = "user_id";
pub type CustomPerson = CustomObject<ApSignature, Person>;
#[derive(Queryable, Identifiable, Serialize, Deserialize, Clone, Debug)] #[derive(Queryable, Identifiable, Serialize, Deserialize, Clone, Debug)]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
@ -84,6 +84,8 @@ pub struct NewUser {
pub shared_inbox_url: Option<String> pub shared_inbox_url: Option<String>
} }
const USER_PREFIX: &'static str = "@";
impl User { impl User {
insert!(users, NewUser); insert!(users, NewUser);
get!(users); get!(users);
@ -91,6 +93,10 @@ impl User {
find_by!(users, find_by_name, username as String, instance_id as i32); find_by!(users, find_by_name, username as String, instance_id as i32);
find_by!(users, find_by_ap_url, ap_url as String); find_by!(users, find_by_ap_url, ap_url as String);
pub fn get_instance(&self, conn: &PgConnection) -> Instance {
Instance::get(conn, self.instance_id).expect("Couldn't find instance")
}
pub fn grant_admin_rights(&self, conn: &PgConnection) { pub fn grant_admin_rights(&self, conn: &PgConnection) {
diesel::update(self) diesel::update(self)
.set(users::is_admin.eq(true)) .set(users::is_admin.eq(true))
@ -153,14 +159,14 @@ impl User {
.send(); .send();
match req { match req {
Ok(mut res) => { Ok(mut res) => {
let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); let json: CustomPerson = serde_json::from_str(&res.text().unwrap()).unwrap();
Some(User::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string())) Some(User::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string()))
}, },
Err(_) => None Err(_) => None
} }
} }
fn from_activity(conn: &PgConnection, acct: serde_json::Value, inst: String) -> User { fn from_activity(conn: &PgConnection, acct: CustomPerson, inst: String) -> User {
let instance = match Instance::find_by_domain(conn, inst.clone()) { let instance = match Instance::find_by_domain(conn, inst.clone()) {
Some(instance) => instance, Some(instance) => instance,
None => { None => {
@ -172,19 +178,21 @@ impl User {
} }
}; };
User::insert(conn, NewUser { User::insert(conn, NewUser {
username: acct["preferredUsername"].as_str().unwrap().to_string(), username: acct.object.ap_actor_props.preferred_username_string().expect("User::from_activity: preferredUsername error"),
display_name: acct["name"].as_str().unwrap().to_string(), display_name: acct.object.object_props.name_string().expect("User::from_activity: name error"),
outbox_url: acct["outbox"].as_str().unwrap().to_string(), outbox_url: acct.object.ap_actor_props.outbox_string().expect("User::from_activity: outbox error"),
inbox_url: acct["inbox"].as_str().unwrap().to_string(), inbox_url: acct.object.ap_actor_props.inbox_string().expect("User::from_activity: inbox error"),
is_admin: false, is_admin: false,
summary: SafeString::new(&acct["summary"].as_str().unwrap().to_string()), summary: SafeString::new(&acct.object.object_props.summary_string().expect("User::from_activity: summary error")),
email: None, email: None,
hashed_password: None, hashed_password: None,
instance_id: instance.id, instance_id: instance.id,
ap_url: acct["id"].as_str().unwrap().to_string(), ap_url: acct.object.object_props.id_string().expect("User::from_activity: id error"),
public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap().to_string(), public_key: acct.custom_props.public_key_publickey().expect("User::from_activity: publicKey error")
.public_key_pem_string().expect("User::from_activity: publicKey.publicKeyPem error"),
private_key: None, private_key: None,
shared_inbox_url: acct["endpoints"]["sharedInbox"].as_str().map(|s| s.to_string()) shared_inbox_url: acct.object.ap_actor_props.endpoints_endpoint()
.and_then(|e| e.shared_inbox_string()).ok()
}) })
} }
@ -197,21 +205,22 @@ impl User {
} }
pub fn update_boxes(&self, conn: &PgConnection) { pub fn update_boxes(&self, conn: &PgConnection) {
let instance = self.get_instance(conn);
if self.outbox_url.len() == 0 { if self.outbox_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(users::outbox_url.eq(self.compute_outbox(conn))) .set(users::outbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "outbox")))
.get_result::<User>(conn).expect("Couldn't update outbox URL"); .get_result::<User>(conn).expect("Couldn't update outbox URL");
} }
if self.inbox_url.len() == 0 { if self.inbox_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(users::inbox_url.eq(self.compute_inbox(conn))) .set(users::inbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "inbox")))
.get_result::<User>(conn).expect("Couldn't update inbox URL"); .get_result::<User>(conn).expect("Couldn't update inbox URL");
} }
if self.ap_url.len() == 0 { if self.ap_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(users::ap_url.eq(self.compute_id(conn))) .set(users::ap_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "")))
.get_result::<User>(conn).expect("Couldn't update AP URL"); .get_result::<User>(conn).expect("Couldn't update AP URL");
} }
@ -306,28 +315,28 @@ impl User {
PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap()
} }
pub fn into_activity(&self, conn: &PgConnection) -> Person { pub fn into_activity(&self, _conn: &PgConnection) -> CustomPerson {
let mut actor = Person::default(); let mut actor = Person::default();
actor.object_props = ObjectProperties { actor.object_props.set_id_string(self.ap_url.clone()).expect("User::into_activity: id error");
id: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), actor.object_props.set_name_string(self.display_name.clone()).expect("User::into_activity: name error");
name: Some(serde_json::to_value(self.get_display_name()).unwrap()), actor.object_props.set_summary_string(self.summary.get().clone()).expect("User::into_activity: summary error");
summary: Some(serde_json::to_value(self.get_summary()).unwrap()), actor.object_props.set_url_string(self.ap_url.clone()).expect("User::into_activity: url error");
url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error");
..ObjectProperties::default() actor.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("User::into_activity: outbox error");
}; actor.ap_actor_props.set_preferred_username_string(self.username.clone()).expect("User::into_activity: preferredUsername error");
actor.ap_actor_props = ApActorProperties {
inbox: serde_json::to_value(self.compute_inbox(conn)).unwrap(), let mut endpoints = Endpoint::default();
outbox: serde_json::to_value(self.compute_outbox(conn)).unwrap(), endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error");
preferred_username: Some(serde_json::to_value(self.get_actor_id()).unwrap()), actor.ap_actor_props.set_endpoints_endpoint(endpoints).expect("User::into_activity: endpoints error");
endpoints: Some(json!({
"sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str())) let mut public_key = PublicKey::default();
})), public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error");
followers: None, public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error");
following: None, public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error");
liked: None, let mut ap_signature = ApSignature::default();
streams: None ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error");
};
actor CustomPerson::new(actor, ap_signature)
} }
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
@ -339,26 +348,38 @@ impl User {
pub fn webfinger(&self, conn: &PgConnection) -> Webfinger { pub fn webfinger(&self, conn: &PgConnection) -> Webfinger {
Webfinger { Webfinger {
subject: format!("acct:{}@{}", self.username, self.get_instance(conn).public_domain), subject: format!("acct:{}@{}", self.username, self.get_instance(conn).public_domain),
aliases: vec![self.compute_id(conn)], aliases: vec![self.ap_url.clone()],
links: vec![ links: vec![
Link { Link {
rel: String::from("http://webfinger.net/rel/profile-page"), rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None, mime_type: None,
href: self.compute_id(conn) href: self.ap_url.clone()
}, },
Link { Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"), rel: String::from("http://schemas.google.com/g/2010#updates-from"),
mime_type: Some(String::from("application/atom+xml")), mime_type: Some(String::from("application/atom+xml")),
href: self.compute_box(conn, "feed.atom") href: self.get_instance(conn).compute_box(USER_PREFIX, self.username.clone(), "feed.atom")
}, },
Link { Link {
rel: String::from("self"), rel: String::from("self"),
mime_type: Some(String::from("application/activity+json")), mime_type: Some(String::from("application/activity+json")),
href: self.compute_id(conn) href: self.ap_url.clone()
} }
] ]
} }
} }
pub fn from_url(conn: &PgConnection, url: String) -> Option<User> {
User::find_by_ap_url(conn, url.clone()).or_else(|| {
// The requested user was not in the DB
// We try to fetch it if it is remote
if Url::parse(url.as_ref()).unwrap().host_str().unwrap() != BASE_URL.as_str() {
Some(User::fetch_from_url(conn, url).unwrap())
} else {
None
}
})
}
} }
impl<'a, 'r> FromRequest<'a, 'r> for User { impl<'a, 'r> FromRequest<'a, 'r> for User {
@ -374,63 +395,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
} }
} }
impl APActor for User {
fn get_box_prefix() -> &'static str {
"@"
}
fn get_actor_id(&self) -> String {
self.username.to_string()
}
fn get_display_name(&self) -> String {
self.display_name.clone()
}
fn get_summary(&self) -> String {
self.summary.get().clone()
}
fn get_instance(&self, conn: &PgConnection) -> Instance {
Instance::get(conn, self.instance_id).unwrap()
}
fn get_actor_type() -> ActorType {
ActorType::Person
}
fn get_inbox_url(&self) -> String {
self.inbox_url.clone()
}
fn get_shared_inbox_url(&self) -> Option<String> {
self.shared_inbox_url.clone()
}
fn custom_props(&self, conn: &PgConnection) -> serde_json::Map<String, serde_json::Value> {
let mut res = serde_json::Map::new();
res.insert("publicKey".to_string(), json!({
"id": self.get_key_id(conn),
"owner": self.compute_id(conn),
"publicKeyPem": self.public_key
}));
res.insert("followers".to_string(), serde_json::Value::String(self.compute_box(conn, "followers")));
res
}
fn from_url(conn: &PgConnection, url: String) -> Option<User> {
User::find_by_ap_url(conn, url.clone()).or_else(|| {
// The requested user was not in the DB
// We try to fetch it if it is remote
if Url::parse(url.as_ref()).unwrap().host_str().unwrap() != BASE_URL.as_str() {
Some(User::fetch_from_url(conn, url).unwrap())
} else {
None
}
})
}
}
impl IntoId for User { impl IntoId for User {
fn into_id(self) -> Id { fn into_id(self) -> Id {
Id::new(self.ap_url.clone()) Id::new(self.ap_url.clone())
@ -450,19 +414,11 @@ impl WithInbox for User {
} }
} }
impl Inbox for User { impl Inbox for User {}
fn received(&self, conn: &PgConnection, act: serde_json::Value) {
if let Err(err) = self.save(conn, act.clone()) {
println!("Inbox error:\n{}\n{}\n\nActivity was: {}", err.cause(), err.backtrace(), act.to_string());
}
// TODO: add to stream, or whatever needs to be done
}
}
impl Signer for User { impl Signer for User {
fn get_key_id(&self, conn: &PgConnection) -> String { fn get_key_id(&self) -> String {
format!("{}#main-key", self.compute_id(conn)) format!("{}#main-key", self.ap_url)
} }
fn sign(&self, to_sign: String) -> Vec<u8> { fn sign(&self, to_sign: String) -> Vec<u8> {

View File

@ -6,7 +6,7 @@ use rocket::{
use rocket_contrib::Template; use rocket_contrib::Template;
use serde_json; use serde_json;
use activity_pub::{ActivityStream, ActivityPub, actor::Actor}; use activity_pub::ActivityStream;
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
blog_authors::*, blog_authors::*,
@ -32,9 +32,9 @@ fn details(name: String, conn: DbConn, user: Option<User>) -> Template {
} }
#[get("/~/<name>", format = "application/activity+json", rank = 1)] #[get("/~/<name>", format = "application/activity+json", rank = 1)]
fn activity_details(name: String, conn: DbConn) -> ActivityPub { fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomGroup> {
let blog = Blog::find_local(&*conn, name).unwrap(); let blog = Blog::find_local(&*conn, name).unwrap();
blog.as_activity_pub(&*conn) ActivityStream::new(blog.into_activity(&*conn))
} }
#[get("/blogs/new")] #[get("/blogs/new")]

View File

@ -1,41 +1,22 @@
use rocket::{ use rocket::{
request::Form, request::Form,
response::{Redirect, Flash} response::Redirect
}; };
use rocket_contrib::Template; use serde_json;
use activity_pub::{broadcast, inbox::Notify}; use activity_pub::{broadcast, inbox::Inbox};
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
blogs::Blog, blogs::Blog,
comments::*, comments::*,
instance::Instance,
posts::Post, posts::Post,
users::User users::User
}; };
use utils;
use safe_string::SafeString;
#[get("/~/<blog>/<slug>/comment")]
fn new(blog: String, slug: String, user: User, conn: DbConn) -> Template {
may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| {
may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| {
Template::render("comments/new", json!({
"post": post,
"account": user
}))
})
})
}
#[get("/~/<blog>/<slug>/comment", rank=2)]
fn new_auth(blog: String, slug: String) -> Flash<Redirect>{
utils::requires_login("You need to be logged in order to post a comment", uri!(new: blog = blog, slug = slug))
}
#[derive(FromForm)] #[derive(FromForm)]
struct CommentQuery { pub struct CommentQuery {
responding_to: Option<i32> pub responding_to: Option<i32>
} }
#[derive(FromForm)] #[derive(FromForm)]
@ -43,23 +24,29 @@ struct NewCommentForm {
pub content: String pub content: String
} }
// See: https://github.com/SergioBenitez/Rocket/pull/454
#[post("/~/<blog_name>/<slug>/comment", data = "<data>")]
fn create(blog_name: String, slug: String, data: Form<NewCommentForm>, user: User, conn: DbConn) -> Redirect {
create_response(blog_name, slug, None, data, user, conn)
}
#[post("/~/<blog_name>/<slug>/comment?<query>", data = "<data>")] #[post("/~/<blog_name>/<slug>/comment?<query>", data = "<data>")]
fn create(blog_name: String, slug: String, query: CommentQuery, data: Form<NewCommentForm>, user: User, conn: DbConn) -> Redirect { fn create_response(blog_name: String, slug: String, query: Option<CommentQuery>, data: Form<NewCommentForm>, user: User, conn: DbConn) -> Redirect {
let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap(); let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap();
let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap(); let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap();
let form = data.get(); let form = data.get();
let comment = Comment::insert(&*conn, NewComment {
content: SafeString::new(&form.content.clone()),
in_response_to_id: query.responding_to,
post_id: post.id,
author_id: user.id,
ap_url: None, // TODO: set it
sensitive: false,
spoiler_text: "".to_string()
});
comment.notify(&*conn);
broadcast(&*conn, &user, comment.create_activity(&*conn), user.get_followers(&*conn)); let (new_comment, id) = NewComment::build()
.content(form.content.clone())
.in_response_to_id(query.and_then(|q| q.responding_to))
.post(post)
.author(user.clone())
.create(&*conn);
Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, comment.id)) let instance = Instance::get_local(&*conn).unwrap();
instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error"))
.expect("We are not compatible with ourselve: local broadcast failed (new comment)");
broadcast(&user, new_comment, user.get_followers(&*conn));
Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id))
} }

View File

@ -35,8 +35,13 @@ fn index(conn: DbConn, user: Option<User>) -> Template {
fn shared_inbox(conn: DbConn, data: String) -> String { fn shared_inbox(conn: DbConn, data: String) -> String {
let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap();
let instance = Instance::get_local(&*conn).unwrap(); let instance = Instance::get_local(&*conn).unwrap();
instance.received(&*conn, act); match instance.received(&*conn, act) {
String::from("") Ok(_) => String::new(),
Err(e) => {
println!("Shared inbox error: {}\n{}", e.cause(), e.backtrace());
format!("Error: {}", e.cause())
}
}
} }
#[get("/nodeinfo")] #[get("/nodeinfo")]

View File

@ -25,11 +25,11 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
like.update_ap_url(&*conn); like.update_ap_url(&*conn);
like.notify(&*conn); like.notify(&*conn);
broadcast(&*conn, &user, like.into_activity(&*conn), user.get_followers(&*conn)); broadcast(&user, like.into_activity(&*conn), user.get_followers(&*conn));
} else { } else {
let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).unwrap(); let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).unwrap();
let delete_act = like.delete(&*conn); let delete_act = like.delete(&*conn);
broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); broadcast(&user, delete_act, user.get_followers(&*conn));
} }
Redirect::to(uri!(super::posts::details: blog = blog, slug = slug)) Redirect::to(uri!(super::posts::details: blog = blog, slug = slug))

View File

@ -1,10 +1,11 @@
use activitypub::object::Article;
use heck::KebabCase; use heck::KebabCase;
use rocket::request::Form; use rocket::request::Form;
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use rocket_contrib::Template; use rocket_contrib::Template;
use serde_json; use serde_json;
use activity_pub::{broadcast, context, activity_pub, ActivityPub, Id}; use activity_pub::{broadcast, ActivityStream};
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
blogs::*, blogs::*,
@ -14,14 +15,21 @@ use models::{
posts::*, posts::*,
users::User users::User
}; };
use routes::comments::CommentQuery;
use safe_string::SafeString; use safe_string::SafeString;
use utils; use utils;
// See: https://github.com/SergioBenitez/Rocket/pull/454
#[get("/~/<blog>/<slug>", rank = 4)] #[get("/~/<blog>/<slug>", rank = 4)]
fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Template { fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Template {
details_response(blog, slug, conn, user, None)
}
#[get("/~/<blog>/<slug>?<query>")]
fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>, query: Option<CommentQuery>) -> Template {
may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| { may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| {
may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| { may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| {
let comments = Comment::find_by_post(&*conn, post.id); let comments = Comment::list_by_post(&*conn, post.id);
Template::render("posts/details", json!({ Template::render("posts/details", json!({
"author": post.get_authors(&*conn)[0].to_json(&*conn), "author": post.get_authors(&*conn)[0].to_json(&*conn),
@ -33,20 +41,20 @@ fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Temp
"n_reshares": post.get_reshares(&*conn).len(), "n_reshares": post.get_reshares(&*conn).len(),
"has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false), "has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false),
"account": user, "account": 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))),
"user_fqn": user.map(|u| u.get_fqn(&*conn)).unwrap_or(String::new())
})) }))
}) })
}) })
} }
#[get("/~/<blog>/<slug>", rank = 3, format = "application/activity+json")] #[get("/~/<blog>/<slug>", rank = 3, format = "application/activity+json")]
fn activity_details(blog: String, slug: String, conn: DbConn) -> ActivityPub { fn activity_details(blog: String, slug: String, conn: DbConn) -> ActivityStream<Article> {
let blog = Blog::find_by_fqn(&*conn, blog).unwrap(); let blog = Blog::find_by_fqn(&*conn, blog).unwrap();
let post = Post::find_by_slug(&*conn, slug, blog.id).unwrap(); let post = Post::find_by_slug(&*conn, slug, blog.id).unwrap();
let mut act = serde_json::to_value(post.into_activity(&*conn)).unwrap(); ActivityStream::new(post.into_activity(&*conn))
act["@context"] = context();
activity_pub(act)
} }
#[get("/~/<blog>/new", rank = 2)] #[get("/~/<blog>/new", rank = 2)]
@ -106,11 +114,11 @@ fn create(blog_name: String, data: Form<NewPostForm>, user: User, conn: DbConn)
}); });
for m in mentions.into_iter() { for m in mentions.into_iter() {
Mention::from_activity(&*conn, Mention::build_activity(&*conn, m), Id::new(post.compute_id(&*conn))); Mention::from_activity(&*conn, Mention::build_activity(&*conn, m), post.id, true);
} }
let act = post.create_activity(&*conn); let act = post.create_activity(&*conn);
broadcast(&*conn, &user, act, user.get_followers(&*conn)); broadcast(&user, act, user.get_followers(&*conn));
Redirect::to(uri!(details: blog = blog_name, slug = slug)) Redirect::to(uri!(details: blog = blog_name, slug = slug))
} }

View File

@ -25,11 +25,11 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
reshare.update_ap_url(&*conn); reshare.update_ap_url(&*conn);
reshare.notify(&*conn); reshare.notify(&*conn);
broadcast(&*conn, &user, reshare.into_activity(&*conn), user.get_followers(&*conn)); broadcast(&user, reshare.into_activity(&*conn), user.get_followers(&*conn));
} else { } else {
let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id).unwrap(); let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id).unwrap();
let delete_act = reshare.delete(&*conn); let delete_act = reshare.delete(&*conn);
broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); broadcast(&user, delete_act, user.get_followers(&*conn));
} }
Redirect::to(uri!(super::posts::details: blog = blog, slug = slug)) Redirect::to(uri!(super::posts::details: blog = blog, slug = slug))

View File

@ -9,9 +9,8 @@ use rocket_contrib::Template;
use serde_json; use serde_json;
use activity_pub::{ use activity_pub::{
activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId, ActivityStream, broadcast, Id, IntoId,
inbox::{Inbox, Notify}, inbox::{Inbox, Notify}
actor::Actor
}; };
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
@ -82,7 +81,7 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect {
act.follow_props.set_object_object(user.into_activity(&*conn)).unwrap(); 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_id_string(format!("{}/follow/{}", user.ap_url, target.ap_url)).unwrap();
broadcast(&*conn, &user, act, vec![target]); broadcast(&user, act, vec![target]);
Redirect::to(uri!(details: name = name)) Redirect::to(uri!(details: name = name))
} }
@ -110,9 +109,9 @@ fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
} }
#[get("/@/<name>", format = "application/activity+json", rank = 1)] #[get("/@/<name>", format = "application/activity+json", rank = 1)]
fn activity_details(name: String, conn: DbConn) -> ActivityPub { fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomPerson> {
let user = User::find_local(&*conn, name).unwrap(); let user = User::find_local(&*conn, name).unwrap();
user.as_activity_pub(&*conn) ActivityStream::new(user.into_activity(&*conn))
} }
#[get("/users/new")] #[get("/users/new")]
@ -199,21 +198,23 @@ fn outbox(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
fn inbox(name: String, conn: DbConn, data: String) -> String { fn inbox(name: String, conn: DbConn, data: String) -> String {
let user = User::find_local(&*conn, name).unwrap(); let user = User::find_local(&*conn, name).unwrap();
let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap();
user.received(&*conn, act); match user.received(&*conn, act) {
String::from("") Ok(_) => String::new(),
Err(e) => {
println!("User inbox error: {}\n{}", e.cause(), e.backtrace());
format!("Error: {}", e.cause())
}
}
} }
#[get("/@/<name>/followers", format = "application/activity+json")] #[get("/@/<name>/followers", format = "application/activity+json")]
fn ap_followers(name: String, conn: DbConn) -> ActivityPub { fn ap_followers(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
let user = User::find_local(&*conn, name).unwrap(); let user = User::find_local(&*conn, name).unwrap();
let followers = user.get_followers(&*conn).into_iter().map(|f| f.compute_id(&*conn)).collect::<Vec<String>>(); let followers = user.get_followers(&*conn).into_iter().map(|f| Id::new(f.ap_url)).collect::<Vec<Id>>();
let json = json!({ let mut coll = OrderedCollection::default();
"@context": context(), coll.object_props.set_id_string(format!("{}/followers", user.ap_url)).expect("Follower collection: id error");
"id": user.compute_box(&*conn, "followers"), coll.collection_props.set_total_items_u64(followers.len() as u64).expect("Follower collection: totalItems error");
"type": "OrderedCollection", coll.collection_props.set_items_link_vec(followers).expect("Follower collection: items error");
"totalItems": followers.len(), ActivityStream::new(coll)
"orderedItems": followers
});
activity_pub(json)
} }

View File

@ -9,7 +9,7 @@ use diesel::{self, deserialize::Queryable,
sql_types::Text, sql_types::Text,
serialize::{self, Output}}; serialize::{self, Output}};
#[derive(Debug,Clone,AsExpression,FromSqlRow)] #[derive(Debug, Clone, AsExpression, FromSqlRow, Default)]
#[sql_type = "Text"] #[sql_type = "Text"]
pub struct SafeString{ pub struct SafeString{
value: String, value: String,

View File

@ -257,7 +257,7 @@ input {
transition: all 0.1s ease-in; transition: all 0.1s ease-in;
display: block; display: block;
width: 100%; width: 100%;
margin: auto; margin: auto auto 5em;
padding: 0.5em; padding: 0.5em;
box-sizing: border-box; box-sizing: border-box;
@ -266,7 +266,7 @@ input {
border: none; border: none;
border-bottom: solid #DADADA 2px; border-bottom: solid #DADADA 2px;
} }
input[type="submit"] { margin: 2em auto; } form input[type="submit"] { margin: 2em auto; }
input:focus { input:focus {
background: #FAFAFA; background: #FAFAFA;
border-bottom-color: #7765E3; border-bottom-color: #7765E3;

View File

@ -1,15 +0,0 @@
{% extends "base" %}
{% block title %}
{{ 'Comment "{{ post }}"' | _(post=post.title) }}
{% endblock title %}
{% block content %}
<h1>{{ 'Comment "{{ post }}"' | _(post=post.title) }}</h1>
<form method="post">
<label for="content">{{ "Content" | _ }}</label>
<textarea id="content" name="content"></textarea>
<input type="submit" value="{{ "Submit comment" | _ }}" />
</form>
{% endblock content %}

View File

@ -60,7 +60,16 @@
<div class="comments"> <div class="comments">
<h2>{{ "Comments" | _ }}</h2> <h2>{{ "Comments" | _ }}</h2>
<a class="button" href="comment?">{{ "Comment" | _ }}</a>
{% if account %}
<form method="post" action="/~/{{ blog.actor_id }}/{{ post.slug }}/comment">
<label for="content">{{ "Your comment" | _ }}</label>
{# Ugly, but we don't have the choice if we don't want weird paddings #}
<textarea id="content" name="content">{% filter trim %}{% if previous %}{% if previous.author.fqn != user_fqn %}@{{ previous.author.fqn }} {% endif %}{% for mention in previous.mentions %}{% if mention != user_fqn %}@{{ mention }} {% endif %}{% endfor %}{% endif %}{% endfilter %}</textarea>
<input type="submit" value="{{ "Submit comment" | _ }}" />
</form>
{% endif %}
<div class="list"> <div class="list">
{% for comment in comments %} {% for comment in comments %}
{% if comment.author.display_name %} {% if comment.author.display_name %}
@ -75,7 +84,7 @@
<span class="username">@{{ comment.author.username }}</span> <span class="username">@{{ comment.author.username }}</span>
</a> </a>
<div class="text">{{ comment.content | safe }}</div> <div class="text">{{ comment.content | safe }}</div>
<a class="button" href="comment?responding_to={{ comment.id }}">{{ "Respond" | _ }}</a> <a class="button" href="?responding_to={{ comment.id }}">{{ "Respond" | _ }}</a>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>