Merge branch 'master' of https://github.com/Plume-org/Plume
This commit is contained in:
commit
feff837313
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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)",
|
||||
|
@ -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"
|
||||
|
@ -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
86
doc/PREREQUISITES.md
Normal 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 we’re 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 distribution’s
|
||||
managers.
|
||||
|
||||
Next, you’ll 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
|
3
po/en.po
3
po/en.po
@ -283,3 +283,6 @@ msgstr ""
|
||||
|
||||
msgid "{{ data }} mentioned you."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your comment"
|
||||
msgstr ""
|
||||
|
4
po/fr.po
4
po/fr.po
@ -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"
|
||||
|
4
po/pl.po
4
po/pl.po
@ -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ę"
|
||||
|
@ -278,3 +278,6 @@ msgstr ""
|
||||
|
||||
msgid "{{ data }} mentioned you."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your comment"
|
||||
msgstr ""
|
||||
|
@ -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>;
|
||||
}
|
@ -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) => {
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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> {
|
||||
|
@ -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(¬e.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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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")]
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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")]
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user