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"
dependencies = [
"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)",
"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)",

View file

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

View file

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

View file

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

View file

@ -278,3 +278,6 @@ msgstr ""
msgid "{{ data }} mentioned you."
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 {
fn received(&self, conn: &PgConnection, act: serde_json::Value);
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> {
fn received(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> {
let actor_id = Id::new(act["actor"].as_str().unwrap());
match act["type"].as_str() {
Some(t) => {

View file

@ -1,24 +1,19 @@
use activitypub::{Activity, Actor, Object, Link};
use array_tool::vec::Uniq;
use diesel::PgConnection;
use reqwest::Client;
use rocket::{
http::{ContentType, Status},
response::{Response, Responder, Content},
http::Status,
response::{Response, Responder},
request::Request
};
use rocket_contrib::Json;
use serde_json;
use self::sign::Signable;
pub mod actor;
pub mod inbox;
pub mod request;
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 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);
impl<T> ActivityStream<T> {
@ -70,11 +61,15 @@ impl<T> ActivityStream<T> {
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
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()
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
.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();
act["@context"] = context();
let signed = act.sign(sender, conn);
let signed = act.sign(sender);
for inbox in boxes {
// TODO: run it in Sidekiq or something like that
let res = Client::new()
.post(&inbox[..])
.headers(request::headers())
.header(request::signature(sender, request::headers(), conn))
.header(request::signature(sender, request::headers()))
.header(request::digest(signed.to_string()))
.body(signed.to_string())
.send();
@ -120,3 +115,27 @@ pub trait IntoId {
}
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 diesel::PgConnection;
use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{Date, Headers, UserAgent};
use std::time::SystemTime;
@ -23,7 +22,7 @@ pub fn 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_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!(
"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,
signature = sign
))

View file

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

View file

@ -2,6 +2,9 @@
#![plugin(rocket_codegen)]
extern crate activitypub;
#[macro_use]
extern crate activitystreams_derive;
extern crate activitystreams_traits;
extern crate ammonia;
extern crate array_tool;
extern crate base64;
@ -68,9 +71,8 @@ fn main() {
routes::blogs::new_auth,
routes::blogs::create,
routes::comments::new,
routes::comments::new_auth,
routes::comments::create,
routes::comments::create_response,
routes::instance::index,
routes::instance::shared_inbox,
@ -83,6 +85,7 @@ fn main() {
routes::notifications::notifications_auth,
routes::posts::details,
routes::posts::details_response,
routes::posts::activity_details,
routes::posts::new,
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::{
Client,
header::{Accept, qitem},
@ -16,15 +16,16 @@ use openssl::{
};
use webfinger::*;
use BASE_URL;
use activity_pub::{
ActivityStream, Id, IntoId,
actor::{Actor as APActor, ActorType},
ApSignature, ActivityStream, Id, IntoId, PublicKey,
inbox::WithInbox,
sign
};
use models::instance::*;
use schema::blogs;
pub type CustomGroup = CustomObject<ApSignature, Group>;
#[derive(Queryable, Identifiable, Serialize, Deserialize, Clone)]
pub struct Blog {
@ -55,9 +56,17 @@ pub struct NewBlog {
pub public_key: String
}
const BLOG_PREFIX: &'static str = "~";
impl Blog {
insert!(blogs, NewBlog);
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> {
use schema::blog_authors;
@ -67,8 +76,6 @@ impl Blog {
.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> {
Blog::find_by_name(conn, name, Instance::local_id(conn))
}
@ -106,14 +113,14 @@ impl Blog {
.send();
match req {
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()))
},
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()) {
Some(instance) => instance,
None => {
@ -125,34 +132,55 @@ impl Blog {
}
};
Blog::insert(conn, NewBlog {
actor_id: acct["preferredUsername"].as_str().unwrap().to_string(),
title: acct["name"].as_str().unwrap().to_string(),
outbox_url: acct["outbox"].as_str().unwrap().to_string(),
inbox_url: acct["inbox"].as_str().unwrap().to_string(),
summary: acct["summary"].as_str().unwrap().to_string(),
actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"),
title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"),
outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"),
inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"),
summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"),
instance_id: instance.id,
ap_url: acct["id"].as_str().unwrap().to_string(),
public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap_or("").to_string(),
ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"),
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
})
}
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) {
let instance = self.get_instance(conn);
if self.outbox_url.len() == 0 {
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");
}
if self.inbox_url.len() == 0 {
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");
}
if self.ap_url.len() == 0 {
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");
}
}
@ -175,26 +203,38 @@ impl Blog {
pub fn webfinger(&self, conn: &PgConnection) -> Webfinger {
Webfinger {
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![
Link {
rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None,
href: self.compute_id(conn)
href: self.ap_url.clone()
},
Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
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 {
rel: String::from("self"),
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 {
@ -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 {
fn get_key_id(&self, conn: &PgConnection) -> String {
format!("{}#main-key", self.compute_id(conn))
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: String) -> Vec<u8> {

View file

@ -1,7 +1,7 @@
use activitypub::{
activity::Create,
link,
object::{Note, properties::ObjectProperties}
object::{Note}
};
use chrono;
use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl::any};
@ -9,10 +9,10 @@ use serde_json;
use activity_pub::{
ap_url, Id, IntoId, PUBLIC_VISIBILTY,
actor::Actor,
inbox::{FromActivity, Notify}
};
use models::{
get_next_id,
instance::Instance,
mentions::Mention,
notifications::*,
@ -21,6 +21,7 @@ use models::{
};
use schema::comments;
use safe_string::SafeString;
use utils;
#[derive(Queryable, Identifiable, Serialize, Clone)]
pub struct Comment {
@ -35,7 +36,7 @@ pub struct Comment {
pub spoiler_text: String
}
#[derive(Insertable)]
#[derive(Insertable, Default)]
#[table_name = "comments"]
pub struct NewComment {
pub content: SafeString,
@ -50,7 +51,7 @@ pub struct NewComment {
impl Comment {
insert!(comments, NewComment);
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);
pub fn get_author(&self, conn: &PgConnection) -> User {
@ -61,37 +62,6 @@ impl Comment {
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 {
use schema::users;
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 {
let mut json = serde_json::to_value(self).unwrap();
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
}
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_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 {
content: SafeString::new(&note.object_props.content_string().unwrap()),
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,
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
}
@ -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 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::{
blogs::Blog,
notifications::*,
@ -44,7 +44,7 @@ impl Follow {
let mut accept = Accept::default();
accept.accept_props.set_actor_link::<Id>(from.clone().into_id()).unwrap();
accept.accept_props.set_object_object(follow).unwrap();
broadcast(conn, &*from, accept, vec![target.clone()]);
broadcast(&*from, accept, vec![target.clone()]);
res
}
}

View file

@ -1,9 +1,8 @@
use chrono::NaiveDateTime;
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection};
use serde_json;
use std::iter::Iterator;
use activity_pub::inbox::Inbox;
use activity_pub::{ap_url, inbox::Inbox};
use models::users::User;
use schema::{instances, users};
@ -59,12 +58,16 @@ impl Instance {
.expect("Couldn't load admins")
.len() > 0
}
}
impl Inbox for Instance {
fn received(&self, conn: &PgConnection, act: serde_json::Value) {
self.save(conn, act.clone()).expect("Shared Inbox: Couldn't save activity");
// TODO: add to stream, or whatever needs to be done
pub fn compute_box(&self, prefix: &'static str, name: String, box_name: &'static str) -> String {
ap_url(format!(
"{instance}/{prefix}/{name}/{box_name}",
instance = self.public_domain,
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::{
Id,
IntoId,
actor::Actor,
inbox::{FromActivity, Deletable, Notify}
};
use models::{
@ -70,8 +69,8 @@ impl Like {
pub fn compute_id(&self, conn: &PgConnection) -> String {
format!(
"{}/like/{}",
User::get(conn, self.user_id).unwrap().compute_id(conn),
Post::get(conn, self.post_id).unwrap().compute_id(conn)
User::get(conn, self.user_id).unwrap().ap_url,
Post::get(conn, self.post_id).unwrap().ap_url
)
}
}

View file

@ -1,7 +1,7 @@
use activitypub::link;
use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods};
use activity_pub::{Id, inbox::Notify};
use activity_pub::inbox::Notify;
use models::{
comments::Comment,
notifications::*,
@ -10,13 +10,13 @@ use models::{
};
use schema::mentions;
#[derive(Queryable, Identifiable)]
#[derive(Queryable, Identifiable, Serialize, Deserialize)]
pub struct Mention {
pub id: i32,
pub mentioned_id: i32,
pub post_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)]
@ -34,6 +34,7 @@ impl Mention {
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_post, post_id as i32);
list_by!(mentions, list_for_comment, comment_id as i32);
pub fn get_mentioned(&self, conn: &PgConnection) -> Option<User> {
User::get(conn, self.mentioned_id)
@ -44,12 +45,11 @@ impl Mention {
}
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 {
let user = User::find_by_fqn(conn, ment.clone());
println!("building act : {} -> {:?}", ment, user);
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_name_string(format!("@{}", ment)).expect("Error setting mention's name");
@ -64,21 +64,23 @@ impl 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 mentioned = User::find_by_ap_url(conn, ap_url).unwrap();
if let Some(post) = Post::find_by_ap_url(conn, inside.clone().into()) {
let res = Mention::insert(conn, NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
comment_id: None,
ap_url: ment.link_props.href_string().unwrap_or(String::new())
});
res.notify(conn);
Some(res)
if in_post {
Post::get(conn, inside.clone().into()).map(|post| {
let res = Mention::insert(conn, NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
comment_id: None,
ap_url: ment.link_props.href_string().unwrap_or(String::new())
});
res.notify(conn);
res
})
} 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 {
mentioned_id: mentioned.id,
post_id: None,
@ -86,10 +88,8 @@ impl Mention {
ap_url: ment.link_props.href_string().unwrap_or(String::new())
});
res.notify(conn);
Some(res)
} else {
None
}
res
})
}
}
}
@ -98,7 +98,7 @@ impl Notify for Mention {
fn notify(&self, conn: &PgConnection) {
let author = self.get_comment(conn)
.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| {
Notification::insert(conn, NewNotification {

View file

@ -1,3 +1,5 @@
use diesel::{PgConnection, RunQueryDsl, select};
macro_rules! find_by {
($table:ident, $fn:ident, $($col:ident as $type:ident),+) => {
/// 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 blogs;
pub mod comments;

View file

@ -155,7 +155,7 @@ impl Post {
content: Some(serde_json::to_value(self.content.clone()).unwrap()),
published: Some(serde_json::to_value(self.creation_date).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()),
cc: Some(serde_json::to_value(Vec::<serde_json::Value>::new()).unwrap()),
..ObjectProperties::default()
@ -187,16 +187,7 @@ impl Post {
impl FromActivity<Article> for Post {
fn from_activity(conn: &PgConnection, article: Article, _actor: Id) -> Post {
// 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, Id::new(article.clone().object_props.clone().url_string().unwrap_or(String::from("")))))
.ok();
}
}
Post::insert(conn, NewPost {
let post = Post::insert(conn, NewPost {
blog_id: 0, // TODO
slug: String::from(""), // TODO
title: article.object_props.name_string().unwrap(),
@ -204,7 +195,17 @@ impl FromActivity<Article> for Post {
published: true,
license: String::from("CC-0"),
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 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 schema::reshares;
@ -34,8 +34,8 @@ impl Reshare {
diesel::update(self)
.set(reshares::ap_url.eq(format!(
"{}/reshare/{}",
User::get(conn, self.user_id).unwrap().compute_id(conn),
Post::get(conn, self.post_id).unwrap().compute_id(conn)
User::get(conn, self.user_id).unwrap().ap_url,
Post::get(conn, self.post_id).unwrap().ap_url
)))
.get_result::<Reshare>(conn).expect("Couldn't update AP URL");
}

View file

@ -1,8 +1,7 @@
use activitypub::{
Actor, Object,
actor::{Person, properties::ApActorProperties},
collection::OrderedCollection,
object::properties::ObjectProperties
Actor, Object, Endpoint, CustomObject,
actor::Person,
collection::OrderedCollection
};
use bcrypt;
use chrono::NaiveDateTime;
@ -28,8 +27,7 @@ use webfinger::*;
use BASE_URL;
use activity_pub::{
ap_url, ActivityStream, Id, IntoId,
actor::{ActorType, Actor as APActor},
ap_url, ActivityStream, Id, IntoId, ApSignature, PublicKey,
inbox::{Inbox, WithInbox},
sign::{Signer, gen_keypair}
};
@ -47,6 +45,8 @@ use safe_string::SafeString;
pub const AUTH_COOKIE: &'static str = "user_id";
pub type CustomPerson = CustomObject<ApSignature, Person>;
#[derive(Queryable, Identifiable, Serialize, Deserialize, Clone, Debug)]
pub struct User {
pub id: i32,
@ -84,6 +84,8 @@ pub struct NewUser {
pub shared_inbox_url: Option<String>
}
const USER_PREFIX: &'static str = "@";
impl User {
insert!(users, NewUser);
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_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) {
diesel::update(self)
.set(users::is_admin.eq(true))
@ -153,14 +159,14 @@ impl User {
.send();
match req {
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()))
},
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()) {
Some(instance) => instance,
None => {
@ -172,19 +178,21 @@ impl User {
}
};
User::insert(conn, NewUser {
username: acct["preferredUsername"].as_str().unwrap().to_string(),
display_name: acct["name"].as_str().unwrap().to_string(),
outbox_url: acct["outbox"].as_str().unwrap().to_string(),
inbox_url: acct["inbox"].as_str().unwrap().to_string(),
username: acct.object.ap_actor_props.preferred_username_string().expect("User::from_activity: preferredUsername error"),
display_name: acct.object.object_props.name_string().expect("User::from_activity: name error"),
outbox_url: acct.object.ap_actor_props.outbox_string().expect("User::from_activity: outbox error"),
inbox_url: acct.object.ap_actor_props.inbox_string().expect("User::from_activity: inbox error"),
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,
hashed_password: None,
instance_id: instance.id,
ap_url: acct["id"].as_str().unwrap().to_string(),
public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap().to_string(),
ap_url: acct.object.object_props.id_string().expect("User::from_activity: id error"),
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,
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) {
let instance = self.get_instance(conn);
if self.outbox_url.len() == 0 {
diesel::update(self)
.set(users::outbox_url.eq(self.compute_outbox(conn)))
.get_result::<User>(conn).expect("Couldn't update outbox URL");
.set(users::outbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "outbox")))
.get_result::<User>(conn).expect("Couldn't update outbox URL");
}
if self.inbox_url.len() == 0 {
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");
}
if self.ap_url.len() == 0 {
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");
}
@ -306,28 +315,28 @@ impl User {
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();
actor.object_props = ObjectProperties {
id: Some(serde_json::to_value(self.compute_id(conn)).unwrap()),
name: Some(serde_json::to_value(self.get_display_name()).unwrap()),
summary: Some(serde_json::to_value(self.get_summary()).unwrap()),
url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()),
..ObjectProperties::default()
};
actor.ap_actor_props = ApActorProperties {
inbox: serde_json::to_value(self.compute_inbox(conn)).unwrap(),
outbox: serde_json::to_value(self.compute_outbox(conn)).unwrap(),
preferred_username: Some(serde_json::to_value(self.get_actor_id()).unwrap()),
endpoints: Some(json!({
"sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str()))
})),
followers: None,
following: None,
liked: None,
streams: None
};
actor
actor.object_props.set_id_string(self.ap_url.clone()).expect("User::into_activity: id error");
actor.object_props.set_name_string(self.display_name.clone()).expect("User::into_activity: name error");
actor.object_props.set_summary_string(self.summary.get().clone()).expect("User::into_activity: summary error");
actor.object_props.set_url_string(self.ap_url.clone()).expect("User::into_activity: url error");
actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error");
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");
let mut endpoints = Endpoint::default();
endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error");
actor.ap_actor_props.set_endpoints_endpoint(endpoints).expect("User::into_activity: endpoints 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");
CustomPerson::new(actor, ap_signature)
}
pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value {
@ -339,26 +348,38 @@ impl User {
pub fn webfinger(&self, conn: &PgConnection) -> Webfinger {
Webfinger {
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![
Link {
rel: String::from("http://webfinger.net/rel/profile-page"),
mime_type: None,
href: self.compute_id(conn)
href: self.ap_url.clone()
},
Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
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 {
rel: String::from("self"),
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 {
@ -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 {
fn into_id(self) -> Id {
Id::new(self.ap_url.clone())
@ -450,19 +414,11 @@ impl WithInbox 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 Inbox for User {}
impl Signer for User {
fn get_key_id(&self, conn: &PgConnection) -> String {
format!("{}#main-key", self.compute_id(conn))
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: String) -> Vec<u8> {

View file

@ -6,7 +6,7 @@ use rocket::{
use rocket_contrib::Template;
use serde_json;
use activity_pub::{ActivityStream, ActivityPub, actor::Actor};
use activity_pub::ActivityStream;
use db_conn::DbConn;
use models::{
blog_authors::*,
@ -32,9 +32,9 @@ fn details(name: String, conn: DbConn, user: Option<User>) -> Template {
}
#[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();
blog.as_activity_pub(&*conn)
ActivityStream::new(blog.into_activity(&*conn))
}
#[get("/blogs/new")]

View file

@ -1,41 +1,22 @@
use rocket::{
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 models::{
blogs::Blog,
comments::*,
instance::Instance,
posts::Post,
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)]
struct CommentQuery {
responding_to: Option<i32>
pub struct CommentQuery {
pub responding_to: Option<i32>
}
#[derive(FromForm)]
@ -43,23 +24,29 @@ struct NewCommentForm {
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>")]
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 post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap();
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 {
let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap();
let instance = Instance::get_local(&*conn).unwrap();
instance.received(&*conn, act);
String::from("")
match instance.received(&*conn, act) {
Ok(_) => String::new(),
Err(e) => {
println!("Shared inbox error: {}\n{}", e.cause(), e.backtrace());
format!("Error: {}", e.cause())
}
}
}
#[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.notify(&*conn);
broadcast(&*conn, &user, like.into_activity(&*conn), user.get_followers(&*conn));
broadcast(&user, like.into_activity(&*conn), user.get_followers(&*conn));
} else {
let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).unwrap();
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))

View file

@ -1,10 +1,11 @@
use activitypub::object::Article;
use heck::KebabCase;
use rocket::request::Form;
use rocket::response::{Redirect, Flash};
use rocket_contrib::Template;
use serde_json;
use activity_pub::{broadcast, context, activity_pub, ActivityPub, Id};
use activity_pub::{broadcast, ActivityStream};
use db_conn::DbConn;
use models::{
blogs::*,
@ -14,14 +15,21 @@ use models::{
posts::*,
users::User
};
use routes::comments::CommentQuery;
use safe_string::SafeString;
use utils;
// See: https://github.com/SergioBenitez/Rocket/pull/454
#[get("/~/<blog>/<slug>", rank = 4)]
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!(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!({
"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(),
"has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false),
"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")]
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 post = Post::find_by_slug(&*conn, slug, blog.id).unwrap();
let mut act = serde_json::to_value(post.into_activity(&*conn)).unwrap();
act["@context"] = context();
activity_pub(act)
ActivityStream::new(post.into_activity(&*conn))
}
#[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() {
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);
broadcast(&*conn, &user, act, user.get_followers(&*conn));
broadcast(&user, act, user.get_followers(&*conn));
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.notify(&*conn);
broadcast(&*conn, &user, reshare.into_activity(&*conn), user.get_followers(&*conn));
broadcast(&user, reshare.into_activity(&*conn), user.get_followers(&*conn));
} else {
let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id).unwrap();
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))

View file

@ -9,9 +9,8 @@ use rocket_contrib::Template;
use serde_json;
use activity_pub::{
activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId,
inbox::{Inbox, Notify},
actor::Actor
ActivityStream, broadcast, Id, IntoId,
inbox::{Inbox, Notify}
};
use db_conn::DbConn;
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.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))
}
@ -110,9 +109,9 @@ fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
}
#[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();
user.as_activity_pub(&*conn)
ActivityStream::new(user.into_activity(&*conn))
}
#[get("/users/new")]
@ -199,21 +198,23 @@ fn outbox(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
fn inbox(name: String, conn: DbConn, data: String) -> String {
let user = User::find_local(&*conn, name).unwrap();
let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap();
user.received(&*conn, act);
String::from("")
match user.received(&*conn, act) {
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")]
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 followers = user.get_followers(&*conn).into_iter().map(|f| f.compute_id(&*conn)).collect::<Vec<String>>();
let json = json!({
"@context": context(),
"id": user.compute_box(&*conn, "followers"),
"type": "OrderedCollection",
"totalItems": followers.len(),
"orderedItems": followers
});
activity_pub(json)
let followers = user.get_followers(&*conn).into_iter().map(|f| Id::new(f.ap_url)).collect::<Vec<Id>>();
let mut coll = OrderedCollection::default();
coll.object_props.set_id_string(format!("{}/followers", user.ap_url)).expect("Follower collection: id error");
coll.collection_props.set_total_items_u64(followers.len() as u64).expect("Follower collection: totalItems error");
coll.collection_props.set_items_link_vec(followers).expect("Follower collection: items error");
ActivityStream::new(coll)
}

View file

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

View file

@ -257,7 +257,7 @@ input {
transition: all 0.1s ease-in;
display: block;
width: 100%;
margin: auto;
margin: auto auto 5em;
padding: 0.5em;
box-sizing: border-box;
@ -266,7 +266,7 @@ input {
border: none;
border-bottom: solid #DADADA 2px;
}
input[type="submit"] { margin: 2em auto; }
form input[type="submit"] { margin: 2em auto; }
input:focus {
background: #FAFAFA;
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">
<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">
{% for comment in comments %}
{% if comment.author.display_name %}
@ -75,7 +84,7 @@
<span class="username">@{{ comment.author.username }}</span>
</a>
<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>
{% endfor %}
</div>