Add unit tests for main model parts (#310)
Add tests for following models: - Blog - Instance - Media - User
This commit is contained in:
parent
0b9727ed28
commit
8a4702df92
|
@ -39,7 +39,7 @@ jobs:
|
||||||
name: "Test with potgresql backend"
|
name: "Test with potgresql backend"
|
||||||
env:
|
env:
|
||||||
- MIGRATION_DIR=migrations/postgres FEATURES=postgres DATABASE_URL=postgres://postgres@localhost/plume_tests
|
- MIGRATION_DIR=migrations/postgres FEATURES=postgres DATABASE_URL=postgres://postgres@localhost/plume_tests
|
||||||
- RUSTFLAGS='-C link-dead-code'
|
- RUSTFLAGS='-C link-dead-code' RUST_TEST_THREADS=1
|
||||||
before_script: psql -c 'create database plume_tests;' -U postgres
|
before_script: psql -c 'create database plume_tests;' -U postgres
|
||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
|
@ -49,7 +49,7 @@ jobs:
|
||||||
name: "Test with Sqlite backend"
|
name: "Test with Sqlite backend"
|
||||||
env:
|
env:
|
||||||
- MIGRATION_DIR=migrations/sqlite FEATURES=sqlite DATABASE_URL=plume.sqlite3
|
- MIGRATION_DIR=migrations/sqlite FEATURES=sqlite DATABASE_URL=plume.sqlite3
|
||||||
- RUSTFLAGS='-C link-dead-code'
|
- RUSTFLAGS='-C link-dead-code' RUST_TEST_THREADS=1
|
||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
cargo test --features "${FEATURES}" --no-default-features --all &&
|
cargo test --features "${FEATURES}" --no-default-features --all &&
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use activitypub::{Object, activity::Create};
|
use activitypub::{activity::Create, Object};
|
||||||
|
|
||||||
use activity_pub::Id;
|
use activity_pub::Id;
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ pub enum InboxError {
|
||||||
#[fail(display = "Invalid activity type")]
|
#[fail(display = "Invalid activity type")]
|
||||||
InvalidType,
|
InvalidType,
|
||||||
#[fail(display = "Couldn't undo activity")]
|
#[fail(display = "Couldn't undo activity")]
|
||||||
CantUndo
|
CantUndo,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FromActivity<T: Object, C>: Sized {
|
pub trait FromActivity<T: Object, C>: Sized {
|
||||||
|
@ -17,7 +17,13 @@ pub trait FromActivity<T: Object, C>: Sized {
|
||||||
|
|
||||||
fn try_from_activity(conn: &C, act: Create) -> bool {
|
fn try_from_activity(conn: &C, act: Create) -> bool {
|
||||||
if let Ok(obj) = act.create_props.object_object() {
|
if let Ok(obj) = act.create_props.object_object() {
|
||||||
Self::from_activity(conn, obj, act.create_props.actor_link::<Id>().expect("FromActivity::try_from_activity: id not found error"));
|
Self::from_activity(
|
||||||
|
conn,
|
||||||
|
obj,
|
||||||
|
act.create_props
|
||||||
|
.actor_link::<Id>()
|
||||||
|
.expect("FromActivity::try_from_activity: id not found error"),
|
||||||
|
);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -32,7 +38,6 @@ pub trait Notify<C> {
|
||||||
pub trait Deletable<C, A> {
|
pub trait Deletable<C, A> {
|
||||||
fn delete(&self, conn: &C) -> A;
|
fn delete(&self, conn: &C) -> A;
|
||||||
fn delete_id(id: String, actor_id: String, conn: &C);
|
fn delete_id(id: String, actor_id: String, conn: &C);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WithInbox {
|
pub trait WithInbox {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use activitypub::{Activity, Actor, Object, Link};
|
use activitypub::{Activity, Actor, Link, Object};
|
||||||
use array_tool::vec::Uniq;
|
use array_tool::vec::Uniq;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
Outcome, http::Status,
|
http::Status,
|
||||||
response::{Response, Responder},
|
request::{FromRequest, Request},
|
||||||
request::{FromRequest, Request}
|
response::{Responder, Response},
|
||||||
|
Outcome,
|
||||||
};
|
};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ pub fn ap_accept_header() -> Vec<&'static str> {
|
||||||
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
|
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
|
||||||
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
|
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
"application/ld+json"
|
"application/ld+json",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ pub fn context() -> serde_json::Value {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ActivityStream<T> (T);
|
pub struct ActivityStream<T>(T);
|
||||||
|
|
||||||
impl<T> ActivityStream<T> {
|
impl<T> ActivityStream<T> {
|
||||||
pub fn new(t: T) -> ActivityStream<T> {
|
pub fn new(t: T) -> ActivityStream<T> {
|
||||||
|
@ -64,9 +65,11 @@ impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
|
||||||
fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
|
fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
|
||||||
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
||||||
json["@context"] = context();
|
json["@context"] = context();
|
||||||
serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r)
|
serde_json::to_string(&json).respond_to(request).map(|r| {
|
||||||
|
Response::build_from(r)
|
||||||
.raw_header("Content-Type", "application/activity+json")
|
.raw_header("Content-Type", "application/activity+json")
|
||||||
.finalize())
|
.finalize()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,29 +79,45 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
|
||||||
request.headers().get_one("Accept").map(|header| header.split(",").map(|ct| match ct.trim() {
|
request
|
||||||
|
.headers()
|
||||||
|
.get_one("Accept")
|
||||||
|
.map(|header| {
|
||||||
|
header
|
||||||
|
.split(",")
|
||||||
|
.map(|ct| match ct.trim() {
|
||||||
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
|
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
|
||||||
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" |
|
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
|
||||||
"application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" |
|
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
|
||||||
"application/activity+json" |
|
| "application/activity+json"
|
||||||
"application/ld+json" => Outcome::Success(ApRequest),
|
| "application/ld+json" => Outcome::Success(ApRequest),
|
||||||
"text/html" => Outcome::Forward(true),
|
"text/html" => Outcome::Forward(true),
|
||||||
_ => Outcome::Forward(false)
|
_ => Outcome::Forward(false),
|
||||||
}).fold(Outcome::Forward(false), |out, ct| if out.clone().forwarded().unwrap_or(out.is_success()) {
|
})
|
||||||
|
.fold(Outcome::Forward(false), |out, ct| {
|
||||||
|
if out.clone().forwarded().unwrap_or(out.is_success()) {
|
||||||
out
|
out
|
||||||
} else {
|
} else {
|
||||||
ct
|
ct
|
||||||
}).map_forward(|_| ())).unwrap_or(Outcome::Forward(()))
|
}
|
||||||
|
})
|
||||||
|
.map_forward(|_| ())
|
||||||
|
})
|
||||||
|
.unwrap_or(Outcome::Forward(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(sender: &S, act: A, to: Vec<T>) {
|
pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(
|
||||||
let boxes = to.into_iter()
|
sender: &S,
|
||||||
|
act: A,
|
||||||
|
to: Vec<T>,
|
||||||
|
) {
|
||||||
|
let boxes = to
|
||||||
|
.into_iter()
|
||||||
.filter(|u| !u.is_local())
|
.filter(|u| !u.is_local())
|
||||||
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
|
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.unique();
|
.unique();
|
||||||
|
|
||||||
|
|
||||||
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
|
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
|
||||||
act["@context"] = context();
|
act["@context"] = context();
|
||||||
let signed = act.sign(sender);
|
let signed = act.sign(sender);
|
||||||
|
@ -121,8 +140,8 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(send
|
||||||
} else {
|
} else {
|
||||||
println!("Error while reading response")
|
println!("Error while reading response")
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => println!("Error while sending to inbox ({:?})", e)
|
Err(e) => println!("Error while sending to inbox ({:?})", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +171,7 @@ impl Link for Id {}
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ApSignature {
|
pub struct ApSignature {
|
||||||
#[activitystreams(concrete(PublicKey), functional)]
|
#[activitystreams(concrete(PublicKey), functional)]
|
||||||
pub public_key: Option<serde_json::Value>
|
pub public_key: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
|
||||||
|
@ -165,7 +184,7 @@ pub struct PublicKey {
|
||||||
pub owner: Option<serde_json::Value>,
|
pub owner: Option<serde_json::Value>,
|
||||||
|
|
||||||
#[activitystreams(concrete(String), functional)]
|
#[activitystreams(concrete(String), functional)]
|
||||||
pub public_key_pem: Option<serde_json::Value>
|
pub public_key_pem: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, UnitString)]
|
#[derive(Clone, Debug, Default, UnitString)]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use base64;
|
use base64;
|
||||||
use chrono::{DateTime, offset::Utc};
|
use chrono::{offset::Utc, DateTime};
|
||||||
use openssl::hash::{Hasher, MessageDigest};
|
use openssl::hash::{Hasher, MessageDigest};
|
||||||
use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT};
|
use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
@ -14,35 +14,52 @@ pub struct Digest(String);
|
||||||
|
|
||||||
impl Digest {
|
impl Digest {
|
||||||
pub fn digest(body: String) -> HeaderValue {
|
pub fn digest(body: String) -> HeaderValue {
|
||||||
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
let mut hasher =
|
||||||
hasher.update(&body.into_bytes()[..]).expect("Digest::digest: content insertion error");
|
Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
||||||
|
hasher
|
||||||
|
.update(&body.into_bytes()[..])
|
||||||
|
.expect("Digest::digest: content insertion error");
|
||||||
let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error"));
|
let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error"));
|
||||||
HeaderValue::from_str(&format!("SHA-256={}", res)).expect("Digest::digest: header creation error")
|
HeaderValue::from_str(&format!("SHA-256={}", res))
|
||||||
|
.expect("Digest::digest: header creation error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify(&self, body: String) -> bool {
|
pub fn verify(&self, body: String) -> bool {
|
||||||
if self.algorithm()=="SHA-256" {
|
if self.algorithm() == "SHA-256" {
|
||||||
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
let mut hasher =
|
||||||
hasher.update(&body.into_bytes()).expect("Digest::digest: content insertion error");
|
Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
||||||
self.value().deref()==hasher.finish().expect("Digest::digest: finalizing error").deref()
|
hasher
|
||||||
|
.update(&body.into_bytes())
|
||||||
|
.expect("Digest::digest: content insertion error");
|
||||||
|
self.value().deref()
|
||||||
|
== hasher
|
||||||
|
.finish()
|
||||||
|
.expect("Digest::digest: finalizing error")
|
||||||
|
.deref()
|
||||||
} else {
|
} else {
|
||||||
false //algorithm not supported
|
false //algorithm not supported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn algorithm(&self) -> &str {
|
pub fn algorithm(&self) -> &str {
|
||||||
let pos = self.0.find('=').expect("Digest::algorithm: invalid header error");
|
let pos = self
|
||||||
|
.0
|
||||||
|
.find('=')
|
||||||
|
.expect("Digest::algorithm: invalid header error");
|
||||||
&self.0[..pos]
|
&self.0[..pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(&self) -> Vec<u8> {
|
pub fn value(&self) -> Vec<u8> {
|
||||||
let pos = self.0.find('=').expect("Digest::value: invalid header error")+1;
|
let pos = self
|
||||||
|
.0
|
||||||
|
.find('=')
|
||||||
|
.expect("Digest::value: invalid header error") + 1;
|
||||||
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
|
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_header(dig: &str) -> Result<Self, ()> {
|
pub fn from_header(dig: &str) -> Result<Self, ()> {
|
||||||
if let Some(pos) = dig.find('=') {
|
if let Some(pos) = dig.find('=') {
|
||||||
let pos = pos+1;
|
let pos = pos + 1;
|
||||||
if let Ok(_) = base64::decode(&dig[pos..]) {
|
if let Ok(_) = base64::decode(&dig[pos..]) {
|
||||||
Ok(Digest(dig.to_owned()))
|
Ok(Digest(dig.to_owned()))
|
||||||
} else {
|
} else {
|
||||||
|
@ -60,15 +77,42 @@ pub fn headers() -> HeaderMap {
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT));
|
headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT));
|
||||||
headers.insert(DATE, HeaderValue::from_str(&date).expect("request::headers: date error"));
|
headers.insert(
|
||||||
headers.insert(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::<Vec<_>>().join(", ")).expect("request::headers: accept error"));
|
DATE,
|
||||||
|
HeaderValue::from_str(&date).expect("request::headers: date error"),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
ACCEPT,
|
||||||
|
HeaderValue::from_str(
|
||||||
|
&ap_accept_header()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", "),
|
||||||
|
).expect("request::headers: accept error"),
|
||||||
|
);
|
||||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE));
|
headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE));
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn signature<S: Signer>(signer: &S, headers: HeaderMap) -> HeaderValue {
|
pub fn signature<S: Signer>(signer: &S, headers: HeaderMap) -> HeaderValue {
|
||||||
let signed_string = headers.iter().map(|(h,v)| format!("{}: {}", h.as_str().to_lowercase(), v.to_str().expect("request::signature: invalid header error"))).collect::<Vec<String>>().join("\n");
|
let signed_string = headers
|
||||||
let signed_headers = headers.iter().map(|(h,_)| h.as_str()).collect::<Vec<&str>>().join(" ").to_lowercase();
|
.iter()
|
||||||
|
.map(|(h, v)| {
|
||||||
|
format!(
|
||||||
|
"{}: {}",
|
||||||
|
h.as_str().to_lowercase(),
|
||||||
|
v.to_str()
|
||||||
|
.expect("request::signature: invalid header error")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n");
|
||||||
|
let signed_headers = headers
|
||||||
|
.iter()
|
||||||
|
.map(|(h, _)| h.as_str())
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" ")
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
let data = signer.sign(signed_string);
|
let data = signer.sign(signed_string);
|
||||||
let sign = base64::encode(&data[..]);
|
let sign = base64::encode(&data[..]);
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
|
use super::request;
|
||||||
use base64;
|
use base64;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use hex;
|
use hex;
|
||||||
use openssl::{
|
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
||||||
pkey::PKey,
|
|
||||||
rsa::Rsa,
|
|
||||||
sha::sha256
|
|
||||||
};
|
|
||||||
use super::request;
|
|
||||||
use rocket::http::HeaderMap;
|
use rocket::http::HeaderMap;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
@ -15,8 +11,12 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
||||||
let keypair = Rsa::generate(2048).expect("sign::gen_keypair: key generation error");
|
let keypair = Rsa::generate(2048).expect("sign::gen_keypair: key generation error");
|
||||||
let keypair = PKey::from_rsa(keypair).expect("sign::gen_keypair: parsing error");
|
let keypair = PKey::from_rsa(keypair).expect("sign::gen_keypair: parsing error");
|
||||||
(
|
(
|
||||||
keypair.public_key_to_pem().expect("sign::gen_keypair: public key encoding error"),
|
keypair
|
||||||
keypair.private_key_to_pem_pkcs8().expect("sign::gen_keypair: private key encoding error")
|
.public_key_to_pem()
|
||||||
|
.expect("sign::gen_keypair: public key encoding error"),
|
||||||
|
keypair
|
||||||
|
.private_key_to_pem_pkcs8()
|
||||||
|
.expect("sign::gen_keypair: private key encoding error"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,8 +30,12 @@ pub trait Signer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Signable {
|
pub trait Signable {
|
||||||
fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer;
|
fn sign<T>(&mut self, creator: &T) -> &mut Self
|
||||||
fn verify<T>(self, creator: &T) -> bool where T: Signer;
|
where
|
||||||
|
T: Signer;
|
||||||
|
fn verify<T>(self, creator: &T) -> bool
|
||||||
|
where
|
||||||
|
T: Signer;
|
||||||
|
|
||||||
fn hash(data: String) -> String {
|
fn hash(data: String) -> String {
|
||||||
let bytes = data.into_bytes();
|
let bytes = data.into_bytes();
|
||||||
|
@ -48,10 +52,12 @@ impl Signable for serde_json::Value {
|
||||||
"created": creation_date
|
"created": creation_date
|
||||||
});
|
});
|
||||||
|
|
||||||
let options_hash = Self::hash(json!({
|
let options_hash = Self::hash(
|
||||||
|
json!({
|
||||||
"@context": "https://w3id.org/identity/v1",
|
"@context": "https://w3id.org/identity/v1",
|
||||||
"created": creation_date
|
"created": creation_date
|
||||||
}).to_string());
|
}).to_string(),
|
||||||
|
);
|
||||||
let document_hash = Self::hash(self.to_string());
|
let document_hash = Self::hash(self.to_string());
|
||||||
let to_be_signed = options_hash + &document_hash;
|
let to_be_signed = options_hash + &document_hash;
|
||||||
|
|
||||||
|
@ -63,29 +69,34 @@ impl Signable for serde_json::Value {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify<T: Signer>(mut self, creator: &T) -> bool {
|
fn verify<T: Signer>(mut self, creator: &T) -> bool {
|
||||||
let signature_obj = if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) {
|
let signature_obj =
|
||||||
|
if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) {
|
||||||
sig
|
sig
|
||||||
} else {
|
} else {
|
||||||
//signature not present
|
//signature not present
|
||||||
return false
|
return false;
|
||||||
};
|
};
|
||||||
let signature = if let Ok(sig) = base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) {
|
let signature = if let Ok(sig) =
|
||||||
|
base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or(""))
|
||||||
|
{
|
||||||
sig
|
sig
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false;
|
||||||
};
|
};
|
||||||
let creation_date = &signature_obj["created"];
|
let creation_date = &signature_obj["created"];
|
||||||
let options_hash = Self::hash(json!({
|
let options_hash = Self::hash(
|
||||||
|
json!({
|
||||||
"@context": "https://w3id.org/identity/v1",
|
"@context": "https://w3id.org/identity/v1",
|
||||||
"created": creation_date
|
"created": creation_date
|
||||||
}).to_string());
|
}).to_string(),
|
||||||
|
);
|
||||||
let document_hash = Self::hash(self.to_string());
|
let document_hash = Self::hash(self.to_string());
|
||||||
let to_be_signed = options_hash + &document_hash;
|
let to_be_signed = options_hash + &document_hash;
|
||||||
creator.verify(to_be_signed, signature)
|
creator.verify(to_be_signed, signature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug,Copy,Clone,PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum SignatureValidity {
|
pub enum SignatureValidity {
|
||||||
Invalid,
|
Invalid,
|
||||||
ValidNoDigest,
|
ValidNoDigest,
|
||||||
|
@ -95,14 +106,18 @@ pub enum SignatureValidity {
|
||||||
|
|
||||||
impl SignatureValidity {
|
impl SignatureValidity {
|
||||||
pub fn is_secure(&self) -> bool {
|
pub fn is_secure(&self) -> bool {
|
||||||
self==&SignatureValidity::Valid
|
self == &SignatureValidity::Valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_http_headers<S: Signer+::std::fmt::Debug>(sender: &S, all_headers: HeaderMap, data: String) -> SignatureValidity{
|
pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
|
||||||
|
sender: &S,
|
||||||
|
all_headers: HeaderMap,
|
||||||
|
data: String,
|
||||||
|
) -> SignatureValidity {
|
||||||
let sig_header = all_headers.get_one("Signature");
|
let sig_header = all_headers.get_one("Signature");
|
||||||
if sig_header.is_none() {
|
if sig_header.is_none() {
|
||||||
return SignatureValidity::Absent
|
return SignatureValidity::Absent;
|
||||||
}
|
}
|
||||||
let sig_header = sig_header.expect("sign::verify_http_headers: unreachable");
|
let sig_header = sig_header.expect("sign::verify_http_headers: unreachable");
|
||||||
|
|
||||||
|
@ -112,35 +127,43 @@ pub fn verify_http_headers<S: Signer+::std::fmt::Debug>(sender: &S, all_headers:
|
||||||
let mut signature = None;
|
let mut signature = None;
|
||||||
for part in sig_header.split(',') {
|
for part in sig_header.split(',') {
|
||||||
match part {
|
match part {
|
||||||
part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len()-1]),
|
part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len() - 1]),
|
||||||
part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len()-1]),
|
part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len() - 1]),
|
||||||
part if part.starts_with("headers=") => headers = Some(&part[9..part.len()-1]),
|
part if part.starts_with("headers=") => headers = Some(&part[9..part.len() - 1]),
|
||||||
part if part.starts_with("signature=") => signature = Some(&part[11..part.len()-1]),
|
part if part.starts_with("signature=") => signature = Some(&part[11..part.len() - 1]),
|
||||||
_ => {},
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if signature.is_none() || headers.is_none() {//missing part of the header
|
if signature.is_none() || headers.is_none() {
|
||||||
return SignatureValidity::Invalid
|
//missing part of the header
|
||||||
|
return SignatureValidity::Invalid;
|
||||||
}
|
}
|
||||||
let headers = headers.expect("sign::verify_http_headers: unreachable").split_whitespace().collect::<Vec<_>>();
|
let headers = headers
|
||||||
|
.expect("sign::verify_http_headers: unreachable")
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let signature = signature.expect("sign::verify_http_headers: unreachable");
|
let signature = signature.expect("sign::verify_http_headers: unreachable");
|
||||||
let h = headers.iter()
|
let h = headers
|
||||||
.map(|header| (header,all_headers.get_one(header)))
|
.iter()
|
||||||
|
.map(|header| (header, all_headers.get_one(header)))
|
||||||
.map(|(header, value)| format!("{}: {}", header.to_lowercase(), value.unwrap_or("")))
|
.map(|(header, value)| format!("{}: {}", header.to_lowercase(), value.unwrap_or("")))
|
||||||
.collect::<Vec<_>>().join("\n");
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
if !sender.verify(h, base64::decode(signature).unwrap_or(Vec::new())) {
|
if !sender.verify(h, base64::decode(signature).unwrap_or(Vec::new())) {
|
||||||
return SignatureValidity::Invalid
|
return SignatureValidity::Invalid;
|
||||||
}
|
}
|
||||||
if !headers.contains(&"digest") {// signature is valid, but body content is not verified
|
if !headers.contains(&"digest") {
|
||||||
return SignatureValidity::ValidNoDigest
|
// signature is valid, but body content is not verified
|
||||||
|
return SignatureValidity::ValidNoDigest;
|
||||||
}
|
}
|
||||||
let digest = all_headers.get_one("digest").unwrap_or("");
|
let digest = all_headers.get_one("digest").unwrap_or("");
|
||||||
let digest = request::Digest::from_header(digest);
|
let digest = request::Digest::from_header(digest);
|
||||||
if !digest.map(|d| d.verify(data)).unwrap_or(false) {// signature was valid, but body content does not match its digest
|
if !digest.map(|d| d.verify(data)).unwrap_or(false) {
|
||||||
|
// signature was valid, but body content does not match its digest
|
||||||
SignatureValidity::Invalid
|
SignatureValidity::Invalid
|
||||||
} else {
|
} else {
|
||||||
SignatureValidity::Valid// all check passed
|
SignatureValidity::Valid // all check passed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use rocket::{Outcome, http::Status, request::{self, FromRequest, Request}};
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
request::{self, FromRequest, Request},
|
||||||
|
Outcome,
|
||||||
|
};
|
||||||
|
|
||||||
use users::User;
|
use users::User;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
Outcome,
|
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{self, FromRequest, Request}
|
request::{self, FromRequest, Request},
|
||||||
|
Outcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
use db_conn::DbConn;
|
use db_conn::DbConn;
|
||||||
|
@ -48,7 +48,7 @@ impl ApiToken {
|
||||||
let full_scope = what.to_owned() + ":" + scope;
|
let full_scope = what.to_owned() + ":" + scope;
|
||||||
for s in self.scopes.split('+') {
|
for s in self.scopes.split('+') {
|
||||||
if s == what || s == full_scope {
|
if s == what || s == full_scope {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use canapi::{Error, Provider};
|
use canapi::{Error, Provider};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use plume_api::apps::AppEndpoint;
|
use plume_api::apps::AppEndpoint;
|
||||||
use plume_common::utils::random_hex;
|
use plume_common::utils::random_hex;
|
||||||
use Connection;
|
|
||||||
use schema::apps;
|
use schema::apps;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
#[derive(Clone, Queryable)]
|
#[derive(Clone, Queryable)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
@ -19,7 +19,7 @@ pub struct App {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
#[table_name= "apps"]
|
#[table_name = "apps"]
|
||||||
pub struct NewApp {
|
pub struct NewApp {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
|
@ -43,13 +43,16 @@ impl Provider<Connection> for App {
|
||||||
let client_id = random_hex();
|
let client_id = random_hex();
|
||||||
|
|
||||||
let client_secret = random_hex();
|
let client_secret = random_hex();
|
||||||
let app = App::insert(conn, NewApp {
|
let app = App::insert(
|
||||||
|
conn,
|
||||||
|
NewApp {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
client_id: client_id,
|
client_id: client_id,
|
||||||
client_secret: client_secret,
|
client_secret: client_secret,
|
||||||
redirect_uri: data.redirect_uri,
|
redirect_uri: data.redirect_uri,
|
||||||
website: data.website,
|
website: data.website,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Ok(AppEndpoint {
|
Ok(AppEndpoint {
|
||||||
id: Some(app.id),
|
id: Some(app.id),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use schema::blog_authors;
|
use schema::blog_authors;
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
use activitypub::{Actor, Object, CustomObject, actor::Group, collection::OrderedCollection};
|
use activitypub::{actor::Group, collection::OrderedCollection, Actor, CustomObject, Object};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use reqwest::{Client,
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
header::{ACCEPT, HeaderValue}
|
|
||||||
};
|
|
||||||
use serde_json;
|
|
||||||
use url::Url;
|
|
||||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
|
||||||
use openssl::{
|
use openssl::{
|
||||||
hash::MessageDigest,
|
hash::MessageDigest,
|
||||||
pkey::{PKey, Private},
|
pkey::{PKey, Private},
|
||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sign::{Signer,Verifier}
|
sign::{Signer, Verifier},
|
||||||
};
|
};
|
||||||
|
use reqwest::{
|
||||||
|
header::{HeaderValue, ACCEPT},
|
||||||
|
Client,
|
||||||
|
};
|
||||||
|
use serde_json;
|
||||||
|
use url::Url;
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
use {BASE_URL, USE_HTTPS, Connection};
|
|
||||||
use plume_common::activity_pub::{
|
|
||||||
ap_accept_header, ApSignature, ActivityStream, Id, IntoId, PublicKey,
|
|
||||||
inbox::{Deletable, WithInbox},
|
|
||||||
sign
|
|
||||||
};
|
|
||||||
use safe_string::SafeString;
|
|
||||||
use instance::*;
|
use instance::*;
|
||||||
|
use plume_common::activity_pub::{
|
||||||
|
ap_accept_header,
|
||||||
|
inbox::{Deletable, WithInbox},
|
||||||
|
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey,
|
||||||
|
};
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
|
use safe_string::SafeString;
|
||||||
use schema::blogs;
|
use schema::blogs;
|
||||||
use users::User;
|
use users::User;
|
||||||
|
use {Connection, BASE_URL, USE_HTTPS};
|
||||||
|
|
||||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ pub struct Blog {
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub ap_url: String,
|
pub ap_url: String,
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: String
|
pub public_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
|
@ -54,7 +55,7 @@ pub struct NewBlog {
|
||||||
pub instance_id: i32,
|
pub instance_id: i32,
|
||||||
pub ap_url: String,
|
pub ap_url: String,
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: String
|
pub public_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLOG_PREFIX: &'static str = "~";
|
const BLOG_PREFIX: &'static str = "~";
|
||||||
|
@ -72,16 +73,22 @@ impl Blog {
|
||||||
pub fn list_authors(&self, conn: &Connection) -> Vec<User> {
|
pub fn list_authors(&self, conn: &Connection) -> Vec<User> {
|
||||||
use schema::blog_authors;
|
use schema::blog_authors;
|
||||||
use schema::users;
|
use schema::users;
|
||||||
let authors_ids = blog_authors::table.filter(blog_authors::blog_id.eq(self.id)).select(blog_authors::author_id);
|
let authors_ids = blog_authors::table
|
||||||
users::table.filter(users::id.eq_any(authors_ids))
|
.filter(blog_authors::blog_id.eq(self.id))
|
||||||
|
.select(blog_authors::author_id);
|
||||||
|
users::table
|
||||||
|
.filter(users::id.eq_any(authors_ids))
|
||||||
.load::<User>(conn)
|
.load::<User>(conn)
|
||||||
.expect("Blog::list_authors: author loading error")
|
.expect("Blog::list_authors: author loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_for_author(conn: &Connection, author_id: i32) -> Vec<Blog> {
|
pub fn find_for_author(conn: &Connection, author: &User) -> Vec<Blog> {
|
||||||
use schema::blog_authors;
|
use schema::blog_authors;
|
||||||
let author_ids = blog_authors::table.filter(blog_authors::author_id.eq(author_id)).select(blog_authors::blog_id);
|
let author_ids = blog_authors::table
|
||||||
blogs::table.filter(blogs::id.eq_any(author_ids))
|
.filter(blog_authors::author_id.eq(author.id))
|
||||||
|
.select(blog_authors::blog_id);
|
||||||
|
blogs::table
|
||||||
|
.filter(blogs::id.eq_any(author_ids))
|
||||||
.load::<Blog>(conn)
|
.load::<Blog>(conn)
|
||||||
.expect("Blog::find_for_author: blog loading error")
|
.expect("Blog::find_for_author: blog loading error")
|
||||||
}
|
}
|
||||||
|
@ -91,24 +98,49 @@ impl Blog {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_fqn(conn: &Connection, fqn: String) -> Option<Blog> {
|
pub fn find_by_fqn(conn: &Connection, fqn: String) -> Option<Blog> {
|
||||||
if fqn.contains("@") { // remote blog
|
if fqn.contains("@") {
|
||||||
match Instance::find_by_domain(conn, String::from(fqn.split("@").last().expect("Blog::find_by_fqn: unreachable"))) {
|
// remote blog
|
||||||
Some(instance) => {
|
match Instance::find_by_domain(
|
||||||
match Blog::find_by_name(conn, String::from(fqn.split("@").nth(0).expect("Blog::find_by_fqn: unreachable")), instance.id) {
|
conn,
|
||||||
|
String::from(
|
||||||
|
fqn.split("@")
|
||||||
|
.last()
|
||||||
|
.expect("Blog::find_by_fqn: unreachable"),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Some(instance) => match Blog::find_by_name(
|
||||||
|
conn,
|
||||||
|
String::from(
|
||||||
|
fqn.split("@")
|
||||||
|
.nth(0)
|
||||||
|
.expect("Blog::find_by_fqn: unreachable"),
|
||||||
|
),
|
||||||
|
instance.id,
|
||||||
|
) {
|
||||||
Some(u) => Some(u),
|
Some(u) => Some(u),
|
||||||
None => Blog::fetch_from_webfinger(conn, fqn)
|
None => Blog::fetch_from_webfinger(conn, fqn),
|
||||||
}
|
|
||||||
},
|
},
|
||||||
None => Blog::fetch_from_webfinger(conn, fqn)
|
None => Blog::fetch_from_webfinger(conn, fqn),
|
||||||
}
|
}
|
||||||
} else { // local blog
|
} else {
|
||||||
|
// local blog
|
||||||
Blog::find_local(conn, fqn)
|
Blog::find_local(conn, fqn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option<Blog> {
|
fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option<Blog> {
|
||||||
match resolve(acct.clone(), *USE_HTTPS) {
|
match resolve(acct.clone(), *USE_HTTPS) {
|
||||||
Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| Blog::fetch_from_url(conn, l.href.expect("Blog::fetch_from_webfinger: href not found error"))),
|
Ok(wf) => wf
|
||||||
|
.links
|
||||||
|
.into_iter()
|
||||||
|
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||||
|
.and_then(|l| {
|
||||||
|
Blog::fetch_from_url(
|
||||||
|
conn,
|
||||||
|
l.href
|
||||||
|
.expect("Blog::fetch_from_webfinger: href not found error"),
|
||||||
|
)
|
||||||
|
}),
|
||||||
Err(details) => {
|
Err(details) => {
|
||||||
println!("{:?}", details);
|
println!("{:?}", details);
|
||||||
None
|
None
|
||||||
|
@ -119,17 +151,37 @@ impl Blog {
|
||||||
fn fetch_from_url(conn: &Connection, url: String) -> Option<Blog> {
|
fn fetch_from_url(conn: &Connection, url: String) -> Option<Blog> {
|
||||||
let req = Client::new()
|
let req = Client::new()
|
||||||
.get(&url[..])
|
.get(&url[..])
|
||||||
.header(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::<Vec<_>>().join(", ")).expect("Blog::fetch_from_url: accept_header generation error"))
|
.header(
|
||||||
|
ACCEPT,
|
||||||
|
HeaderValue::from_str(
|
||||||
|
&ap_accept_header()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", "),
|
||||||
|
).expect("Blog::fetch_from_url: accept_header generation error"),
|
||||||
|
)
|
||||||
.send();
|
.send();
|
||||||
match req {
|
match req {
|
||||||
Ok(mut res) => {
|
Ok(mut res) => {
|
||||||
let text = &res.text().expect("Blog::fetch_from_url: body reading error");
|
let text = &res
|
||||||
let ap_sign: ApSignature = serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error");
|
.text()
|
||||||
let mut json: CustomGroup = serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error");
|
.expect("Blog::fetch_from_url: body reading error");
|
||||||
|
let ap_sign: ApSignature =
|
||||||
|
serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error");
|
||||||
|
let mut json: CustomGroup =
|
||||||
|
serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error");
|
||||||
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
|
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
|
||||||
Some(Blog::from_activity(conn, json, Url::parse(url.as_ref()).expect("Blog::fetch_from_url: url parsing error").host_str().expect("Blog::fetch_from_url: host extraction error").to_string()))
|
Some(Blog::from_activity(
|
||||||
},
|
conn,
|
||||||
Err(_) => None
|
json,
|
||||||
|
Url::parse(url.as_ref())
|
||||||
|
.expect("Blog::fetch_from_url: url parsing error")
|
||||||
|
.host_str()
|
||||||
|
.expect("Blog::fetch_from_url: host extraction error")
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +189,9 @@ impl Blog {
|
||||||
let instance = match Instance::find_by_domain(conn, inst.clone()) {
|
let instance = match Instance::find_by_domain(conn, inst.clone()) {
|
||||||
Some(instance) => instance,
|
Some(instance) => instance,
|
||||||
None => {
|
None => {
|
||||||
Instance::insert(conn, NewInstance {
|
Instance::insert(
|
||||||
|
conn,
|
||||||
|
NewInstance {
|
||||||
public_domain: inst.clone(),
|
public_domain: inst.clone(),
|
||||||
name: inst.clone(),
|
name: inst.clone(),
|
||||||
local: false,
|
local: false,
|
||||||
|
@ -147,39 +201,91 @@ impl Blog {
|
||||||
default_license: String::new(),
|
default_license: String::new(),
|
||||||
open_registrations: true,
|
open_registrations: true,
|
||||||
short_description_html: String::new(),
|
short_description_html: String::new(),
|
||||||
long_description_html: String::new()
|
long_description_html: String::new(),
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Blog::insert(conn, NewBlog {
|
Blog::insert(
|
||||||
actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"),
|
conn,
|
||||||
title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"),
|
NewBlog {
|
||||||
outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"),
|
actor_id: acct
|
||||||
inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"),
|
.object
|
||||||
summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"),
|
.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,
|
instance_id: instance.id,
|
||||||
ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"),
|
ap_url: acct
|
||||||
public_key: acct.custom_props.public_key_publickey().expect("Blog::from_activity: publicKey error")
|
.object
|
||||||
.public_key_pem_string().expect("Blog::from_activity: publicKey.publicKeyPem error"),
|
.object_props
|
||||||
private_key: None
|
.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: &Connection) -> CustomGroup {
|
pub fn into_activity(&self, _conn: &Connection) -> CustomGroup {
|
||||||
let mut blog = Group::default();
|
let mut blog = Group::default();
|
||||||
blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error");
|
blog.ap_actor_props
|
||||||
blog.object_props.set_name_string(self.title.clone()).expect("Blog::into_activity: name error");
|
.set_preferred_username_string(self.actor_id.clone())
|
||||||
blog.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("Blog::into_activity: outbox error");
|
.expect("Blog::into_activity: preferredUsername error");
|
||||||
blog.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("Blog::into_activity: inbox error");
|
blog.object_props
|
||||||
blog.object_props.set_summary_string(self.summary.clone()).expect("Blog::into_activity: summary error");
|
.set_name_string(self.title.clone())
|
||||||
blog.object_props.set_id_string(self.ap_url.clone()).expect("Blog::into_activity: id error");
|
.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();
|
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
|
||||||
public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error");
|
.set_id_string(format!("{}#main-key", self.ap_url))
|
||||||
public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error");
|
.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();
|
let mut ap_signature = ApSignature::default();
|
||||||
ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error");
|
ap_signature
|
||||||
|
.set_public_key_publickey(public_key)
|
||||||
|
.expect("Blog::into_activity: publicKey error");
|
||||||
|
|
||||||
CustomGroup::new(blog, ap_signature)
|
CustomGroup::new(blog, ap_signature)
|
||||||
}
|
}
|
||||||
|
@ -188,27 +294,41 @@ impl Blog {
|
||||||
let instance = self.get_instance(conn);
|
let instance = self.get_instance(conn);
|
||||||
if self.outbox_url.len() == 0 {
|
if self.outbox_url.len() == 0 {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox")))
|
.set(blogs::outbox_url.eq(instance.compute_box(
|
||||||
.execute(conn).expect("Blog::update_boxes: outbox update error");
|
BLOG_PREFIX,
|
||||||
|
self.actor_id.clone(),
|
||||||
|
"outbox",
|
||||||
|
)))
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Blog::update_boxes: outbox update error");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.inbox_url.len() == 0 {
|
if self.inbox_url.len() == 0 {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox")))
|
.set(blogs::inbox_url.eq(instance.compute_box(
|
||||||
.execute(conn).expect("Blog::update_boxes: inbox update error");
|
BLOG_PREFIX,
|
||||||
|
self.actor_id.clone(),
|
||||||
|
"inbox",
|
||||||
|
)))
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Blog::update_boxes: inbox update error");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.ap_url.len() == 0 {
|
if self.ap_url.len() == 0 {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "")))
|
.set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "")))
|
||||||
.execute(conn).expect("Blog::update_boxes: ap_url update error");
|
.execute(conn)
|
||||||
|
.expect("Blog::update_boxes: ap_url update error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn outbox(&self, conn: &Connection) -> ActivityStream<OrderedCollection> {
|
pub fn outbox(&self, conn: &Connection) -> ActivityStream<OrderedCollection> {
|
||||||
let mut coll = OrderedCollection::default();
|
let mut coll = OrderedCollection::default();
|
||||||
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)).expect("Blog::outbox: activity serialization error");
|
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))
|
||||||
coll.collection_props.set_total_items_u64(self.get_activities(conn).len() as u64).expect("Blog::outbox: count serialization error");
|
.expect("Blog::outbox: activity serialization error");
|
||||||
|
coll.collection_props
|
||||||
|
.set_total_items_u64(self.get_activities(conn).len() as u64)
|
||||||
|
.expect("Blog::outbox: count serialization error");
|
||||||
ActivityStream::new(coll)
|
ActivityStream::new(coll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,35 +337,48 @@ impl Blog {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_keypair(&self) -> PKey<Private> {
|
pub fn get_keypair(&self) -> PKey<Private> {
|
||||||
PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().expect("Blog::get_keypair: private key not found error").as_ref())
|
PKey::from_rsa(
|
||||||
.expect("Blog::get_keypair: pem parsing error"))
|
Rsa::private_key_from_pem(
|
||||||
.expect("Blog::get_keypair: private key deserialization error")
|
self.private_key
|
||||||
|
.clone()
|
||||||
|
.expect("Blog::get_keypair: private key not found error")
|
||||||
|
.as_ref(),
|
||||||
|
).expect("Blog::get_keypair: pem parsing error"),
|
||||||
|
).expect("Blog::get_keypair: private key deserialization error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn webfinger(&self, conn: &Connection) -> Webfinger {
|
pub fn webfinger(&self, conn: &Connection) -> Webfinger {
|
||||||
Webfinger {
|
Webfinger {
|
||||||
subject: format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain),
|
subject: format!(
|
||||||
|
"acct:{}@{}",
|
||||||
|
self.actor_id,
|
||||||
|
self.get_instance(conn).public_domain
|
||||||
|
),
|
||||||
aliases: vec![self.ap_url.clone()],
|
aliases: vec![self.ap_url.clone()],
|
||||||
links: vec![
|
links: vec![
|
||||||
Link {
|
Link {
|
||||||
rel: String::from("http://webfinger.net/rel/profile-page"),
|
rel: String::from("http://webfinger.net/rel/profile-page"),
|
||||||
mime_type: None,
|
mime_type: None,
|
||||||
href: Some(self.ap_url.clone()),
|
href: Some(self.ap_url.clone()),
|
||||||
template: None
|
template: None,
|
||||||
},
|
},
|
||||||
Link {
|
Link {
|
||||||
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
|
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
|
||||||
mime_type: Some(String::from("application/atom+xml")),
|
mime_type: Some(String::from("application/atom+xml")),
|
||||||
href: Some(self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")),
|
href: Some(self.get_instance(conn).compute_box(
|
||||||
template: None
|
BLOG_PREFIX,
|
||||||
|
self.actor_id.clone(),
|
||||||
|
"feed.atom",
|
||||||
|
)),
|
||||||
|
template: None,
|
||||||
},
|
},
|
||||||
Link {
|
Link {
|
||||||
rel: String::from("self"),
|
rel: String::from("self"),
|
||||||
mime_type: Some(String::from("application/activity+json")),
|
mime_type: Some(String::from("application/activity+json")),
|
||||||
href: Some(self.ap_url.clone()),
|
href: Some(self.ap_url.clone()),
|
||||||
template: None
|
template: None,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +386,11 @@ impl Blog {
|
||||||
Blog::find_by_ap_url(conn, url.clone()).or_else(|| {
|
Blog::find_by_ap_url(conn, url.clone()).or_else(|| {
|
||||||
// The requested blog was not in the DB
|
// The requested blog was not in the DB
|
||||||
// We try to fetch it if it is remote
|
// We try to fetch it if it is remote
|
||||||
if Url::parse(url.as_ref()).expect("Blog::from_url: ap_url parsing error").host_str().expect("Blog::from_url: host extraction error") != BASE_URL.as_str() {
|
if Url::parse(url.as_ref())
|
||||||
|
.expect("Blog::from_url: ap_url parsing error")
|
||||||
|
.host_str()
|
||||||
|
.expect("Blog::from_url: host extraction error") != BASE_URL.as_str()
|
||||||
|
{
|
||||||
Blog::fetch_from_url(conn, url)
|
Blog::fetch_from_url(conn, url)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -265,7 +402,11 @@ impl Blog {
|
||||||
if self.instance_id == Instance::local_id(conn) {
|
if self.instance_id == Instance::local_id(conn) {
|
||||||
self.actor_id.clone()
|
self.actor_id.clone()
|
||||||
} else {
|
} else {
|
||||||
format!("{}@{}", self.actor_id, self.get_instance(conn).public_domain)
|
format!(
|
||||||
|
"{}@{}",
|
||||||
|
self.actor_id,
|
||||||
|
self.get_instance(conn).public_domain
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +420,9 @@ impl Blog {
|
||||||
for post in Post::get_for_blog(conn, &self) {
|
for post in Post::get_for_blog(conn, &self) {
|
||||||
post.delete(conn);
|
post.delete(conn);
|
||||||
}
|
}
|
||||||
diesel::delete(self).execute(conn).expect("Blog::delete: blog deletion error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Blog::delete: blog deletion error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,17 +456,29 @@ impl sign::Signer for Blog {
|
||||||
|
|
||||||
fn sign(&self, to_sign: String) -> Vec<u8> {
|
fn sign(&self, to_sign: String) -> Vec<u8> {
|
||||||
let key = self.get_keypair();
|
let key = self.get_keypair();
|
||||||
let mut signer = Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error");
|
let mut signer =
|
||||||
signer.update(to_sign.as_bytes()).expect("Blog::sign: content insertion error");
|
Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error");
|
||||||
signer.sign_to_vec().expect("Blog::sign: finalization error")
|
signer
|
||||||
|
.update(to_sign.as_bytes())
|
||||||
|
.expect("Blog::sign: content insertion error");
|
||||||
|
signer
|
||||||
|
.sign_to_vec()
|
||||||
|
.expect("Blog::sign: finalization error")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&self, data: String, signature: Vec<u8>) -> bool {
|
fn verify(&self, data: String, signature: Vec<u8>) -> bool {
|
||||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).expect("Blog::verify: pem parsing error"))
|
let key = PKey::from_rsa(
|
||||||
.expect("Blog::verify: deserialization error");
|
Rsa::public_key_from_pem(self.public_key.as_ref())
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &key).expect("Blog::verify: initialization error");
|
.expect("Blog::verify: pem parsing error"),
|
||||||
verifier.update(data.as_bytes()).expect("Blog::verify: content insertion error");
|
).expect("Blog::verify: deserialization error");
|
||||||
verifier.verify(&signature).expect("Blog::verify: finalization error")
|
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)
|
||||||
|
.expect("Blog::verify: initialization error");
|
||||||
|
verifier
|
||||||
|
.update(data.as_bytes())
|
||||||
|
.expect("Blog::verify: content insertion error");
|
||||||
|
verifier
|
||||||
|
.verify(&signature)
|
||||||
|
.expect("Blog::verify: finalization error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,7 +487,7 @@ impl NewBlog {
|
||||||
actor_id: String,
|
actor_id: String,
|
||||||
title: String,
|
title: String,
|
||||||
summary: String,
|
summary: String,
|
||||||
instance_id: i32
|
instance_id: i32,
|
||||||
) -> NewBlog {
|
) -> NewBlog {
|
||||||
let (pub_key, priv_key) = sign::gen_keypair();
|
let (pub_key, priv_key) = sign::gen_keypair();
|
||||||
NewBlog {
|
NewBlog {
|
||||||
|
@ -344,7 +499,337 @@ impl NewBlog {
|
||||||
instance_id: instance_id,
|
instance_id: instance_id,
|
||||||
ap_url: String::from(""),
|
ap_url: String::from(""),
|
||||||
public_key: String::from_utf8(pub_key).expect("NewBlog::new_local: public key error"),
|
public_key: String::from_utf8(pub_key).expect("NewBlog::new_local: public key error"),
|
||||||
private_key: Some(String::from_utf8(priv_key).expect("NewBlog::new_local: private key error"))
|
private_key: Some(
|
||||||
|
String::from_utf8(priv_key).expect("NewBlog::new_local: private key error"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use super::*;
|
||||||
|
use blog_authors::*;
|
||||||
|
use diesel::Connection;
|
||||||
|
use instance::tests as instance_tests;
|
||||||
|
use tests::db;
|
||||||
|
use users::tests as usersTests;
|
||||||
|
use Connection as Conn;
|
||||||
|
|
||||||
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<Blog> {
|
||||||
|
instance_tests::fill_database(conn);
|
||||||
|
let users = usersTests::fill_database(conn);
|
||||||
|
let blogs = vec![
|
||||||
|
NewBlog::new_local(
|
||||||
|
"BlogName".to_owned(),
|
||||||
|
"Blog name".to_owned(),
|
||||||
|
"This is a small blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
NewBlog::new_local(
|
||||||
|
"MyBlog".to_owned(),
|
||||||
|
"My blog".to_owned(),
|
||||||
|
"Welcome to my blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
NewBlog::new_local(
|
||||||
|
"WhyILikePlume".to_owned(),
|
||||||
|
"Why I like Plume".to_owned(),
|
||||||
|
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
].into_iter()
|
||||||
|
.map(|nb| Blog::insert(conn, nb))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blogs[0].id,
|
||||||
|
author_id: users[0].id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blogs[0].id,
|
||||||
|
author_id: users[1].id,
|
||||||
|
is_owner: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blogs[1].id,
|
||||||
|
author_id: users[1].id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blogs[2].id,
|
||||||
|
author_id: users[2].id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
blogs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_instance() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
fill_database(conn);
|
||||||
|
|
||||||
|
let blog = Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
"SomeName".to_owned(),
|
||||||
|
"Some name".to_owned(),
|
||||||
|
"This is some blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(blog.get_instance(conn).id, Instance::local_id(conn));
|
||||||
|
// TODO add tests for remote instance
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authors() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let user = usersTests::fill_database(conn);
|
||||||
|
fill_database(conn);
|
||||||
|
|
||||||
|
let blog = vec![
|
||||||
|
Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
"SomeName".to_owned(),
|
||||||
|
"Some name".to_owned(),
|
||||||
|
"This is some blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
"Blog".to_owned(),
|
||||||
|
"Blog".to_owned(),
|
||||||
|
"I've named my blog Blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blog[0].id,
|
||||||
|
author_id: user[0].id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blog[0].id,
|
||||||
|
author_id: user[1].id,
|
||||||
|
is_owner: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blog[1].id,
|
||||||
|
author_id: user[0].id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
blog[0]
|
||||||
|
.list_authors(conn)
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.id == user[0].id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
blog[0]
|
||||||
|
.list_authors(conn)
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.id == user[1].id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
blog[1]
|
||||||
|
.list_authors(conn)
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.id == user[0].id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!blog[1]
|
||||||
|
.list_authors(conn)
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.id == user[1].id)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
Blog::find_for_author(conn, &user[0])
|
||||||
|
.iter()
|
||||||
|
.any(|b| b.id == blog[0].id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
Blog::find_for_author(conn, &user[1])
|
||||||
|
.iter()
|
||||||
|
.any(|b| b.id == blog[0].id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
Blog::find_for_author(conn, &user[0])
|
||||||
|
.iter()
|
||||||
|
.any(|b| b.id == blog[1].id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!Blog::find_for_author(conn, &user[1])
|
||||||
|
.iter()
|
||||||
|
.any(|b| b.id == blog[1].id)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_local() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
fill_database(conn);
|
||||||
|
|
||||||
|
let blog = Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
"SomeName".to_owned(),
|
||||||
|
"Some name".to_owned(),
|
||||||
|
"This is some blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Blog::find_local(conn, "SomeName".to_owned()).unwrap().id,
|
||||||
|
blog.id
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_fqn() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
fill_database(conn);
|
||||||
|
|
||||||
|
let blog = Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
"SomeName".to_owned(),
|
||||||
|
"Some name".to_owned(),
|
||||||
|
"This is some blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(blog.get_fqn(conn), "SomeName");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let blogs = fill_database(conn);
|
||||||
|
|
||||||
|
blogs[0].delete(conn);
|
||||||
|
assert!(Blog::get(conn, blogs[0].id).is_none());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_via_user() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let user = usersTests::fill_database(conn);
|
||||||
|
fill_database(conn);
|
||||||
|
|
||||||
|
let blog = vec![
|
||||||
|
Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
"SomeName".to_owned(),
|
||||||
|
"Some name".to_owned(),
|
||||||
|
"This is some blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
|
"Blog".to_owned(),
|
||||||
|
"Blog".to_owned(),
|
||||||
|
"I've named my blog Blog".to_owned(),
|
||||||
|
Instance::local_id(conn),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blog[0].id,
|
||||||
|
author_id: user[0].id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blog[0].id,
|
||||||
|
author_id: user[1].id,
|
||||||
|
is_owner: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BlogAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewBlogAuthor {
|
||||||
|
blog_id: blog[1].id,
|
||||||
|
author_id: user[0].id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
user[0].delete(conn);
|
||||||
|
assert!(Blog::get(conn, blog[0].id).is_some());
|
||||||
|
assert!(Blog::get(conn, blog[1].id).is_none());
|
||||||
|
user[1].delete(conn);
|
||||||
|
assert!(Blog::get(conn, blog[0].id).is_none());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
use activitypub::{
|
use activitypub::{activity::Create, link, object::Note};
|
||||||
activity::Create,
|
|
||||||
link,
|
|
||||||
object::{Note}
|
|
||||||
};
|
|
||||||
use chrono::{self, NaiveDateTime};
|
use chrono::{self, NaiveDateTime};
|
||||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use plume_common::activity_pub::{
|
|
||||||
Id, IntoId, PUBLIC_VISIBILTY,
|
|
||||||
inbox::{FromActivity, Notify}
|
|
||||||
};
|
|
||||||
use plume_common::utils;
|
|
||||||
use Connection;
|
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
|
use plume_common::activity_pub::{
|
||||||
|
inbox::{FromActivity, Notify},
|
||||||
|
Id, IntoId, PUBLIC_VISIBILTY,
|
||||||
|
};
|
||||||
|
use plume_common::utils;
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use users::User;
|
|
||||||
use schema::comments;
|
|
||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
|
use schema::comments;
|
||||||
|
use users::User;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Serialize, Clone)]
|
#[derive(Queryable, Identifiable, Serialize, Clone)]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
|
@ -31,7 +27,7 @@ pub struct Comment {
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub ap_url: Option<String>,
|
pub ap_url: Option<String>,
|
||||||
pub sensitive: bool,
|
pub sensitive: bool,
|
||||||
pub spoiler_text: String
|
pub spoiler_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, Default)]
|
#[derive(Insertable, Default)]
|
||||||
|
@ -43,7 +39,7 @@ pub struct NewComment {
|
||||||
pub author_id: i32,
|
pub author_id: i32,
|
||||||
pub ap_url: Option<String>,
|
pub ap_url: Option<String>,
|
||||||
pub sensitive: bool,
|
pub sensitive: bool,
|
||||||
pub spoiler_text: String
|
pub spoiler_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Comment {
|
impl Comment {
|
||||||
|
@ -62,24 +58,35 @@ impl Comment {
|
||||||
|
|
||||||
pub fn count_local(conn: &Connection) -> usize {
|
pub fn count_local(conn: &Connection) -> usize {
|
||||||
use schema::users;
|
use schema::users;
|
||||||
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id);
|
let local_authors = users::table
|
||||||
comments::table.filter(comments::author_id.eq_any(local_authors))
|
.filter(users::instance_id.eq(Instance::local_id(conn)))
|
||||||
|
.select(users::id);
|
||||||
|
comments::table
|
||||||
|
.filter(comments::author_id.eq_any(local_authors))
|
||||||
.load::<Comment>(conn)
|
.load::<Comment>(conn)
|
||||||
.expect("Comment::count_local: loading error")
|
.expect("Comment::count_local: loading error")
|
||||||
.len()// TODO count in database?
|
.len() // TODO count in database?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(&self, conn: &Connection, others: &Vec<Comment>) -> serde_json::Value {
|
pub fn to_json(&self, conn: &Connection, others: &Vec<Comment>) -> serde_json::Value {
|
||||||
let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error");
|
let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error");
|
||||||
json["author"] = self.get_author(conn).to_json(conn);
|
json["author"] = self.get_author(conn).to_json(conn);
|
||||||
let mentions = Mention::list_for_comment(conn, self.id).into_iter()
|
let mentions = Mention::list_for_comment(conn, self.id)
|
||||||
.map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new()))
|
.into_iter()
|
||||||
|
.map(|m| {
|
||||||
|
m.get_mentioned(conn)
|
||||||
|
.map(|u| u.get_fqn(conn))
|
||||||
|
.unwrap_or(String::new())
|
||||||
|
})
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error");
|
json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error");
|
||||||
json["responses"] = json!(others.into_iter()
|
json["responses"] = json!(
|
||||||
|
others
|
||||||
|
.into_iter()
|
||||||
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
|
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
|
||||||
.map(|c| c.to_json(conn, others))
|
.map(|c| c.to_json(conn, others))
|
||||||
.collect::<Vec<_>>());
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
json
|
json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,61 +113,138 @@ impl Comment {
|
||||||
let mut note = Note::default();
|
let mut note = Note::default();
|
||||||
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];
|
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];
|
||||||
|
|
||||||
note.object_props.set_id_string(self.ap_url.clone().unwrap_or(String::new())).expect("Comment::into_activity: id error");
|
note.object_props
|
||||||
note.object_props.set_summary_string(self.spoiler_text.clone()).expect("Comment::into_activity: summary error");
|
.set_id_string(self.ap_url.clone().unwrap_or(String::new()))
|
||||||
note.object_props.set_content_string(html).expect("Comment::into_activity: content error");
|
.expect("Comment::into_activity: id 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).expect("Comment::into_activity: post error").ap_url, |id| {
|
note.object_props
|
||||||
let comm = Comment::get(conn, id).expect("Comment::into_activity: comment error");
|
.set_summary_string(self.spoiler_text.clone())
|
||||||
|
.expect("Comment::into_activity: summary error");
|
||||||
|
note.object_props
|
||||||
|
.set_content_string(html)
|
||||||
|
.expect("Comment::into_activity: 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)
|
||||||
|
.expect("Comment::into_activity: post error")
|
||||||
|
.ap_url
|
||||||
|
},
|
||||||
|
|id| {
|
||||||
|
let comm =
|
||||||
|
Comment::get(conn, id).expect("Comment::into_activity: comment error");
|
||||||
comm.ap_url.clone().unwrap_or(comm.compute_id(conn))
|
comm.ap_url.clone().unwrap_or(comm.compute_id(conn))
|
||||||
}))).expect("Comment::into_activity: in_reply_to error");
|
},
|
||||||
note.object_props.set_published_string(chrono::Utc::now().to_rfc3339()).expect("Comment::into_activity: published error");
|
)))
|
||||||
note.object_props.set_attributed_to_link(author.clone().into_id()).expect("Comment::into_activity: attributed_to error");
|
.expect("Comment::into_activity: in_reply_to error");
|
||||||
note.object_props.set_to_link_vec(to.clone()).expect("Comment::into_activity: to error");
|
note.object_props
|
||||||
note.object_props.set_tag_link_vec(mentions.into_iter().map(|m| Mention::build_activity(conn, m)).collect::<Vec<link::Mention>>())
|
.set_published_string(chrono::Utc::now().to_rfc3339())
|
||||||
|
.expect("Comment::into_activity: published error");
|
||||||
|
note.object_props
|
||||||
|
.set_attributed_to_link(author.clone().into_id())
|
||||||
|
.expect("Comment::into_activity: attributed_to error");
|
||||||
|
note.object_props
|
||||||
|
.set_to_link_vec(to.clone())
|
||||||
|
.expect("Comment::into_activity: to error");
|
||||||
|
note.object_props
|
||||||
|
.set_tag_link_vec(
|
||||||
|
mentions
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| Mention::build_activity(conn, m))
|
||||||
|
.collect::<Vec<link::Mention>>(),
|
||||||
|
)
|
||||||
.expect("Comment::into_activity: tag error");
|
.expect("Comment::into_activity: tag error");
|
||||||
note
|
note
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_activity(&self, conn: &Connection) -> Create {
|
pub fn create_activity(&self, conn: &Connection) -> Create {
|
||||||
let author = User::get(conn, self.author_id).expect("Comment::create_activity: author error");
|
let author =
|
||||||
|
User::get(conn, self.author_id).expect("Comment::create_activity: author error");
|
||||||
|
|
||||||
let note = self.into_activity(conn);
|
let note = self.into_activity(conn);
|
||||||
let mut act = Create::default();
|
let mut act = Create::default();
|
||||||
act.create_props.set_actor_link(author.into_id()).expect("Comment::create_activity: actor error");
|
act.create_props
|
||||||
act.create_props.set_object_object(note.clone()).expect("Comment::create_activity: object error");
|
.set_actor_link(author.into_id())
|
||||||
act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().expect("Comment::create_activity: ap_url error"))).expect("Comment::create_activity: id error");
|
.expect("Comment::create_activity: actor error");
|
||||||
act.object_props.set_to_link_vec(note.object_props.to_link_vec::<Id>().expect("Comment::create_activity: id error")).expect("Comment::create_activity: to error");
|
act.create_props
|
||||||
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Comment::create_activity: cc error");
|
.set_object_object(note.clone())
|
||||||
|
.expect("Comment::create_activity: object error");
|
||||||
|
act.object_props
|
||||||
|
.set_id_string(format!(
|
||||||
|
"{}/activity",
|
||||||
|
self.ap_url
|
||||||
|
.clone()
|
||||||
|
.expect("Comment::create_activity: ap_url error")
|
||||||
|
))
|
||||||
|
.expect("Comment::create_activity: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link_vec(
|
||||||
|
note.object_props
|
||||||
|
.to_link_vec::<Id>()
|
||||||
|
.expect("Comment::create_activity: id error"),
|
||||||
|
)
|
||||||
|
.expect("Comment::create_activity: to error");
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Comment::create_activity: cc error");
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromActivity<Note, Connection> for Comment {
|
impl FromActivity<Note, Connection> for Comment {
|
||||||
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment {
|
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment {
|
||||||
let previous_url = note.object_props.in_reply_to.clone().expect("Comment::from_activity: not an answer error").as_str().expect("Comment::from_activity: in_reply_to parsing error").to_string();
|
let previous_url = note
|
||||||
|
.object_props
|
||||||
|
.in_reply_to
|
||||||
|
.clone()
|
||||||
|
.expect("Comment::from_activity: not an answer error")
|
||||||
|
.as_str()
|
||||||
|
.expect("Comment::from_activity: in_reply_to parsing error")
|
||||||
|
.to_string();
|
||||||
let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone());
|
let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone());
|
||||||
|
|
||||||
let comm = Comment::insert(conn, NewComment {
|
let comm = Comment::insert(
|
||||||
content: SafeString::new(¬e.object_props.content_string().expect("Comment::from_activity: content deserialization error")),
|
conn,
|
||||||
spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")),
|
NewComment {
|
||||||
|
content: SafeString::new(
|
||||||
|
¬e
|
||||||
|
.object_props
|
||||||
|
.content_string()
|
||||||
|
.expect("Comment::from_activity: content deserialization error"),
|
||||||
|
),
|
||||||
|
spoiler_text: note
|
||||||
|
.object_props
|
||||||
|
.summary_string()
|
||||||
|
.unwrap_or(String::from("")),
|
||||||
ap_url: note.object_props.id_string().ok(),
|
ap_url: note.object_props.id_string().ok(),
|
||||||
in_response_to_id: previous_comment.clone().map(|c| c.id),
|
in_response_to_id: previous_comment.clone().map(|c| c.id),
|
||||||
post_id: previous_comment
|
post_id: previous_comment.map(|c| c.post_id).unwrap_or_else(|| {
|
||||||
.map(|c| c.post_id)
|
Post::find_by_ap_url(conn, previous_url)
|
||||||
.unwrap_or_else(|| Post::find_by_ap_url(conn, previous_url).expect("Comment::from_activity: post error").id),
|
.expect("Comment::from_activity: post error")
|
||||||
author_id: User::from_url(conn, actor.clone().into()).expect("Comment::from_activity: author error").id,
|
.id
|
||||||
sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
|
}),
|
||||||
});
|
author_id: User::from_url(conn, actor.clone().into())
|
||||||
|
.expect("Comment::from_activity: author error")
|
||||||
|
.id,
|
||||||
|
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// save mentions
|
// save mentions
|
||||||
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
|
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
|
||||||
for tag in tags.into_iter() {
|
for tag in tags.into_iter() {
|
||||||
serde_json::from_value::<link::Mention>(tag)
|
serde_json::from_value::<link::Mention>(tag)
|
||||||
.map(|m| {
|
.map(|m| {
|
||||||
let author = &Post::get(conn, comm.post_id).expect("Comment::from_activity: error").get_authors(conn)[0];
|
let author = &Post::get(conn, comm.post_id)
|
||||||
let not_author = m.link_props.href_string().expect("Comment::from_activity: no href error") != author.ap_url.clone();
|
.expect("Comment::from_activity: error")
|
||||||
|
.get_authors(conn)[0];
|
||||||
|
let not_author = m
|
||||||
|
.link_props
|
||||||
|
.href_string()
|
||||||
|
.expect("Comment::from_activity: no href error")
|
||||||
|
!= author.ap_url.clone();
|
||||||
Mention::from_activity(conn, m, comm.id, false, not_author)
|
Mention::from_activity(conn, m, comm.id, false, not_author)
|
||||||
}).ok();
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,11 +256,14 @@ impl FromActivity<Note, Connection> for Comment {
|
||||||
impl Notify<Connection> for Comment {
|
impl Notify<Connection> for Comment {
|
||||||
fn notify(&self, conn: &Connection) {
|
fn notify(&self, conn: &Connection) {
|
||||||
for author in self.get_post(conn).get_authors(conn) {
|
for author in self.get_post(conn).get_authors(conn) {
|
||||||
Notification::insert(conn, NewNotification {
|
Notification::insert(
|
||||||
|
conn,
|
||||||
|
NewNotification {
|
||||||
kind: notification_kind::COMMENT.to_string(),
|
kind: notification_kind::COMMENT.to_string(),
|
||||||
object_id: self.id,
|
object_id: self.id,
|
||||||
user_id: author.id
|
user_id: author.id,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use diesel::{
|
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
||||||
r2d2::{ConnectionManager, Pool, PooledConnection}
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
request::{self, FromRequest},
|
||||||
|
Outcome, Request, State,
|
||||||
};
|
};
|
||||||
use rocket::{Request, State, Outcome, http::Status, request::{self, FromRequest}};
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use Connection;
|
use Connection;
|
||||||
|
@ -23,7 +25,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
|
||||||
let pool = request.guard::<State<DbPool>>()?;
|
let pool = request.guard::<State<DbPool>>()?;
|
||||||
match pool.get() {
|
match pool.get() {
|
||||||
Ok(conn) => Outcome::Success(DbConn(conn)),
|
Ok(conn) => Outcome::Success(DbConn(conn)),
|
||||||
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
|
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
use activitypub::{Actor, activity::{Accept, Follow as FollowAct, Undo}, actor::Person};
|
use activitypub::{
|
||||||
|
activity::{Accept, Follow as FollowAct, Undo},
|
||||||
|
actor::Person,
|
||||||
|
Actor,
|
||||||
|
};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
|
|
||||||
use {BASE_URL, ap_url, Connection};
|
|
||||||
use blogs::Blog;
|
use blogs::Blog;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
use users::User;
|
use plume_common::activity_pub::{
|
||||||
|
broadcast,
|
||||||
|
inbox::{Deletable, FromActivity, Notify, WithInbox},
|
||||||
|
sign::Signer,
|
||||||
|
Id, IntoId,
|
||||||
|
};
|
||||||
use schema::follows;
|
use schema::follows;
|
||||||
|
use users::User;
|
||||||
|
use {ap_url, Connection, BASE_URL};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, Associations)]
|
#[derive(Clone, Queryable, Identifiable, Associations)]
|
||||||
#[belongs_to(User, foreign_key = "following_id")]
|
#[belongs_to(User, foreign_key = "following_id")]
|
||||||
|
@ -31,22 +40,35 @@ impl Follow {
|
||||||
find_by!(follows, find_by_ap_url, ap_url as String);
|
find_by!(follows, find_by_ap_url, ap_url as String);
|
||||||
|
|
||||||
pub fn find(conn: &Connection, from: i32, to: i32) -> Option<Follow> {
|
pub fn find(conn: &Connection, from: i32, to: i32) -> Option<Follow> {
|
||||||
follows::table.filter(follows::follower_id.eq(from))
|
follows::table
|
||||||
|
.filter(follows::follower_id.eq(from))
|
||||||
.filter(follows::following_id.eq(to))
|
.filter(follows::following_id.eq(to))
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_activity(&self, conn: &Connection) -> FollowAct {
|
pub fn into_activity(&self, conn: &Connection) -> FollowAct {
|
||||||
let user = User::get(conn, self.follower_id).expect("Follow::into_activity: actor not found error");
|
let user = User::get(conn, self.follower_id)
|
||||||
let target = User::get(conn, self.following_id).expect("Follow::into_activity: target not found error");
|
.expect("Follow::into_activity: actor not found error");
|
||||||
|
let target = User::get(conn, self.following_id)
|
||||||
|
.expect("Follow::into_activity: target not found error");
|
||||||
|
|
||||||
let mut act = FollowAct::default();
|
let mut act = FollowAct::default();
|
||||||
act.follow_props.set_actor_link::<Id>(user.clone().into_id()).expect("Follow::into_activity: actor error");
|
act.follow_props
|
||||||
act.follow_props.set_object_object(user.into_activity(&*conn)).expect("Follow::into_activity: object error");
|
.set_actor_link::<Id>(user.clone().into_id())
|
||||||
act.object_props.set_id_string(self.ap_url.clone()).expect("Follow::into_activity: id error");
|
.expect("Follow::into_activity: actor error");
|
||||||
act.object_props.set_to_link(target.clone().into_id()).expect("Follow::into_activity: target error");
|
act.follow_props
|
||||||
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Follow::into_activity: cc error");
|
.set_object_object(user.into_activity(&*conn))
|
||||||
|
.expect("Follow::into_activity: object error");
|
||||||
|
act.object_props
|
||||||
|
.set_id_string(self.ap_url.clone())
|
||||||
|
.expect("Follow::into_activity: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link(target.clone().into_id())
|
||||||
|
.expect("Follow::into_activity: target error");
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Follow::into_activity: cc error");
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,23 +80,41 @@ impl Follow {
|
||||||
target: &A,
|
target: &A,
|
||||||
follow: FollowAct,
|
follow: FollowAct,
|
||||||
from_id: i32,
|
from_id: i32,
|
||||||
target_id: i32
|
target_id: i32,
|
||||||
) -> Follow {
|
) -> Follow {
|
||||||
let from_url: String = from.clone().into_id().into();
|
let from_url: String = from.clone().into_id().into();
|
||||||
let target_url: String = target.clone().into_id().into();
|
let target_url: String = target.clone().into_id().into();
|
||||||
let res = Follow::insert(conn, NewFollow {
|
let res = Follow::insert(
|
||||||
|
conn,
|
||||||
|
NewFollow {
|
||||||
follower_id: from_id,
|
follower_id: from_id,
|
||||||
following_id: target_id,
|
following_id: target_id,
|
||||||
ap_url: format!("{}/follow/{}", from_url, target_url),
|
ap_url: format!("{}/follow/{}", from_url, target_url),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let mut accept = Accept::default();
|
let mut accept = Accept::default();
|
||||||
let accept_id = ap_url(format!("{}/follow/{}/accept", BASE_URL.as_str(), res.id));
|
let accept_id = ap_url(format!("{}/follow/{}/accept", BASE_URL.as_str(), res.id));
|
||||||
accept.object_props.set_id_string(accept_id).expect("Follow::accept_follow: id error");
|
accept
|
||||||
accept.object_props.set_to_link(from.clone().into_id()).expect("Follow::accept_follow: to error");
|
.object_props
|
||||||
accept.object_props.set_cc_link_vec::<Id>(vec![]).expect("Follow::accept_follow: cc error");
|
.set_id_string(accept_id)
|
||||||
accept.accept_props.set_actor_link::<Id>(target.clone().into_id()).expect("Follow::accept_follow: actor error");
|
.expect("Follow::accept_follow: id error");
|
||||||
accept.accept_props.set_object_object(follow).expect("Follow::accept_follow: object error");
|
accept
|
||||||
|
.object_props
|
||||||
|
.set_to_link(from.clone().into_id())
|
||||||
|
.expect("Follow::accept_follow: to error");
|
||||||
|
accept
|
||||||
|
.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Follow::accept_follow: cc error");
|
||||||
|
accept
|
||||||
|
.accept_props
|
||||||
|
.set_actor_link::<Id>(target.clone().into_id())
|
||||||
|
.expect("Follow::accept_follow: actor error");
|
||||||
|
accept
|
||||||
|
.accept_props
|
||||||
|
.set_object_object(follow)
|
||||||
|
.expect("Follow::accept_follow: object error");
|
||||||
broadcast(&*target, accept, vec![from.clone()]);
|
broadcast(&*target, accept, vec![from.clone()]);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -82,14 +122,41 @@ impl Follow {
|
||||||
|
|
||||||
impl FromActivity<FollowAct, Connection> for Follow {
|
impl FromActivity<FollowAct, Connection> for Follow {
|
||||||
fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow {
|
fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow {
|
||||||
let from_id = follow.follow_props.actor_link::<Id>().map(|l| l.into())
|
let from_id = follow
|
||||||
.unwrap_or_else(|_| follow.follow_props.actor_object::<Person>().expect("Follow::from_activity: actor not found error").object_props.id_string().expect("Follow::from_activity: actor not found error"));
|
.follow_props
|
||||||
let from = User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error");
|
.actor_link::<Id>()
|
||||||
match User::from_url(conn, follow.follow_props.object.as_str().expect("Follow::from_activity: target url parsing error").to_string()) {
|
.map(|l| l.into())
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.actor_object::<Person>()
|
||||||
|
.expect("Follow::from_activity: actor not found error")
|
||||||
|
.object_props
|
||||||
|
.id_string()
|
||||||
|
.expect("Follow::from_activity: actor not found error")
|
||||||
|
});
|
||||||
|
let from =
|
||||||
|
User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error");
|
||||||
|
match User::from_url(
|
||||||
|
conn,
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.object
|
||||||
|
.as_str()
|
||||||
|
.expect("Follow::from_activity: target url parsing error")
|
||||||
|
.to_string(),
|
||||||
|
) {
|
||||||
Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
|
Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
|
||||||
None => {
|
None => {
|
||||||
let blog = Blog::from_url(conn, follow.follow_props.object.as_str().expect("Follow::from_activity: target url parsing error").to_string())
|
let blog = Blog::from_url(
|
||||||
.expect("Follow::from_activity: target not found error");
|
conn,
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.object
|
||||||
|
.as_str()
|
||||||
|
.expect("Follow::from_activity: target url parsing error")
|
||||||
|
.to_string(),
|
||||||
|
).expect("Follow::from_activity: target not found error");
|
||||||
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
|
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,27 +165,44 @@ impl FromActivity<FollowAct, Connection> for Follow {
|
||||||
|
|
||||||
impl Notify<Connection> for Follow {
|
impl Notify<Connection> for Follow {
|
||||||
fn notify(&self, conn: &Connection) {
|
fn notify(&self, conn: &Connection) {
|
||||||
Notification::insert(conn, NewNotification {
|
Notification::insert(
|
||||||
|
conn,
|
||||||
|
NewNotification {
|
||||||
kind: notification_kind::FOLLOW.to_string(),
|
kind: notification_kind::FOLLOW.to_string(),
|
||||||
object_id: self.id,
|
object_id: self.id,
|
||||||
user_id: self.following_id
|
user_id: self.following_id,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deletable<Connection, Undo> for Follow {
|
impl Deletable<Connection, Undo> for Follow {
|
||||||
fn delete(&self, conn: &Connection) -> Undo {
|
fn delete(&self, conn: &Connection) -> Undo {
|
||||||
diesel::delete(self).execute(conn).expect("Follow::delete: follow deletion error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Follow::delete: follow deletion error");
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||||
diesel::delete(¬if).execute(conn).expect("Follow::delete: notification deletion error");
|
diesel::delete(¬if)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Follow::delete: notification deletion error");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut undo = Undo::default();
|
let mut undo = Undo::default();
|
||||||
undo.undo_props.set_actor_link(User::get(conn, self.follower_id).expect("Follow::delete: actor error").into_id()).expect("Follow::delete: actor error");
|
undo.undo_props
|
||||||
undo.object_props.set_id_string(format!("{}/undo", self.ap_url)).expect("Follow::delete: id error");
|
.set_actor_link(
|
||||||
undo.undo_props.set_object_object(self.into_activity(conn)).expect("Follow::delete: object error");
|
User::get(conn, self.follower_id)
|
||||||
|
.expect("Follow::delete: actor error")
|
||||||
|
.into_id(),
|
||||||
|
)
|
||||||
|
.expect("Follow::delete: actor error");
|
||||||
|
undo.object_props
|
||||||
|
.set_id_string(format!("{}/undo", self.ap_url))
|
||||||
|
.expect("Follow::delete: id error");
|
||||||
|
undo.undo_props
|
||||||
|
.set_object_object(self.into_activity(conn))
|
||||||
|
.expect("Follow::delete: object error");
|
||||||
undo
|
undo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
use activitypub::{
|
||||||
|
activity::{Accept, Follow as FollowAct, Undo},
|
||||||
|
actor::Person,
|
||||||
|
Actor,
|
||||||
|
};
|
||||||
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
|
||||||
|
use {BASE_URL, ap_url, Connection};
|
||||||
|
=======
|
||||||
|
>>>>>>> Run rustfmt and rename instanceTests to instance_tests
|
||||||
|
use blogs::Blog;
|
||||||
|
use notifications::*;
|
||||||
|
use plume_common::activity_pub::{
|
||||||
|
broadcast,
|
||||||
|
inbox::{Deletable, FromActivity, Notify, WithInbox},
|
||||||
|
sign::Signer,
|
||||||
|
Id, IntoId,
|
||||||
|
};
|
||||||
|
use schema::follows;
|
||||||
|
use users::User;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
|
#[derive(Clone, Queryable, Identifiable, Associations)]
|
||||||
|
#[belongs_to(User, foreign_key = "following_id")]
|
||||||
|
pub struct Follow {
|
||||||
|
pub id: i32,
|
||||||
|
pub follower_id: i32,
|
||||||
|
pub following_id: i32,
|
||||||
|
pub ap_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[table_name = "follows"]
|
||||||
|
pub struct NewFollow {
|
||||||
|
pub follower_id: i32,
|
||||||
|
pub following_id: i32,
|
||||||
|
pub ap_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Follow {
|
||||||
|
insert!(follows, NewFollow);
|
||||||
|
get!(follows);
|
||||||
|
find_by!(follows, find_by_ap_url, ap_url as String);
|
||||||
|
|
||||||
|
pub fn find(conn: &Connection, from: i32, to: i32) -> Option<Follow> {
|
||||||
|
follows::table
|
||||||
|
.filter(follows::follower_id.eq(from))
|
||||||
|
.filter(follows::following_id.eq(to))
|
||||||
|
.get_result(conn)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_activity(&self, conn: &Connection) -> FollowAct {
|
||||||
|
let user = User::get(conn, self.follower_id)
|
||||||
|
.expect("Follow::into_activity: actor not found error");
|
||||||
|
let target = User::get(conn, self.following_id)
|
||||||
|
.expect("Follow::into_activity: target not found error");
|
||||||
|
|
||||||
|
let mut act = FollowAct::default();
|
||||||
|
act.follow_props
|
||||||
|
.set_actor_link::<Id>(user.clone().into_id())
|
||||||
|
.expect("Follow::into_activity: actor error");
|
||||||
|
act.follow_props
|
||||||
|
.set_object_object(user.into_activity(&*conn))
|
||||||
|
.expect("Follow::into_activity: object error");
|
||||||
|
act.object_props
|
||||||
|
.set_id_string(self.ap_url.clone())
|
||||||
|
.expect("Follow::into_activity: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link(target.clone().into_id())
|
||||||
|
.expect("Follow::into_activity: target error");
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Follow::into_activity: cc error");
|
||||||
|
act
|
||||||
|
}
|
||||||
|
|
||||||
|
/// from -> The one sending the follow request
|
||||||
|
/// target -> The target of the request, responding with Accept
|
||||||
|
pub fn accept_follow<A: Signer + IntoId + Clone, B: Clone + WithInbox + Actor + IntoId>(
|
||||||
|
conn: &Connection,
|
||||||
|
from: &B,
|
||||||
|
target: &A,
|
||||||
|
follow: FollowAct,
|
||||||
|
from_id: i32,
|
||||||
|
target_id: i32,
|
||||||
|
) -> Follow {
|
||||||
|
let from_url: String = from.clone().into_id().into();
|
||||||
|
let target_url: String = target.clone().into_id().into();
|
||||||
|
let res = Follow::insert(
|
||||||
|
conn,
|
||||||
|
NewFollow {
|
||||||
|
follower_id: from_id,
|
||||||
|
following_id: target_id,
|
||||||
|
ap_url: format!("{}/follow/{}", from_url, target_url),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut accept = Accept::default();
|
||||||
|
<<<<<<< HEAD
|
||||||
|
let accept_id = ap_url(format!("{}/follow/{}/accept", BASE_URL.as_str(), res.id));
|
||||||
|
accept.object_props.set_id_string(accept_id).expect("Follow::accept_follow: id error");
|
||||||
|
accept.object_props.set_to_link(from.clone().into_id()).expect("Follow::accept_follow: to error");
|
||||||
|
accept.object_props.set_cc_link_vec::<Id>(vec![]).expect("Follow::accept_follow: cc error");
|
||||||
|
accept.accept_props.set_actor_link::<Id>(target.clone().into_id()).expect("Follow::accept_follow: actor error");
|
||||||
|
accept.accept_props.set_object_object(follow).expect("Follow::accept_follow: object error");
|
||||||
|
=======
|
||||||
|
let accept_id = format!(
|
||||||
|
"{}#accept",
|
||||||
|
follow.object_props.id_string().unwrap_or(String::new())
|
||||||
|
);
|
||||||
|
accept
|
||||||
|
.object_props
|
||||||
|
.set_id_string(accept_id)
|
||||||
|
.expect("Follow::accept_follow: id error");
|
||||||
|
accept
|
||||||
|
.object_props
|
||||||
|
.set_to_link(from.clone().into_id())
|
||||||
|
.expect("Follow::accept_follow: to error");
|
||||||
|
accept
|
||||||
|
.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Follow::accept_follow: cc error");
|
||||||
|
accept
|
||||||
|
.accept_props
|
||||||
|
.set_actor_link::<Id>(target.clone().into_id())
|
||||||
|
.expect("Follow::accept_follow: actor error");
|
||||||
|
accept
|
||||||
|
.accept_props
|
||||||
|
.set_object_object(follow)
|
||||||
|
.expect("Follow::accept_follow: object error");
|
||||||
|
>>>>>>> Run rustfmt and rename instanceTests to instance_tests
|
||||||
|
broadcast(&*target, accept, vec![from.clone()]);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromActivity<FollowAct, Connection> for Follow {
|
||||||
|
fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow {
|
||||||
|
let from_id = follow
|
||||||
|
.follow_props
|
||||||
|
.actor_link::<Id>()
|
||||||
|
.map(|l| l.into())
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.actor_object::<Person>()
|
||||||
|
.expect("Follow::from_activity: actor not found error")
|
||||||
|
.object_props
|
||||||
|
.id_string()
|
||||||
|
.expect("Follow::from_activity: actor not found error")
|
||||||
|
});
|
||||||
|
let from =
|
||||||
|
User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error");
|
||||||
|
match User::from_url(
|
||||||
|
conn,
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.object
|
||||||
|
.as_str()
|
||||||
|
.expect("Follow::from_activity: target url parsing error")
|
||||||
|
.to_string(),
|
||||||
|
) {
|
||||||
|
Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
|
||||||
|
None => {
|
||||||
|
let blog = Blog::from_url(
|
||||||
|
conn,
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.object
|
||||||
|
.as_str()
|
||||||
|
.expect("Follow::from_activity: target url parsing error")
|
||||||
|
.to_string(),
|
||||||
|
).expect("Follow::from_activity: target not found error");
|
||||||
|
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notify<Connection> for Follow {
|
||||||
|
fn notify(&self, conn: &Connection) {
|
||||||
|
Notification::insert(
|
||||||
|
conn,
|
||||||
|
NewNotification {
|
||||||
|
kind: notification_kind::FOLLOW.to_string(),
|
||||||
|
object_id: self.id,
|
||||||
|
user_id: self.following_id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deletable<Connection, Undo> for Follow {
|
||||||
|
fn delete(&self, conn: &Connection) -> Undo {
|
||||||
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Follow::delete: follow deletion error");
|
||||||
|
|
||||||
|
// delete associated notification if any
|
||||||
|
if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||||
|
diesel::delete(¬if)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Follow::delete: notification deletion error");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut undo = Undo::default();
|
||||||
|
undo.undo_props
|
||||||
|
.set_actor_link(
|
||||||
|
User::get(conn, self.follower_id)
|
||||||
|
.expect("Follow::delete: actor error")
|
||||||
|
.into_id(),
|
||||||
|
)
|
||||||
|
.expect("Follow::delete: actor error");
|
||||||
|
undo.object_props
|
||||||
|
.set_id_string(format!("{}/undo", self.ap_url))
|
||||||
|
.expect("Follow::delete: id error");
|
||||||
|
undo.undo_props
|
||||||
|
.set_object_object(self.into_activity(conn))
|
||||||
|
.expect("Follow::delete: object error");
|
||||||
|
undo
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_id(id: String, actor_id: String, conn: &Connection) {
|
||||||
|
if let Some(follow) = Follow::find_by_ap_url(conn, id) {
|
||||||
|
if let Some(user) = User::find_by_ap_url(conn, actor_id) {
|
||||||
|
if user.id == follow.follower_id {
|
||||||
|
follow.delete(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
use rocket::request::{self, FromRequest, Request};
|
use rocket::request::{self, FromRequest, Request};
|
||||||
use rocket::{http::{Header, HeaderMap}, Outcome};
|
use rocket::{
|
||||||
|
http::{Header, HeaderMap},
|
||||||
|
Outcome,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Headers<'r>(pub HeaderMap<'r>);
|
pub struct Headers<'r>(pub HeaderMap<'r>);
|
||||||
|
|
||||||
|
@ -18,10 +20,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> {
|
||||||
} else {
|
} else {
|
||||||
ori.path().to_owned()
|
ori.path().to_owned()
|
||||||
};
|
};
|
||||||
headers.add(Header::new("(request-target)",
|
headers.add(Header::new(
|
||||||
format!("{} {}",
|
"(request-target)",
|
||||||
request.method().as_str().to_lowercase(),
|
format!("{} {}", request.method().as_str().to_lowercase(), uri),
|
||||||
uri)));
|
));
|
||||||
Outcome::Success(Headers(headers))
|
Outcome::Success(Headers(headers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use std::iter::Iterator;
|
use std::iter::Iterator;
|
||||||
|
|
||||||
use plume_common::utils::md_to_html;
|
|
||||||
use Connection;
|
|
||||||
use safe_string::SafeString;
|
|
||||||
use ap_url;
|
use ap_url;
|
||||||
use users::User;
|
use plume_common::utils::md_to_html;
|
||||||
|
use safe_string::SafeString;
|
||||||
use schema::{instances, users};
|
use schema::{instances, users};
|
||||||
|
use users::User;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable, Serialize)]
|
#[derive(Clone, Identifiable, Queryable, Serialize)]
|
||||||
pub struct Instance {
|
pub struct Instance {
|
||||||
|
@ -20,12 +20,12 @@ pub struct Instance {
|
||||||
pub open_registrations: bool,
|
pub open_registrations: bool,
|
||||||
pub short_description: SafeString,
|
pub short_description: SafeString,
|
||||||
pub long_description: SafeString,
|
pub long_description: SafeString,
|
||||||
pub default_license : String,
|
pub default_license: String,
|
||||||
pub long_description_html: String,
|
pub long_description_html: String,
|
||||||
pub short_description_html: String
|
pub short_description_html: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Clone, Insertable)]
|
||||||
#[table_name = "instances"]
|
#[table_name = "instances"]
|
||||||
pub struct NewInstance {
|
pub struct NewInstance {
|
||||||
pub public_domain: String,
|
pub public_domain: String,
|
||||||
|
@ -34,28 +34,32 @@ pub struct NewInstance {
|
||||||
pub open_registrations: bool,
|
pub open_registrations: bool,
|
||||||
pub short_description: SafeString,
|
pub short_description: SafeString,
|
||||||
pub long_description: SafeString,
|
pub long_description: SafeString,
|
||||||
pub default_license : String,
|
pub default_license: String,
|
||||||
pub long_description_html: String,
|
pub long_description_html: String,
|
||||||
pub short_description_html: String
|
pub short_description_html: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Instance {
|
impl Instance {
|
||||||
pub fn get_local(conn: &Connection) -> Option<Instance> {
|
pub fn get_local(conn: &Connection) -> Option<Instance> {
|
||||||
instances::table.filter(instances::local.eq(true))
|
instances::table
|
||||||
|
.filter(instances::local.eq(true))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.load::<Instance>(conn)
|
.load::<Instance>(conn)
|
||||||
.expect("Instance::get_local: loading error")
|
.expect("Instance::get_local: loading error")
|
||||||
.into_iter().nth(0)
|
.into_iter()
|
||||||
|
.nth(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_remotes(conn: &Connection) -> Vec<Instance> {
|
pub fn get_remotes(conn: &Connection) -> Vec<Instance> {
|
||||||
instances::table.filter(instances::local.eq(false))
|
instances::table
|
||||||
|
.filter(instances::local.eq(false))
|
||||||
.load::<Instance>(conn)
|
.load::<Instance>(conn)
|
||||||
.expect("Instance::get_remotes: loading error")
|
.expect("Instance::get_remotes: loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Instance> {
|
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Instance> {
|
||||||
instances::table.order(instances::public_domain.asc())
|
instances::table
|
||||||
|
.order(instances::public_domain.asc())
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
.limit((max - min).into())
|
.limit((max - min).into())
|
||||||
.load::<Instance>(conn)
|
.load::<Instance>(conn)
|
||||||
|
@ -63,7 +67,9 @@ impl Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_id(conn: &Connection) -> i32 {
|
pub fn local_id(conn: &Connection) -> i32 {
|
||||||
Instance::get_local(conn).expect("Instance::local_id: local instance not found error").id
|
Instance::get_local(conn)
|
||||||
|
.expect("Instance::local_id: local instance not found error")
|
||||||
|
.id
|
||||||
}
|
}
|
||||||
|
|
||||||
insert!(instances, NewInstance);
|
insert!(instances, NewInstance);
|
||||||
|
@ -79,10 +85,12 @@ impl Instance {
|
||||||
|
|
||||||
/// id: AP object id
|
/// id: AP object id
|
||||||
pub fn is_blocked(conn: &Connection, id: String) -> bool {
|
pub fn is_blocked(conn: &Connection, id: String) -> bool {
|
||||||
for block in instances::table.filter(instances::blocked.eq(true))
|
for block in instances::table
|
||||||
|
.filter(instances::blocked.eq(true))
|
||||||
.get_results::<Instance>(conn)
|
.get_results::<Instance>(conn)
|
||||||
.expect("Instance::is_blocked: loading error") {
|
.expect("Instance::is_blocked: loading error")
|
||||||
if id.starts_with(format!("https://{}", block.public_domain).as_str()) {
|
{
|
||||||
|
if id.starts_with(format!("https://{}/", block.public_domain).as_str()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +99,8 @@ impl Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_admin(&self, conn: &Connection) -> bool {
|
pub fn has_admin(&self, conn: &Connection) -> bool {
|
||||||
users::table.filter(users::instance_id.eq(self.id))
|
users::table
|
||||||
|
.filter(users::instance_id.eq(self.id))
|
||||||
.filter(users::is_admin.eq(true))
|
.filter(users::is_admin.eq(true))
|
||||||
.load::<User>(conn)
|
.load::<User>(conn)
|
||||||
.expect("Instance::has_admin: loading error")
|
.expect("Instance::has_admin: loading error")
|
||||||
|
@ -99,14 +108,20 @@ impl Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_admin(&self, conn: &Connection) -> User {
|
pub fn main_admin(&self, conn: &Connection) -> User {
|
||||||
users::table.filter(users::instance_id.eq(self.id))
|
users::table
|
||||||
|
.filter(users::instance_id.eq(self.id))
|
||||||
.filter(users::is_admin.eq(true))
|
.filter(users::is_admin.eq(true))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.get_result::<User>(conn)
|
.get_result::<User>(conn)
|
||||||
.expect("Instance::main_admin: loading error")
|
.expect("Instance::main_admin: loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute_box(&self, prefix: &'static str, name: String, box_name: &'static str) -> String {
|
pub fn compute_box(
|
||||||
|
&self,
|
||||||
|
prefix: &'static str,
|
||||||
|
name: String,
|
||||||
|
box_name: &'static str,
|
||||||
|
) -> String {
|
||||||
ap_url(format!(
|
ap_url(format!(
|
||||||
"{instance}/{prefix}/{name}/{box_name}",
|
"{instance}/{prefix}/{name}/{box_name}",
|
||||||
instance = self.public_domain,
|
instance = self.public_domain,
|
||||||
|
@ -116,7 +131,14 @@ impl Instance {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&self, conn: &Connection, name: String, open_registrations: bool, short_description: SafeString, long_description: SafeString) {
|
pub fn update(
|
||||||
|
&self,
|
||||||
|
conn: &Connection,
|
||||||
|
name: String,
|
||||||
|
open_registrations: bool,
|
||||||
|
short_description: SafeString,
|
||||||
|
long_description: SafeString,
|
||||||
|
) {
|
||||||
let (sd, _, _) = md_to_html(short_description.as_ref());
|
let (sd, _, _) = md_to_html(short_description.as_ref());
|
||||||
let (ld, _, _) = md_to_html(long_description.as_ref());
|
let (ld, _, _) = md_to_html(long_description.as_ref());
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
|
@ -126,12 +148,258 @@ impl Instance {
|
||||||
instances::short_description.eq(short_description),
|
instances::short_description.eq(short_description),
|
||||||
instances::long_description.eq(long_description),
|
instances::long_description.eq(long_description),
|
||||||
instances::short_description_html.eq(sd),
|
instances::short_description_html.eq(sd),
|
||||||
instances::long_description_html.eq(ld)
|
instances::long_description_html.eq(ld),
|
||||||
)).execute(conn)
|
))
|
||||||
|
.execute(conn)
|
||||||
.expect("Instance::update: update error");
|
.expect("Instance::update: update error");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count(conn: &Connection) -> i64 {
|
pub fn count(conn: &Connection) -> i64 {
|
||||||
instances::table.count().get_result(conn).expect("Instance::count: counting error")
|
instances::table
|
||||||
|
.count()
|
||||||
|
.get_result(conn)
|
||||||
|
.expect("Instance::count: counting error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use super::*;
|
||||||
|
use diesel::Connection;
|
||||||
|
use tests::db;
|
||||||
|
use Connection as Conn;
|
||||||
|
|
||||||
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
|
||||||
|
vec![
|
||||||
|
NewInstance {
|
||||||
|
default_license: "WTFPL".to_string(),
|
||||||
|
local: true,
|
||||||
|
long_description: SafeString::new("This is my instance."),
|
||||||
|
long_description_html: "<p>This is my instance</p>".to_string(),
|
||||||
|
short_description: SafeString::new("My instance."),
|
||||||
|
short_description_html: "<p>My instance</p>".to_string(),
|
||||||
|
name: "My instance".to_string(),
|
||||||
|
open_registrations: true,
|
||||||
|
public_domain: "plu.me".to_string(),
|
||||||
|
},
|
||||||
|
NewInstance {
|
||||||
|
default_license: "WTFPL".to_string(),
|
||||||
|
local: false,
|
||||||
|
long_description: SafeString::new("This is an instance."),
|
||||||
|
long_description_html: "<p>This is an instance</p>".to_string(),
|
||||||
|
short_description: SafeString::new("An instance."),
|
||||||
|
short_description_html: "<p>An instance</p>".to_string(),
|
||||||
|
name: "An instance".to_string(),
|
||||||
|
open_registrations: true,
|
||||||
|
public_domain: "1plu.me".to_string(),
|
||||||
|
},
|
||||||
|
NewInstance {
|
||||||
|
default_license: "CC-0".to_string(),
|
||||||
|
local: false,
|
||||||
|
long_description: SafeString::new("This is the instance of someone."),
|
||||||
|
long_description_html: "<p>This is the instance of someone</p>".to_string(),
|
||||||
|
short_description: SafeString::new("Someone instance."),
|
||||||
|
short_description_html: "<p>Someone instance</p>".to_string(),
|
||||||
|
name: "Someone instance".to_string(),
|
||||||
|
open_registrations: false,
|
||||||
|
public_domain: "2plu.me".to_string(),
|
||||||
|
},
|
||||||
|
NewInstance {
|
||||||
|
default_license: "CC-0-BY-SA".to_string(),
|
||||||
|
local: false,
|
||||||
|
long_description: SafeString::new("Good morning"),
|
||||||
|
long_description_html: "<p>Good morning</p>".to_string(),
|
||||||
|
short_description: SafeString::new("Hello"),
|
||||||
|
short_description_html: "<p>Hello</p>".to_string(),
|
||||||
|
name: "Nice day".to_string(),
|
||||||
|
open_registrations: true,
|
||||||
|
public_domain: "3plu.me".to_string(),
|
||||||
|
},
|
||||||
|
].into_iter()
|
||||||
|
.map(|inst| {
|
||||||
|
(
|
||||||
|
inst.clone(),
|
||||||
|
Instance::find_by_domain(conn, inst.public_domain.clone())
|
||||||
|
.unwrap_or_else(|| Instance::insert(conn, inst)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_instance() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let inserted = fill_database(conn)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(inserted, _)| inserted)
|
||||||
|
.find(|inst| inst.local)
|
||||||
|
.unwrap();
|
||||||
|
let res = Instance::get_local(conn).unwrap();
|
||||||
|
|
||||||
|
part_eq!(
|
||||||
|
res,
|
||||||
|
inserted,
|
||||||
|
[
|
||||||
|
default_license,
|
||||||
|
local,
|
||||||
|
long_description,
|
||||||
|
long_description_html,
|
||||||
|
short_description,
|
||||||
|
short_description_html,
|
||||||
|
name,
|
||||||
|
open_registrations,
|
||||||
|
public_domain
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(Instance::local_id(conn), res.id);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remote_instance() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let inserted = fill_database(conn);
|
||||||
|
assert_eq!(Instance::count(conn), inserted.len() as i64);
|
||||||
|
|
||||||
|
let res = Instance::get_remotes(conn);
|
||||||
|
assert_eq!(
|
||||||
|
res.len(),
|
||||||
|
inserted.iter().filter(|(inst, _)| !inst.local).count()
|
||||||
|
);
|
||||||
|
|
||||||
|
inserted
|
||||||
|
.iter()
|
||||||
|
.filter(|(newinst, _)| !newinst.local)
|
||||||
|
.map(|(newinst, inst)| (newinst, res.iter().find(|res| res.id == inst.id).unwrap()))
|
||||||
|
.for_each(|(newinst, inst)| {
|
||||||
|
part_eq!(
|
||||||
|
newinst,
|
||||||
|
inst,
|
||||||
|
[
|
||||||
|
default_license,
|
||||||
|
local,
|
||||||
|
long_description,
|
||||||
|
long_description_html,
|
||||||
|
short_description,
|
||||||
|
short_description_html,
|
||||||
|
name,
|
||||||
|
open_registrations,
|
||||||
|
public_domain
|
||||||
|
]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let page = Instance::page(conn, (0, 2));
|
||||||
|
assert_eq!(page.len(), 2);
|
||||||
|
let page1 = &page[0];
|
||||||
|
let page2 = &page[1];
|
||||||
|
assert!(page1.public_domain <= page2.public_domain);
|
||||||
|
|
||||||
|
let mut last_domaine: String = Instance::page(conn, (0, 1))[0].public_domain.clone();
|
||||||
|
for i in 1..inserted.len() as i32 {
|
||||||
|
let page = Instance::page(conn, (i, i + 1));
|
||||||
|
assert_eq!(page.len(), 1);
|
||||||
|
assert!(last_domaine <= page[0].public_domain);
|
||||||
|
last_domaine = page[0].public_domain.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blocked() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let inst_list = fill_database(conn);
|
||||||
|
let inst = &inst_list[0].1;
|
||||||
|
let inst_list = &inst_list[1..];
|
||||||
|
|
||||||
|
let blocked = inst.blocked;
|
||||||
|
inst.toggle_block(conn);
|
||||||
|
let inst = Instance::get(conn, inst.id).unwrap();
|
||||||
|
assert_eq!(inst.blocked, !blocked);
|
||||||
|
assert_eq!(
|
||||||
|
inst_list
|
||||||
|
.iter()
|
||||||
|
.filter(
|
||||||
|
|(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked
|
||||||
|
)
|
||||||
|
.count(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Instance::is_blocked(conn, format!("https://{}/something", inst.public_domain)),
|
||||||
|
inst.blocked
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Instance::is_blocked(conn, format!("https://{}a/something", inst.public_domain)),
|
||||||
|
Instance::find_by_domain(conn, format!("{}a", inst.public_domain))
|
||||||
|
.map(|inst| inst.blocked)
|
||||||
|
.unwrap_or(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
inst.toggle_block(conn);
|
||||||
|
let inst = Instance::get(conn, inst.id).unwrap();
|
||||||
|
assert_eq!(inst.blocked, blocked);
|
||||||
|
assert_eq!(
|
||||||
|
Instance::is_blocked(conn, format!("https://{}/something", inst.public_domain)),
|
||||||
|
inst.blocked
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Instance::is_blocked(conn, format!("https://{}a/something", inst.public_domain)),
|
||||||
|
Instance::find_by_domain(conn, format!("{}a", inst.public_domain))
|
||||||
|
.map(|inst| inst.blocked)
|
||||||
|
.unwrap_or(false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
inst_list
|
||||||
|
.iter()
|
||||||
|
.filter(
|
||||||
|
|(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked
|
||||||
|
)
|
||||||
|
.count(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let inst = &fill_database(conn)[0].1;
|
||||||
|
|
||||||
|
inst.update(
|
||||||
|
conn,
|
||||||
|
"NewName".to_owned(),
|
||||||
|
false,
|
||||||
|
SafeString::new("[short](#link)"),
|
||||||
|
SafeString::new("[long_description](/with_link)"),
|
||||||
|
);
|
||||||
|
let inst = Instance::get(conn, inst.id).unwrap();
|
||||||
|
assert_eq!(inst.name, "NewName".to_owned());
|
||||||
|
assert_eq!(inst.open_registrations, false);
|
||||||
|
assert_eq!(
|
||||||
|
inst.long_description.get(),
|
||||||
|
"[long_description](/with_link)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
inst.long_description_html,
|
||||||
|
"<p><a href=\"/with_link\">long_description</a></p>\n"
|
||||||
|
);
|
||||||
|
assert_eq!(inst.short_description.get(), "[short](#link)");
|
||||||
|
assert_eq!(
|
||||||
|
inst.short_description_html,
|
||||||
|
"<p><a href=\"#link\">short</a></p>\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,10 @@ extern crate serde_json;
|
||||||
extern crate url;
|
extern crate url;
|
||||||
extern crate webfinger;
|
extern crate webfinger;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
||||||
|
@ -99,11 +103,13 @@ macro_rules! list_by {
|
||||||
macro_rules! get {
|
macro_rules! get {
|
||||||
($table:ident) => {
|
($table:ident) => {
|
||||||
pub fn get(conn: &crate::Connection, id: i32) -> Option<Self> {
|
pub fn get(conn: &crate::Connection, id: i32) -> Option<Self> {
|
||||||
$table::table.filter($table::id.eq(id))
|
$table::table
|
||||||
|
.filter($table::id.eq(id))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
.expect("macro::get: Error loading $table by id")
|
.expect("macro::get: Error loading $table by id")
|
||||||
.into_iter().nth(0)
|
.into_iter()
|
||||||
|
.nth(0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -177,11 +183,16 @@ macro_rules! update {
|
||||||
macro_rules! last {
|
macro_rules! last {
|
||||||
($table:ident) => {
|
($table:ident) => {
|
||||||
pub fn last(conn: &crate::Connection) -> Self {
|
pub fn last(conn: &crate::Connection) -> Self {
|
||||||
$table::table.order_by($table::id.desc())
|
$table::table
|
||||||
|
.order_by($table::id.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
.expect(concat!("macro::last: Error getting last ", stringify!($table)))
|
.expect(concat!(
|
||||||
.iter().next()
|
"macro::last: Error getting last ",
|
||||||
|
stringify!($table)
|
||||||
|
))
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
.expect(concat!("macro::last: No last ", stringify!($table)))
|
.expect(concat!("macro::last: No last ", stringify!($table)))
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
@ -189,31 +200,67 @@ macro_rules! last {
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref BASE_URL: String = env::var("BASE_URL")
|
pub static ref BASE_URL: String = env::var("BASE_URL").unwrap_or(format!(
|
||||||
.unwrap_or(format!("127.0.0.1:{}", env::var("ROCKET_PORT").unwrap_or(String::from("8000"))));
|
"127.0.0.1:{}",
|
||||||
|
env::var("ROCKET_PORT").unwrap_or(String::from("8000"))
|
||||||
|
));
|
||||||
pub static ref USE_HTTPS: bool = env::var("USE_HTTPS").map(|val| val == "1").unwrap_or(true);
|
pub static ref USE_HTTPS: bool = env::var("USE_HTTPS").map(|val| val == "1").unwrap_or(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
static DB_NAME: &str = "plume";
|
||||||
|
#[cfg(test)]
|
||||||
|
static DB_NAME: &str = "plume_tests";
|
||||||
|
|
||||||
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref DATABASE_URL: String = env::var("DATABASE_URL").unwrap_or(String::from("postgres://plume:plume@localhost/plume"));
|
pub static ref DATABASE_URL: String =
|
||||||
|
env::var("DATABASE_URL").unwrap_or(format!("postgres://plume:plume@localhost/{}", DB_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref DATABASE_URL: String = env::var("DATABASE_URL").unwrap_or(String::from("plume.sqlite"));
|
pub static ref DATABASE_URL: String =
|
||||||
|
env::var("DATABASE_URL").unwrap_or(format!("{}.sqlite", DB_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ap_url(url: String) -> String {
|
pub fn ap_url(url: String) -> String {
|
||||||
let scheme = if *USE_HTTPS {
|
let scheme = if *USE_HTTPS { "https" } else { "http" };
|
||||||
"https"
|
|
||||||
} else {
|
|
||||||
"http"
|
|
||||||
};
|
|
||||||
format!("{}://{}", scheme, url)
|
format!("{}://{}", scheme, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
mod tests {
|
||||||
|
use diesel::Connection;
|
||||||
|
use Connection as Conn;
|
||||||
|
use DATABASE_URL;
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
embed_migrations!("../migrations/sqlite");
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
embed_migrations!("../migrations/postgres");
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! part_eq {
|
||||||
|
( $x:expr, $y:expr, [$( $var:ident ),*] ) => {
|
||||||
|
{
|
||||||
|
$(
|
||||||
|
assert_eq!($x.$var, $y.$var);
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn db() -> Conn {
|
||||||
|
let conn =
|
||||||
|
Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database");
|
||||||
|
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
||||||
|
conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod api_tokens;
|
pub mod api_tokens;
|
||||||
pub mod apps;
|
pub mod apps;
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
use activitypub::activity;
|
use activitypub::activity;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use plume_common::activity_pub::{
|
|
||||||
PUBLIC_VISIBILTY,
|
|
||||||
Id,
|
|
||||||
IntoId,
|
|
||||||
inbox::{FromActivity, Deletable, Notify}
|
|
||||||
};
|
|
||||||
use Connection;
|
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
|
use plume_common::activity_pub::{
|
||||||
|
inbox::{Deletable, FromActivity, Notify},
|
||||||
|
Id, IntoId, PUBLIC_VISIBILTY,
|
||||||
|
};
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use users::User;
|
|
||||||
use schema::likes;
|
use schema::likes;
|
||||||
|
use users::User;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
pub struct Like {
|
pub struct Like {
|
||||||
|
@ -20,7 +18,7 @@ pub struct Like {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub ap_url: String
|
pub ap_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
#[derive(Default, Insertable)]
|
||||||
|
@ -28,7 +26,7 @@ pub struct Like {
|
||||||
pub struct NewLike {
|
pub struct NewLike {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub ap_url: String
|
pub ap_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Like {
|
impl Like {
|
||||||
|
@ -45,17 +43,36 @@ impl Like {
|
||||||
User::get(conn, self.user_id).expect("Like::update_ap_url: user error").ap_url,
|
User::get(conn, self.user_id).expect("Like::update_ap_url: user error").ap_url,
|
||||||
Post::get(conn, self.post_id).expect("Like::update_ap_url: post error").ap_url
|
Post::get(conn, self.post_id).expect("Like::update_ap_url: post error").ap_url
|
||||||
)))
|
)))
|
||||||
.execute(conn).expect("Like::update_ap_url: update error");
|
.execute(conn)
|
||||||
|
.expect("Like::update_ap_url: update error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_activity(&self, conn: &Connection) -> activity::Like {
|
pub fn into_activity(&self, conn: &Connection) -> activity::Like {
|
||||||
let mut act = activity::Like::default();
|
let mut act = activity::Like::default();
|
||||||
act.like_props.set_actor_link(User::get(conn, self.user_id).expect("Like::into_activity: user error").into_id()).expect("Like::into_activity: actor error");
|
act.like_props
|
||||||
act.like_props.set_object_link(Post::get(conn, self.post_id).expect("Like::into_activity: post error").into_id()).expect("Like::into_activity: object error");
|
.set_actor_link(
|
||||||
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::into_activity: to error");
|
User::get(conn, self.user_id)
|
||||||
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Like::into_activity: cc error");
|
.expect("Like::into_activity: user error")
|
||||||
act.object_props.set_id_string(self.ap_url.clone()).expect("Like::into_activity: id error");
|
.into_id(),
|
||||||
|
)
|
||||||
|
.expect("Like::into_activity: actor error");
|
||||||
|
act.like_props
|
||||||
|
.set_object_link(
|
||||||
|
Post::get(conn, self.post_id)
|
||||||
|
.expect("Like::into_activity: post error")
|
||||||
|
.into_id(),
|
||||||
|
)
|
||||||
|
.expect("Like::into_activity: object error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
|
||||||
|
.expect("Like::into_activity: to error");
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Like::into_activity: cc error");
|
||||||
|
act.object_props
|
||||||
|
.set_id_string(self.ap_url.clone())
|
||||||
|
.expect("Like::into_activity: id error");
|
||||||
|
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
@ -63,13 +80,30 @@ impl Like {
|
||||||
|
|
||||||
impl FromActivity<activity::Like, Connection> for Like {
|
impl FromActivity<activity::Like, Connection> for Like {
|
||||||
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Like {
|
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Like {
|
||||||
let liker = User::from_url(conn, like.like_props.actor.as_str().expect("Like::from_activity: actor error").to_string());
|
let liker = User::from_url(
|
||||||
let post = Post::find_by_ap_url(conn, like.like_props.object.as_str().expect("Like::from_activity: object error").to_string());
|
conn,
|
||||||
let res = Like::insert(conn, NewLike {
|
like.like_props
|
||||||
|
.actor
|
||||||
|
.as_str()
|
||||||
|
.expect("Like::from_activity: actor error")
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
let post = Post::find_by_ap_url(
|
||||||
|
conn,
|
||||||
|
like.like_props
|
||||||
|
.object
|
||||||
|
.as_str()
|
||||||
|
.expect("Like::from_activity: object error")
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
let res = Like::insert(
|
||||||
|
conn,
|
||||||
|
NewLike {
|
||||||
post_id: post.expect("Like::from_activity: post error").id,
|
post_id: post.expect("Like::from_activity: post error").id,
|
||||||
user_id: liker.expect("Like::from_activity: user error").id,
|
user_id: liker.expect("Like::from_activity: user error").id,
|
||||||
ap_url: like.object_props.id_string().unwrap_or(String::from(""))
|
ap_url: like.object_props.id_string().unwrap_or(String::from("")),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
res.notify(conn);
|
res.notify(conn);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -79,30 +113,51 @@ impl Notify<Connection> for Like {
|
||||||
fn notify(&self, conn: &Connection) {
|
fn notify(&self, conn: &Connection) {
|
||||||
let post = Post::get(conn, self.post_id).expect("Like::notify: post error");
|
let post = Post::get(conn, self.post_id).expect("Like::notify: post error");
|
||||||
for author in post.get_authors(conn) {
|
for author in post.get_authors(conn) {
|
||||||
Notification::insert(conn, NewNotification {
|
Notification::insert(
|
||||||
|
conn,
|
||||||
|
NewNotification {
|
||||||
kind: notification_kind::LIKE.to_string(),
|
kind: notification_kind::LIKE.to_string(),
|
||||||
object_id: self.id,
|
object_id: self.id,
|
||||||
user_id: author.id
|
user_id: author.id,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deletable<Connection, activity::Undo> for Like {
|
impl Deletable<Connection, activity::Undo> for Like {
|
||||||
fn delete(&self, conn: &Connection) -> activity::Undo {
|
fn delete(&self, conn: &Connection) -> activity::Undo {
|
||||||
diesel::delete(self).execute(conn).expect("Like::delete: delete error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Like::delete: delete error");
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Some(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
if let Some(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
||||||
diesel::delete(¬if).execute(conn).expect("Like::delete: notification error");
|
diesel::delete(¬if)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Like::delete: notification error");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut act = activity::Undo::default();
|
let mut act = activity::Undo::default();
|
||||||
act.undo_props.set_actor_link(User::get(conn, self.user_id).expect("Like::delete: user error").into_id()).expect("Like::delete: actor error");
|
act.undo_props
|
||||||
act.undo_props.set_object_object(self.into_activity(conn)).expect("Like::delete: object error");
|
.set_actor_link(
|
||||||
act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Like::delete: id error");
|
User::get(conn, self.user_id)
|
||||||
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::delete: to error");
|
.expect("Like::delete: user error")
|
||||||
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Like::delete: cc error");
|
.into_id(),
|
||||||
|
)
|
||||||
|
.expect("Like::delete: actor error");
|
||||||
|
act.undo_props
|
||||||
|
.set_object_object(self.into_activity(conn))
|
||||||
|
.expect("Like::delete: object error");
|
||||||
|
act.object_props
|
||||||
|
.set_id_string(format!("{}#delete", self.ap_url))
|
||||||
|
.expect("Like::delete: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
|
||||||
|
.expect("Like::delete: to error");
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Like::delete: cc error");
|
||||||
|
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use activitypub::object::Image;
|
use activitypub::object::Image;
|
||||||
use diesel::{self, QueryDsl, ExpressionMethods, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use guid_create::GUID;
|
use guid_create::GUID;
|
||||||
use reqwest;
|
use reqwest;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
@ -7,10 +7,10 @@ use std::{fs, path::Path};
|
||||||
|
|
||||||
use plume_common::activity_pub::Id;
|
use plume_common::activity_pub::Id;
|
||||||
|
|
||||||
use {ap_url, Connection};
|
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use users::User;
|
|
||||||
use schema::medias;
|
use schema::medias;
|
||||||
|
use users::User;
|
||||||
|
use {ap_url, Connection};
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable, Serialize)]
|
#[derive(Clone, Identifiable, Queryable, Serialize)]
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
|
@ -21,7 +21,7 @@ pub struct Media {
|
||||||
pub remote_url: Option<String>,
|
pub remote_url: Option<String>,
|
||||||
pub sensitive: bool,
|
pub sensitive: bool,
|
||||||
pub content_warning: Option<String>,
|
pub content_warning: Option<String>,
|
||||||
pub owner_id: i32
|
pub owner_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
|
@ -33,7 +33,7 @@ pub struct NewMedia {
|
||||||
pub remote_url: Option<String>,
|
pub remote_url: Option<String>,
|
||||||
pub sensitive: bool,
|
pub sensitive: bool,
|
||||||
pub content_warning: Option<String>,
|
pub content_warning: Option<String>,
|
||||||
pub owner_id: i32
|
pub owner_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Media {
|
impl Media {
|
||||||
|
@ -41,29 +41,64 @@ impl Media {
|
||||||
get!(medias);
|
get!(medias);
|
||||||
list_by!(medias, for_user, owner_id as i32);
|
list_by!(medias, for_user, owner_id as i32);
|
||||||
|
|
||||||
|
pub fn list_all_medias(conn: &Connection) -> Vec<Media> {
|
||||||
|
medias::table
|
||||||
|
.load::<Media>(conn)
|
||||||
|
.expect("Media::list_all_medias: loading error")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
|
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
|
||||||
let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error");
|
let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error");
|
||||||
let url = self.url(conn);
|
let url = self.url(conn);
|
||||||
let (cat, preview, html, md) = match self.file_path.rsplitn(2, '.').next().expect("Media::to_json: extension error") {
|
let (cat, preview, html, md) = match self
|
||||||
|
.file_path
|
||||||
|
.rsplitn(2, '.')
|
||||||
|
.next()
|
||||||
|
.expect("Media::to_json: extension error")
|
||||||
|
{
|
||||||
"png" | "jpg" | "jpeg" | "gif" | "svg" => (
|
"png" | "jpg" | "jpeg" | "gif" | "svg" => (
|
||||||
"image",
|
"image",
|
||||||
format!("<img src=\"{}\" alt=\"{}\" title=\"{}\" class=\"preview\">", url, self.alt_text, self.alt_text),
|
format!(
|
||||||
format!("<img src=\"{}\" alt=\"{}\" title=\"{}\">", url, self.alt_text, self.alt_text),
|
"<img src=\"{}\" alt=\"{}\" title=\"{}\" class=\"preview\">",
|
||||||
|
url, self.alt_text, self.alt_text
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"<img src=\"{}\" alt=\"{}\" title=\"{}\">",
|
||||||
|
url, self.alt_text, self.alt_text
|
||||||
|
),
|
||||||
format!("![{}]({})", self.alt_text, url),
|
format!("![{}]({})", self.alt_text, url),
|
||||||
),
|
),
|
||||||
"mp3" | "wav" | "flac" => (
|
"mp3" | "wav" | "flac" => (
|
||||||
"audio",
|
"audio",
|
||||||
format!("<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>", url, self.alt_text),
|
format!(
|
||||||
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text),
|
"<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>",
|
||||||
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text),
|
url, self.alt_text
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"<audio src=\"{}\" title=\"{}\"></audio>",
|
||||||
|
url, self.alt_text
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"<audio src=\"{}\" title=\"{}\"></audio>",
|
||||||
|
url, self.alt_text
|
||||||
|
),
|
||||||
),
|
),
|
||||||
"mp4" | "avi" | "webm" | "mov" => (
|
"mp4" | "avi" | "webm" | "mov" => (
|
||||||
"video",
|
"video",
|
||||||
format!("<video src=\"{}\" title=\"{}\" class=\"preview\"></video>", url, self.alt_text),
|
format!(
|
||||||
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text),
|
"<video src=\"{}\" title=\"{}\" class=\"preview\"></video>",
|
||||||
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text),
|
url, self.alt_text
|
||||||
),
|
),
|
||||||
_ => ("unknown", String::new(), String::new(), String::new())
|
format!(
|
||||||
|
"<video src=\"{}\" title=\"{}\"></video>",
|
||||||
|
url, self.alt_text
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"<video src=\"{}\" title=\"{}\"></video>",
|
||||||
|
url, self.alt_text
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => ("unknown", String::new(), String::new(), String::new()),
|
||||||
};
|
};
|
||||||
json["html_preview"] = json!(preview);
|
json["html_preview"] = json!(preview);
|
||||||
json["html"] = json!(html);
|
json["html"] = json!(html);
|
||||||
|
@ -77,30 +112,43 @@ impl Media {
|
||||||
if self.is_remote {
|
if self.is_remote {
|
||||||
self.remote_url.clone().unwrap_or(String::new())
|
self.remote_url.clone().unwrap_or(String::new())
|
||||||
} else {
|
} else {
|
||||||
ap_url(format!("{}/{}", Instance::get_local(conn).expect("Media::url: local instance not found error").public_domain, self.file_path))
|
ap_url(format!(
|
||||||
|
"{}/{}",
|
||||||
|
Instance::get_local(conn)
|
||||||
|
.expect("Media::url: local instance not found error")
|
||||||
|
.public_domain,
|
||||||
|
self.file_path
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) {
|
pub fn delete(&self, conn: &Connection) {
|
||||||
|
if !self.is_remote {
|
||||||
fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error");
|
fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error");
|
||||||
diesel::delete(self).execute(conn).expect("Media::delete: database entry deletion error");
|
}
|
||||||
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Media::delete: database entry deletion error");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_remote(conn: &Connection, url: String) -> Media {
|
pub fn save_remote(conn: &Connection, url: String, user: &User) -> Media {
|
||||||
Media::insert(conn, NewMedia {
|
Media::insert(
|
||||||
|
conn,
|
||||||
|
NewMedia {
|
||||||
file_path: String::new(),
|
file_path: String::new(),
|
||||||
alt_text: String::new(),
|
alt_text: String::new(),
|
||||||
is_remote: true,
|
is_remote: true,
|
||||||
remote_url: Some(url),
|
remote_url: Some(url),
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
content_warning: None,
|
content_warning: None,
|
||||||
owner_id: 1 // It will be owned by the admin during an instant, but set_owner will be called just after
|
owner_id: user.id,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_owner(&self, conn: &Connection, id: i32) {
|
pub fn set_owner(&self, conn: &Connection, user: &User) {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(medias::owner_id.eq(id))
|
.set(medias::owner_id.eq(user.id))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.expect("Media::set_owner: owner update error");
|
.expect("Media::set_owner: owner update error");
|
||||||
}
|
}
|
||||||
|
@ -108,21 +156,199 @@ impl Media {
|
||||||
// TODO: merge with save_remote?
|
// TODO: merge with save_remote?
|
||||||
pub fn from_activity(conn: &Connection, image: Image) -> Option<Media> {
|
pub fn from_activity(conn: &Connection, image: Image) -> Option<Media> {
|
||||||
let remote_url = image.object_props.url_string().ok()?;
|
let remote_url = image.object_props.url_string().ok()?;
|
||||||
let ext = remote_url.rsplit('.').next().map(|ext| ext.to_owned()).unwrap_or("png".to_owned());
|
let ext = remote_url
|
||||||
let path = Path::new("static").join("media").join(format!("{}.{}", GUID::rand().to_string(), ext));
|
.rsplit('.')
|
||||||
|
.next()
|
||||||
|
.map(|ext| ext.to_owned())
|
||||||
|
.unwrap_or("png".to_owned());
|
||||||
|
let path =
|
||||||
|
Path::new("static")
|
||||||
|
.join("media")
|
||||||
|
.join(format!("{}.{}", GUID::rand().to_string(), ext));
|
||||||
|
|
||||||
let mut dest = fs::File::create(path.clone()).ok()?;
|
let mut dest = fs::File::create(path.clone()).ok()?;
|
||||||
reqwest::get(remote_url.as_str()).ok()?
|
reqwest::get(remote_url.as_str())
|
||||||
.copy_to(&mut dest).ok()?;
|
.ok()?
|
||||||
|
.copy_to(&mut dest)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
Some(Media::insert(conn, NewMedia {
|
Some(Media::insert(
|
||||||
|
conn,
|
||||||
|
NewMedia {
|
||||||
file_path: path.to_str()?.to_string(),
|
file_path: path.to_str()?.to_string(),
|
||||||
alt_text: image.object_props.content_string().ok()?,
|
alt_text: image.object_props.content_string().ok()?,
|
||||||
is_remote: true,
|
is_remote: true,
|
||||||
remote_url: None,
|
remote_url: None,
|
||||||
sensitive: image.object_props.summary_string().is_ok(),
|
sensitive: image.object_props.summary_string().is_ok(),
|
||||||
content_warning: image.object_props.summary_string().ok(),
|
content_warning: image.object_props.summary_string().ok(),
|
||||||
owner_id: User::from_url(conn, image.object_props.attributed_to_link_vec::<Id>().ok()?.into_iter().next()?.into())?.id
|
owner_id: User::from_url(
|
||||||
}))
|
conn,
|
||||||
|
image
|
||||||
|
.object_props
|
||||||
|
.attributed_to_link_vec::<Id>()
|
||||||
|
.ok()?
|
||||||
|
.into_iter()
|
||||||
|
.next()?
|
||||||
|
.into(),
|
||||||
|
)?.id,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use super::*;
|
||||||
|
use diesel::Connection;
|
||||||
|
use std::env::{current_dir, set_current_dir};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use tests::db;
|
||||||
|
use users::tests as usersTests;
|
||||||
|
use Connection as Conn;
|
||||||
|
|
||||||
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<Media> {
|
||||||
|
let mut wd = current_dir().unwrap().to_path_buf();
|
||||||
|
while wd.pop() {
|
||||||
|
if wd.join(".git").exists() {
|
||||||
|
set_current_dir(wd).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let users = usersTests::fill_database(conn);
|
||||||
|
let user_one = users[0].id;
|
||||||
|
let user_two = users[1].id;
|
||||||
|
let f1 = "static/media/1.png".to_owned();
|
||||||
|
let f2 = "static/media/2.mp3".to_owned();
|
||||||
|
fs::write(f1.clone(), []).unwrap();
|
||||||
|
fs::write(f2.clone(), []).unwrap();
|
||||||
|
vec![
|
||||||
|
NewMedia {
|
||||||
|
file_path: f1,
|
||||||
|
alt_text: "some alt".to_owned(),
|
||||||
|
is_remote: false,
|
||||||
|
remote_url: None,
|
||||||
|
sensitive: false,
|
||||||
|
content_warning: None,
|
||||||
|
owner_id: user_one,
|
||||||
|
},
|
||||||
|
NewMedia {
|
||||||
|
file_path: f2,
|
||||||
|
alt_text: "alt message".to_owned(),
|
||||||
|
is_remote: false,
|
||||||
|
remote_url: None,
|
||||||
|
sensitive: true,
|
||||||
|
content_warning: Some("Content warning".to_owned()),
|
||||||
|
owner_id: user_one,
|
||||||
|
},
|
||||||
|
NewMedia {
|
||||||
|
file_path: "".to_owned(),
|
||||||
|
alt_text: "another alt".to_owned(),
|
||||||
|
is_remote: true,
|
||||||
|
remote_url: Some("https://example.com/".to_owned()),
|
||||||
|
sensitive: false,
|
||||||
|
content_warning: None,
|
||||||
|
owner_id: user_two,
|
||||||
|
},
|
||||||
|
].into_iter()
|
||||||
|
.map(|nm| Media::insert(conn, nm))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clean(conn: &Conn) {
|
||||||
|
//used to remove files generated by tests
|
||||||
|
for media in Media::list_all_medias(conn) {
|
||||||
|
media.delete(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//set_owner
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let user = usersTests::fill_database(conn)[0].id;
|
||||||
|
fill_database(conn);
|
||||||
|
|
||||||
|
let path = "static/media/test_deletion".to_owned();
|
||||||
|
fs::write(path.clone(), []).unwrap();
|
||||||
|
|
||||||
|
let media = Media::insert(
|
||||||
|
conn,
|
||||||
|
NewMedia {
|
||||||
|
file_path: path.clone(),
|
||||||
|
alt_text: "alt message".to_owned(),
|
||||||
|
is_remote: false,
|
||||||
|
remote_url: None,
|
||||||
|
sensitive: false,
|
||||||
|
content_warning: None,
|
||||||
|
owner_id: user,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(Path::new(&path).exists());
|
||||||
|
media.delete(conn);
|
||||||
|
assert!(!Path::new(&path).exists());
|
||||||
|
|
||||||
|
clean(conn);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
|
||||||
|
fn set_owner() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let users = usersTests::fill_database(conn);
|
||||||
|
let u1 = &users[0];
|
||||||
|
let u2 = &users[1];
|
||||||
|
fill_database(conn);
|
||||||
|
|
||||||
|
let path = "static/media/test_set_owner".to_owned();
|
||||||
|
fs::write(path.clone(), []).unwrap();
|
||||||
|
|
||||||
|
let media = Media::insert(
|
||||||
|
conn,
|
||||||
|
NewMedia {
|
||||||
|
file_path: path.clone(),
|
||||||
|
alt_text: "alt message".to_owned(),
|
||||||
|
is_remote: false,
|
||||||
|
remote_url: None,
|
||||||
|
sensitive: false,
|
||||||
|
content_warning: None,
|
||||||
|
owner_id: u1.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
Media::for_user(conn, u1.id)
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.id == media.id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!Media::for_user(conn, u2.id)
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.id == media.id)
|
||||||
|
);
|
||||||
|
media.set_owner(conn, u2);
|
||||||
|
assert!(
|
||||||
|
!Media::for_user(conn, u1.id)
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.id == media.id)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
Media::for_user(conn, u2.id)
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.id == media.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
clean(conn);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use activitypub::link;
|
use activitypub::link;
|
||||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use plume_common::activity_pub::inbox::Notify;
|
|
||||||
use Connection;
|
|
||||||
use comments::Comment;
|
use comments::Comment;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
|
use plume_common::activity_pub::inbox::Notify;
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use users::User;
|
|
||||||
use schema::mentions;
|
use schema::mentions;
|
||||||
|
use users::User;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)]
|
#[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)]
|
||||||
pub struct Mention {
|
pub struct Mention {
|
||||||
|
@ -15,7 +15,7 @@ pub struct Mention {
|
||||||
pub mentioned_id: i32,
|
pub mentioned_id: i32,
|
||||||
pub post_id: Option<i32>,
|
pub post_id: Option<i32>,
|
||||||
pub comment_id: Option<i32>,
|
pub comment_id: Option<i32>,
|
||||||
pub ap_url: String // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake
|
pub ap_url: String, // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
|
@ -24,7 +24,7 @@ pub struct NewMention {
|
||||||
pub mentioned_id: i32,
|
pub mentioned_id: i32,
|
||||||
pub post_id: Option<i32>,
|
pub post_id: Option<i32>,
|
||||||
pub comment_id: Option<i32>,
|
pub comment_id: Option<i32>,
|
||||||
pub ap_url: String
|
pub ap_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mention {
|
impl Mention {
|
||||||
|
@ -50,38 +50,62 @@ impl Mention {
|
||||||
pub fn get_user(&self, conn: &Connection) -> Option<User> {
|
pub fn get_user(&self, conn: &Connection) -> Option<User> {
|
||||||
match self.get_post(conn) {
|
match self.get_post(conn) {
|
||||||
Some(p) => p.get_authors(conn).into_iter().next(),
|
Some(p) => p.get_authors(conn).into_iter().next(),
|
||||||
None => self.get_comment(conn).map(|c| c.get_author(conn))
|
None => self.get_comment(conn).map(|c| c.get_author(conn)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_activity(conn: &Connection, ment: String) -> link::Mention {
|
pub fn build_activity(conn: &Connection, ment: String) -> link::Mention {
|
||||||
let user = User::find_by_fqn(conn, ment.clone());
|
let user = User::find_by_fqn(conn, ment.clone());
|
||||||
let mut mention = link::Mention::default();
|
let mut mention = link::Mention::default();
|
||||||
mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Mention::build_activity: href error");
|
mention
|
||||||
mention.link_props.set_name_string(format!("@{}", ment)).expect("Mention::build_activity: name error:");
|
.link_props
|
||||||
|
.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new()))
|
||||||
|
.expect("Mention::build_activity: href error");
|
||||||
|
mention
|
||||||
|
.link_props
|
||||||
|
.set_name_string(format!("@{}", ment))
|
||||||
|
.expect("Mention::build_activity: name error:");
|
||||||
mention
|
mention
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> link::Mention {
|
pub fn to_activity(&self, conn: &Connection) -> link::Mention {
|
||||||
let user = self.get_mentioned(conn);
|
let user = self.get_mentioned(conn);
|
||||||
let mut mention = link::Mention::default();
|
let mut mention = link::Mention::default();
|
||||||
mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Mention::to_activity: href error");
|
mention
|
||||||
mention.link_props.set_name_string(user.map(|u| format!("@{}", u.get_fqn(conn))).unwrap_or(String::new())).expect("Mention::to_activity: mention error");
|
.link_props
|
||||||
|
.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new()))
|
||||||
|
.expect("Mention::to_activity: href error");
|
||||||
|
mention
|
||||||
|
.link_props
|
||||||
|
.set_name_string(
|
||||||
|
user.map(|u| format!("@{}", u.get_fqn(conn)))
|
||||||
|
.unwrap_or(String::new()),
|
||||||
|
)
|
||||||
|
.expect("Mention::to_activity: mention error");
|
||||||
mention
|
mention
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_activity(conn: &Connection, ment: link::Mention, inside: i32, in_post: bool, notify: bool) -> Option<Self> {
|
pub fn from_activity(
|
||||||
|
conn: &Connection,
|
||||||
|
ment: link::Mention,
|
||||||
|
inside: i32,
|
||||||
|
in_post: bool,
|
||||||
|
notify: bool,
|
||||||
|
) -> Option<Self> {
|
||||||
let ap_url = ment.link_props.href_string().ok()?;
|
let ap_url = ment.link_props.href_string().ok()?;
|
||||||
let mentioned = User::find_by_ap_url(conn, ap_url)?;
|
let mentioned = User::find_by_ap_url(conn, ap_url)?;
|
||||||
|
|
||||||
if in_post {
|
if in_post {
|
||||||
Post::get(conn, inside.clone().into()).map(|post| {
|
Post::get(conn, inside.clone().into()).map(|post| {
|
||||||
let res = Mention::insert(conn, NewMention {
|
let res = Mention::insert(
|
||||||
|
conn,
|
||||||
|
NewMention {
|
||||||
mentioned_id: mentioned.id,
|
mentioned_id: mentioned.id,
|
||||||
post_id: Some(post.id),
|
post_id: Some(post.id),
|
||||||
comment_id: None,
|
comment_id: None,
|
||||||
ap_url: ment.link_props.href_string().unwrap_or(String::new())
|
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if notify {
|
if notify {
|
||||||
res.notify(conn);
|
res.notify(conn);
|
||||||
}
|
}
|
||||||
|
@ -89,12 +113,15 @@ impl Mention {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Comment::get(conn, inside.into()).map(|comment| {
|
Comment::get(conn, inside.into()).map(|comment| {
|
||||||
let res = Mention::insert(conn, NewMention {
|
let res = Mention::insert(
|
||||||
|
conn,
|
||||||
|
NewMention {
|
||||||
mentioned_id: mentioned.id,
|
mentioned_id: mentioned.id,
|
||||||
post_id: None,
|
post_id: None,
|
||||||
comment_id: Some(comment.id),
|
comment_id: Some(comment.id),
|
||||||
ap_url: ment.link_props.href_string().unwrap_or(String::new())
|
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if notify {
|
if notify {
|
||||||
res.notify(conn);
|
res.notify(conn);
|
||||||
}
|
}
|
||||||
|
@ -106,18 +133,23 @@ impl Mention {
|
||||||
pub fn delete(&self, conn: &Connection) {
|
pub fn delete(&self, conn: &Connection) {
|
||||||
//find related notifications and delete them
|
//find related notifications and delete them
|
||||||
Notification::find(conn, notification_kind::MENTION, self.id).map(|n| n.delete(conn));
|
Notification::find(conn, notification_kind::MENTION, self.id).map(|n| n.delete(conn));
|
||||||
diesel::delete(self).execute(conn).expect("Mention::delete: mention deletion error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Mention::delete: mention deletion error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Notify<Connection> for Mention {
|
impl Notify<Connection> for Mention {
|
||||||
fn notify(&self, conn: &Connection) {
|
fn notify(&self, conn: &Connection) {
|
||||||
self.get_mentioned(conn).map(|m| {
|
self.get_mentioned(conn).map(|m| {
|
||||||
Notification::insert(conn, NewNotification {
|
Notification::insert(
|
||||||
|
conn,
|
||||||
|
NewNotification {
|
||||||
kind: notification_kind::MENTION.to_string(),
|
kind: notification_kind::MENTION.to_string(),
|
||||||
object_id: self.id,
|
object_id: self.id,
|
||||||
user_id: m.id
|
user_id: m.id,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use Connection;
|
|
||||||
use comments::Comment;
|
use comments::Comment;
|
||||||
use follows::Follow;
|
use follows::Follow;
|
||||||
use likes::Like;
|
use likes::Like;
|
||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use reshares::Reshare;
|
use reshares::Reshare;
|
||||||
use users::User;
|
|
||||||
use schema::notifications;
|
use schema::notifications;
|
||||||
|
use users::User;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
pub mod notification_kind {
|
pub mod notification_kind {
|
||||||
pub const COMMENT: &'static str = "COMMENT";
|
pub const COMMENT: &'static str = "COMMENT";
|
||||||
|
@ -26,7 +26,7 @@ pub struct Notification {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub object_id: i32
|
pub object_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
|
@ -34,7 +34,7 @@ pub struct Notification {
|
||||||
pub struct NewNotification {
|
pub struct NewNotification {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub object_id: i32
|
pub object_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Notification {
|
impl Notification {
|
||||||
|
@ -42,14 +42,20 @@ impl Notification {
|
||||||
get!(notifications);
|
get!(notifications);
|
||||||
|
|
||||||
pub fn find_for_user(conn: &Connection, user: &User) -> Vec<Notification> {
|
pub fn find_for_user(conn: &Connection, user: &User) -> Vec<Notification> {
|
||||||
notifications::table.filter(notifications::user_id.eq(user.id))
|
notifications::table
|
||||||
|
.filter(notifications::user_id.eq(user.id))
|
||||||
.order_by(notifications::creation_date.desc())
|
.order_by(notifications::creation_date.desc())
|
||||||
.load::<Notification>(conn)
|
.load::<Notification>(conn)
|
||||||
.expect("Notification::find_for_user: notification loading error")
|
.expect("Notification::find_for_user: notification loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Vec<Notification> {
|
pub fn page_for_user(
|
||||||
notifications::table.filter(notifications::user_id.eq(user.id))
|
conn: &Connection,
|
||||||
|
user: &User,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Vec<Notification> {
|
||||||
|
notifications::table
|
||||||
|
.filter(notifications::user_id.eq(user.id))
|
||||||
.order_by(notifications::creation_date.desc())
|
.order_by(notifications::creation_date.desc())
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
.limit((max - min).into())
|
.limit((max - min).into())
|
||||||
|
@ -58,7 +64,8 @@ impl Notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Option<Notification> {
|
pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Option<Notification> {
|
||||||
notifications::table.filter(notifications::kind.eq(kind.into()))
|
notifications::table
|
||||||
|
.filter(notifications::kind.eq(kind.into()))
|
||||||
.filter(notifications::object_id.eq(obj))
|
.filter(notifications::object_id.eq(obj))
|
||||||
.get_result::<Notification>(conn)
|
.get_result::<Notification>(conn)
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -67,25 +74,23 @@ impl Notification {
|
||||||
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
|
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
|
||||||
let mut json = json!(self);
|
let mut json = json!(self);
|
||||||
json["object"] = json!(match self.kind.as_ref() {
|
json["object"] = json!(match self.kind.as_ref() {
|
||||||
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment|
|
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| json!({
|
||||||
json!({
|
|
||||||
"post": comment.get_post(conn).to_json(conn),
|
"post": comment.get_post(conn).to_json(conn),
|
||||||
"user": comment.get_author(conn).to_json(conn),
|
"user": comment.get_author(conn).to_json(conn),
|
||||||
"id": comment.id
|
"id": comment.id
|
||||||
})
|
})),
|
||||||
),
|
notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow| {
|
||||||
notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow|
|
|
||||||
json!({
|
json!({
|
||||||
"follower": User::get(conn, follow.follower_id).map(|u| u.to_json(conn))
|
"follower": User::get(conn, follow.follower_id).map(|u| u.to_json(conn))
|
||||||
})
|
})
|
||||||
),
|
}),
|
||||||
notification_kind::LIKE => Like::get(conn, self.object_id).map(|like|
|
notification_kind::LIKE => Like::get(conn, self.object_id).map(|like| {
|
||||||
json!({
|
json!({
|
||||||
"post": Post::get(conn, like.post_id).map(|p| p.to_json(conn)),
|
"post": Post::get(conn, like.post_id).map(|p| p.to_json(conn)),
|
||||||
"user": User::get(conn, like.user_id).map(|u| u.to_json(conn))
|
"user": User::get(conn, like.user_id).map(|u| u.to_json(conn))
|
||||||
})
|
})
|
||||||
),
|
}),
|
||||||
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention|
|
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| {
|
||||||
json!({
|
json!({
|
||||||
"user": mention.get_user(conn).map(|u| u.to_json(conn)),
|
"user": mention.get_user(conn).map(|u| u.to_json(conn)),
|
||||||
"url": mention.get_post(conn).map(|p| p.to_json(conn)["url"].clone())
|
"url": mention.get_post(conn).map(|p| p.to_json(conn)["url"].clone())
|
||||||
|
@ -95,19 +100,21 @@ impl Notification {
|
||||||
json!(format!("{}#comment-{}", post["url"].as_str().expect("Notification::to_json: post url error"), comment.id))
|
json!(format!("{}#comment-{}", post["url"].as_str().expect("Notification::to_json: post url error"), comment.id))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
),
|
}),
|
||||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare|
|
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| {
|
||||||
json!({
|
json!({
|
||||||
"post": reshare.get_post(conn).map(|p| p.to_json(conn)),
|
"post": reshare.get_post(conn).map(|p| p.to_json(conn)),
|
||||||
"user": reshare.get_user(conn).map(|u| u.to_json(conn))
|
"user": reshare.get_user(conn).map(|u| u.to_json(conn))
|
||||||
})
|
})
|
||||||
),
|
}),
|
||||||
_ => Some(json!({}))
|
_ => Some(json!({})),
|
||||||
});
|
});
|
||||||
json
|
json
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) {
|
pub fn delete(&self, conn: &Connection) {
|
||||||
diesel::delete(self).execute(conn).expect("Notification::delete: notification deletion error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Notification::delete: notification deletion error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use users::User;
|
|
||||||
use schema::post_authors;
|
use schema::post_authors;
|
||||||
|
use users::User;
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, Associations)]
|
#[derive(Clone, Queryable, Identifiable, Associations)]
|
||||||
#[belongs_to(Post)]
|
#[belongs_to(Post)]
|
||||||
|
@ -10,14 +10,14 @@ use schema::post_authors;
|
||||||
pub struct PostAuthor {
|
pub struct PostAuthor {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub author_id: i32
|
pub author_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
#[table_name = "post_authors"]
|
#[table_name = "post_authors"]
|
||||||
pub struct NewPostAuthor {
|
pub struct NewPostAuthor {
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub author_id: i32
|
pub author_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostAuthor {
|
impl PostAuthor {
|
||||||
|
|
|
@ -1,36 +1,35 @@
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
activity::{Create, Delete, Update},
|
activity::{Create, Delete, Update},
|
||||||
link,
|
link,
|
||||||
object::{Article, Image, Tombstone}
|
object::{Article, Image, Tombstone},
|
||||||
};
|
};
|
||||||
use canapi::{Error, Provider};
|
use canapi::{Error, Provider};
|
||||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods, BelongingToDsl};
|
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use heck::{CamelCase, KebabCase};
|
use heck::{CamelCase, KebabCase};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use plume_api::posts::PostEndpoint;
|
|
||||||
use plume_common::{
|
|
||||||
activity_pub::{
|
|
||||||
Hashtag, Source,
|
|
||||||
PUBLIC_VISIBILTY, Id, IntoId,
|
|
||||||
inbox::{Deletable, FromActivity}
|
|
||||||
},
|
|
||||||
utils::md_to_html
|
|
||||||
};
|
|
||||||
use {BASE_URL, ap_url, Connection};
|
|
||||||
use blogs::Blog;
|
use blogs::Blog;
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use likes::Like;
|
use likes::Like;
|
||||||
use medias::Media;
|
use medias::Media;
|
||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
|
use plume_api::posts::PostEndpoint;
|
||||||
|
use plume_common::{
|
||||||
|
activity_pub::{
|
||||||
|
inbox::{Deletable, FromActivity},
|
||||||
|
Hashtag, Id, IntoId, Source, PUBLIC_VISIBILTY,
|
||||||
|
},
|
||||||
|
utils::md_to_html,
|
||||||
|
};
|
||||||
use post_authors::*;
|
use post_authors::*;
|
||||||
use reshares::Reshare;
|
use reshares::Reshare;
|
||||||
|
use safe_string::SafeString;
|
||||||
|
use schema::posts;
|
||||||
|
use std::collections::HashSet;
|
||||||
use tags::Tag;
|
use tags::Tag;
|
||||||
use users::User;
|
use users::User;
|
||||||
use schema::posts;
|
use {ap_url, Connection, BASE_URL};
|
||||||
use safe_string::SafeString;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)]
|
#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
|
@ -68,24 +67,32 @@ pub struct NewPost {
|
||||||
impl<'a> Provider<(&'a Connection, Option<i32>)> for Post {
|
impl<'a> Provider<(&'a Connection, Option<i32>)> for Post {
|
||||||
type Data = PostEndpoint;
|
type Data = PostEndpoint;
|
||||||
|
|
||||||
fn get((conn, user_id): &(&'a Connection, Option<i32>), id: i32) -> Result<PostEndpoint, Error> {
|
fn get(
|
||||||
|
(conn, user_id): &(&'a Connection, Option<i32>),
|
||||||
|
id: i32,
|
||||||
|
) -> Result<PostEndpoint, Error> {
|
||||||
if let Some(post) = Post::get(conn, id) {
|
if let Some(post) = Post::get(conn, id) {
|
||||||
if !post.published && !user_id.map(|u| post.is_author(conn, u)).unwrap_or(false) {
|
if !post.published && !user_id.map(|u| post.is_author(conn, u)).unwrap_or(false) {
|
||||||
return Err(Error::Authorization("You are not authorized to access this post yet.".to_string()))
|
return Err(Error::Authorization(
|
||||||
|
"You are not authorized to access this post yet.".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PostEndpoint {
|
Ok(PostEndpoint {
|
||||||
id: Some(post.id),
|
id: Some(post.id),
|
||||||
title: Some(post.title.clone()),
|
title: Some(post.title.clone()),
|
||||||
subtitle: Some(post.subtitle.clone()),
|
subtitle: Some(post.subtitle.clone()),
|
||||||
content: Some(post.content.get().clone())
|
content: Some(post.content.get().clone()),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::NotFound("Request post was not found".to_string()))
|
Err(Error::NotFound("Request post was not found".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list((conn, user_id): &(&'a Connection, Option<i32>), filter: PostEndpoint) -> Vec<PostEndpoint> {
|
fn list(
|
||||||
|
(conn, user_id): &(&'a Connection, Option<i32>),
|
||||||
|
filter: PostEndpoint,
|
||||||
|
) -> Vec<PostEndpoint> {
|
||||||
let mut query = posts::table.into_boxed();
|
let mut query = posts::table.into_boxed();
|
||||||
if let Some(title) = filter.title {
|
if let Some(title) = filter.title {
|
||||||
query = query.filter(posts::title.eq(title));
|
query = query.filter(posts::title.eq(title));
|
||||||
|
@ -97,23 +104,36 @@ impl<'a> Provider<(&'a Connection, Option<i32>)> for Post {
|
||||||
query = query.filter(posts::content.eq(content));
|
query = query.filter(posts::content.eq(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
query.get_results::<Post>(*conn).map(|ps| ps.into_iter()
|
query
|
||||||
.filter(|p| p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false))
|
.get_results::<Post>(*conn)
|
||||||
|
.map(|ps| {
|
||||||
|
ps.into_iter()
|
||||||
|
.filter(|p| {
|
||||||
|
p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false)
|
||||||
|
})
|
||||||
.map(|p| PostEndpoint {
|
.map(|p| PostEndpoint {
|
||||||
id: Some(p.id),
|
id: Some(p.id),
|
||||||
title: Some(p.title.clone()),
|
title: Some(p.title.clone()),
|
||||||
subtitle: Some(p.subtitle.clone()),
|
subtitle: Some(p.subtitle.clone()),
|
||||||
content: Some(p.content.get().clone())
|
content: Some(p.content.get().clone()),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
).unwrap_or(vec![])
|
})
|
||||||
|
.unwrap_or(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create((_conn, _user_id): &(&'a Connection, Option<i32>), _query: PostEndpoint) -> Result<PostEndpoint, Error> {
|
fn create(
|
||||||
|
(_conn, _user_id): &(&'a Connection, Option<i32>),
|
||||||
|
_query: PostEndpoint,
|
||||||
|
) -> Result<PostEndpoint, Error> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update((_conn, _user_id): &(&'a Connection, Option<i32>), _id: i32, _new_data: PostEndpoint) -> Result<PostEndpoint, Error> {
|
fn update(
|
||||||
|
(_conn, _user_id): &(&'a Connection, Option<i32>),
|
||||||
|
_id: i32,
|
||||||
|
_new_data: PostEndpoint,
|
||||||
|
) -> Result<PostEndpoint, Error> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +158,8 @@ impl Post {
|
||||||
use schema::tags;
|
use schema::tags;
|
||||||
|
|
||||||
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
||||||
posts::table.filter(posts::id.eq_any(ids))
|
posts::table
|
||||||
|
.filter(posts::id.eq_any(ids))
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.order(posts::creation_date.desc())
|
.order(posts::creation_date.desc())
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
|
@ -150,35 +171,45 @@ impl Post {
|
||||||
pub fn count_for_tag(conn: &Connection, tag: String) -> i64 {
|
pub fn count_for_tag(conn: &Connection, tag: String) -> i64 {
|
||||||
use schema::tags;
|
use schema::tags;
|
||||||
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
||||||
*posts::table.filter(posts::id.eq_any(ids))
|
*posts::table
|
||||||
|
.filter(posts::id.eq_any(ids))
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.count()
|
.count()
|
||||||
.load(conn)
|
.load(conn)
|
||||||
.expect("Post::count_for_tag: counting error")
|
.expect("Post::count_for_tag: counting error")
|
||||||
.iter().next().expect("Post::count_for_tag: no result error")
|
.iter()
|
||||||
|
.next()
|
||||||
|
.expect("Post::count_for_tag: no result error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_local(conn: &Connection) -> usize {
|
pub fn count_local(conn: &Connection) -> usize {
|
||||||
use schema::post_authors;
|
use schema::post_authors;
|
||||||
use schema::users;
|
use schema::users;
|
||||||
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id);
|
let local_authors = users::table
|
||||||
let local_posts_id = post_authors::table.filter(post_authors::author_id.eq_any(local_authors)).select(post_authors::post_id);
|
.filter(users::instance_id.eq(Instance::local_id(conn)))
|
||||||
posts::table.filter(posts::id.eq_any(local_posts_id))
|
.select(users::id);
|
||||||
|
let local_posts_id = post_authors::table
|
||||||
|
.filter(post_authors::author_id.eq_any(local_authors))
|
||||||
|
.select(post_authors::post_id);
|
||||||
|
posts::table
|
||||||
|
.filter(posts::id.eq_any(local_posts_id))
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.load::<Post>(conn)
|
.load::<Post>(conn)
|
||||||
.expect("Post::count_local: loading error")
|
.expect("Post::count_local: loading error")
|
||||||
.len()// TODO count in database?
|
.len() // TODO count in database?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count(conn: &Connection) -> i64 {
|
pub fn count(conn: &Connection) -> i64 {
|
||||||
posts::table.filter(posts::published.eq(true))
|
posts::table
|
||||||
|
.filter(posts::published.eq(true))
|
||||||
.count()
|
.count()
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.expect("Post::count: counting error")
|
.expect("Post::count: counting error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_recents(conn: &Connection, limit: i64) -> Vec<Post> {
|
pub fn get_recents(conn: &Connection, limit: i64) -> Vec<Post> {
|
||||||
posts::table.order(posts::creation_date.desc())
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.load::<Post>(conn)
|
.load::<Post>(conn)
|
||||||
|
@ -189,7 +220,8 @@ impl Post {
|
||||||
use schema::post_authors;
|
use schema::post_authors;
|
||||||
|
|
||||||
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
||||||
posts::table.filter(posts::id.eq_any(posts))
|
posts::table
|
||||||
|
.filter(posts::id.eq_any(posts))
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.order(posts::creation_date.desc())
|
.order(posts::creation_date.desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
@ -198,7 +230,8 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Vec<Post> {
|
pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Vec<Post> {
|
||||||
posts::table.filter(posts::blog_id.eq(blog.id))
|
posts::table
|
||||||
|
.filter(posts::blog_id.eq(blog.id))
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.order(posts::creation_date.desc())
|
.order(posts::creation_date.desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
@ -206,15 +239,17 @@ impl Post {
|
||||||
.expect("Post::get_recents_for_blog: loading error")
|
.expect("Post::get_recents_for_blog: loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_for_blog(conn: &Connection, blog:&Blog) -> Vec<Post> {
|
pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Vec<Post> {
|
||||||
posts::table.filter(posts::blog_id.eq(blog.id))
|
posts::table
|
||||||
|
.filter(posts::blog_id.eq(blog.id))
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.load::<Post>(conn)
|
.load::<Post>(conn)
|
||||||
.expect("Post::get_for_blog:: loading error")
|
.expect("Post::get_for_blog:: loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Vec<Post> {
|
pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Vec<Post> {
|
||||||
posts::table.filter(posts::blog_id.eq(blog.id))
|
posts::table
|
||||||
|
.filter(posts::blog_id.eq(blog.id))
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.order(posts::creation_date.desc())
|
.order(posts::creation_date.desc())
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
|
@ -225,7 +260,8 @@ impl Post {
|
||||||
|
|
||||||
/// Give a page of all the recent posts known to this instance (= federated timeline)
|
/// Give a page of all the recent posts known to this instance (= federated timeline)
|
||||||
pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Post> {
|
pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Post> {
|
||||||
posts::table.order(posts::creation_date.desc())
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
.limit((max - min).into())
|
.limit((max - min).into())
|
||||||
|
@ -234,12 +270,19 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Give a page of posts from a specific instance
|
/// Give a page of posts from a specific instance
|
||||||
pub fn get_instance_page(conn: &Connection, instance_id: i32, (min, max): (i32, i32)) -> Vec<Post> {
|
pub fn get_instance_page(
|
||||||
|
conn: &Connection,
|
||||||
|
instance_id: i32,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Vec<Post> {
|
||||||
use schema::blogs;
|
use schema::blogs;
|
||||||
|
|
||||||
let blog_ids = blogs::table.filter(blogs::instance_id.eq(instance_id)).select(blogs::id);
|
let blog_ids = blogs::table
|
||||||
|
.filter(blogs::instance_id.eq(instance_id))
|
||||||
|
.select(blogs::id);
|
||||||
|
|
||||||
posts::table.order(posts::creation_date.desc())
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.filter(posts::blog_id.eq_any(blog_ids))
|
.filter(posts::blog_id.eq_any(blog_ids))
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
|
@ -249,13 +292,18 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Give a page of customized user feed, based on a list of followed users
|
/// Give a page of customized user feed, based on a list of followed users
|
||||||
pub fn user_feed_page(conn: &Connection, followed: Vec<i32>, (min, max): (i32, i32)) -> Vec<Post> {
|
pub fn user_feed_page(
|
||||||
|
conn: &Connection,
|
||||||
|
followed: Vec<i32>,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Vec<Post> {
|
||||||
use schema::post_authors;
|
use schema::post_authors;
|
||||||
let post_ids = post_authors::table
|
let post_ids = post_authors::table
|
||||||
.filter(post_authors::author_id.eq_any(followed))
|
.filter(post_authors::author_id.eq_any(followed))
|
||||||
.select(post_authors::post_id);
|
.select(post_authors::post_id);
|
||||||
|
|
||||||
posts::table.order(posts::creation_date.desc())
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.filter(posts::id.eq_any(post_ids))
|
.filter(posts::id.eq_any(post_ids))
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
|
@ -268,7 +316,8 @@ impl Post {
|
||||||
use schema::post_authors;
|
use schema::post_authors;
|
||||||
|
|
||||||
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
||||||
posts::table.order(posts::creation_date.desc())
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
.filter(posts::published.eq(false))
|
.filter(posts::published.eq(false))
|
||||||
.filter(posts::id.eq_any(posts))
|
.filter(posts::id.eq_any(posts))
|
||||||
.load::<Post>(conn)
|
.load::<Post>(conn)
|
||||||
|
@ -276,10 +325,13 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_authors(&self, conn: &Connection) -> Vec<User> {
|
pub fn get_authors(&self, conn: &Connection) -> Vec<User> {
|
||||||
use schema::users;
|
|
||||||
use schema::post_authors;
|
use schema::post_authors;
|
||||||
|
use schema::users;
|
||||||
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
|
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
|
||||||
users::table.filter(users::id.eq_any(author_list)).load::<User>(conn).expect("Post::get_authors: loading error")
|
users::table
|
||||||
|
.filter(users::id.eq_any(author_list))
|
||||||
|
.load::<User>(conn)
|
||||||
|
.expect("Post::get_authors: loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_author(&self, conn: &Connection, author_id: i32) -> bool {
|
pub fn is_author(&self, conn: &Connection, author_id: i32) -> bool {
|
||||||
|
@ -293,23 +345,28 @@ impl Post {
|
||||||
|
|
||||||
pub fn get_blog(&self, conn: &Connection) -> Blog {
|
pub fn get_blog(&self, conn: &Connection) -> Blog {
|
||||||
use schema::blogs;
|
use schema::blogs;
|
||||||
blogs::table.filter(blogs::id.eq(self.blog_id))
|
blogs::table
|
||||||
|
.filter(blogs::id.eq(self.blog_id))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.load::<Blog>(conn)
|
.load::<Blog>(conn)
|
||||||
.expect("Post::get_blog: loading error")
|
.expect("Post::get_blog: loading error")
|
||||||
.into_iter().nth(0).expect("Post::get_blog: no result error")
|
.into_iter()
|
||||||
|
.nth(0)
|
||||||
|
.expect("Post::get_blog: no result error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_likes(&self, conn: &Connection) -> Vec<Like> {
|
pub fn get_likes(&self, conn: &Connection) -> Vec<Like> {
|
||||||
use schema::likes;
|
use schema::likes;
|
||||||
likes::table.filter(likes::post_id.eq(self.id))
|
likes::table
|
||||||
|
.filter(likes::post_id.eq(self.id))
|
||||||
.load::<Like>(conn)
|
.load::<Like>(conn)
|
||||||
.expect("Post::get_likes: loading error")
|
.expect("Post::get_likes: loading error")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_reshares(&self, conn: &Connection) -> Vec<Reshare> {
|
pub fn get_reshares(&self, conn: &Connection) -> Vec<Reshare> {
|
||||||
use schema::reshares;
|
use schema::reshares;
|
||||||
reshares::table.filter(reshares::post_id.eq(self.id))
|
reshares::table
|
||||||
|
.filter(reshares::post_id.eq(self.id))
|
||||||
.load::<Reshare>(conn)
|
.load::<Reshare>(conn)
|
||||||
.expect("Post::get_reshares: loading error")
|
.expect("Post::get_reshares: loading error")
|
||||||
}
|
}
|
||||||
|
@ -318,7 +375,8 @@ impl Post {
|
||||||
if self.ap_url.len() == 0 {
|
if self.ap_url.len() == 0 {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(posts::ap_url.eq(self.compute_id(conn)))
|
.set(posts::ap_url.eq(self.compute_id(conn)))
|
||||||
.execute(conn).expect("Post::update_ap_url: update error");
|
.execute(conn)
|
||||||
|
.expect("Post::update_ap_url: update error");
|
||||||
Post::get(conn, self.id).expect("Post::update_ap_url: get error")
|
Post::get(conn, self.id).expect("Post::update_ap_url: get error")
|
||||||
} else {
|
} else {
|
||||||
self.clone()
|
self.clone()
|
||||||
|
@ -326,7 +384,11 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_receivers_urls(&self, conn: &Connection) -> Vec<String> {
|
pub fn get_receivers_urls(&self, conn: &Connection) -> Vec<String> {
|
||||||
let followers = self.get_authors(conn).into_iter().map(|a| a.get_followers(conn)).collect::<Vec<Vec<User>>>();
|
let followers = self
|
||||||
|
.get_authors(conn)
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.get_followers(conn))
|
||||||
|
.collect::<Vec<Vec<User>>>();
|
||||||
let to = followers.into_iter().fold(vec![], |mut acc, f| {
|
let to = followers.into_iter().fold(vec![], |mut acc, f| {
|
||||||
for x in f {
|
for x in f {
|
||||||
acc.push(x.ap_url);
|
acc.push(x.ap_url);
|
||||||
|
@ -340,74 +402,170 @@ impl Post {
|
||||||
let mut to = self.get_receivers_urls(conn);
|
let mut to = self.get_receivers_urls(conn);
|
||||||
to.push(PUBLIC_VISIBILTY.to_string());
|
to.push(PUBLIC_VISIBILTY.to_string());
|
||||||
|
|
||||||
let mut mentions_json = Mention::list_for_post(conn, self.id).into_iter().map(|m| json!(m.to_activity(conn))).collect::<Vec<serde_json::Value>>();
|
let mut mentions_json = Mention::list_for_post(conn, self.id)
|
||||||
let mut tags_json = Tag::for_post(conn, self.id).into_iter().map(|t| json!(t.into_activity(conn))).collect::<Vec<serde_json::Value>>();
|
.into_iter()
|
||||||
|
.map(|m| json!(m.to_activity(conn)))
|
||||||
|
.collect::<Vec<serde_json::Value>>();
|
||||||
|
let mut tags_json = Tag::for_post(conn, self.id)
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| json!(t.into_activity(conn)))
|
||||||
|
.collect::<Vec<serde_json::Value>>();
|
||||||
mentions_json.append(&mut tags_json);
|
mentions_json.append(&mut tags_json);
|
||||||
|
|
||||||
let mut article = Article::default();
|
let mut article = Article::default();
|
||||||
article.object_props.set_name_string(self.title.clone()).expect("Post::into_activity: name error");
|
article
|
||||||
article.object_props.set_id_string(self.ap_url.clone()).expect("Post::into_activity: id error");
|
.object_props
|
||||||
|
.set_name_string(self.title.clone())
|
||||||
|
.expect("Post::into_activity: name error");
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.set_id_string(self.ap_url.clone())
|
||||||
|
.expect("Post::into_activity: id error");
|
||||||
|
|
||||||
let mut authors = self.get_authors(conn).into_iter().map(|x| Id::new(x.ap_url)).collect::<Vec<Id>>();
|
let mut authors = self
|
||||||
|
.get_authors(conn)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| Id::new(x.ap_url))
|
||||||
|
.collect::<Vec<Id>>();
|
||||||
authors.push(self.get_blog(conn).into_id()); // add the blog URL here too
|
authors.push(self.get_blog(conn).into_id()); // add the blog URL here too
|
||||||
article.object_props.set_attributed_to_link_vec::<Id>(authors).expect("Post::into_activity: attributedTo error");
|
article
|
||||||
article.object_props.set_content_string(self.content.get().clone()).expect("Post::into_activity: content error");
|
.object_props
|
||||||
article.ap_object_props.set_source_object(Source {
|
.set_attributed_to_link_vec::<Id>(authors)
|
||||||
|
.expect("Post::into_activity: attributedTo error");
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.set_content_string(self.content.get().clone())
|
||||||
|
.expect("Post::into_activity: content error");
|
||||||
|
article
|
||||||
|
.ap_object_props
|
||||||
|
.set_source_object(Source {
|
||||||
content: self.source.clone(),
|
content: self.source.clone(),
|
||||||
media_type: String::from("text/markdown"),
|
media_type: String::from("text/markdown"),
|
||||||
}).expect("Post::into_activity: source error");
|
})
|
||||||
article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Post::into_activity: published error");
|
.expect("Post::into_activity: source error");
|
||||||
article.object_props.set_summary_string(self.subtitle.clone()).expect("Post::into_activity: summary error");
|
article
|
||||||
|
.object_props
|
||||||
|
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))
|
||||||
|
.expect("Post::into_activity: published error");
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.set_summary_string(self.subtitle.clone())
|
||||||
|
.expect("Post::into_activity: summary error");
|
||||||
article.object_props.tag = Some(json!(mentions_json));
|
article.object_props.tag = Some(json!(mentions_json));
|
||||||
|
|
||||||
if let Some(media_id) = self.cover_id {
|
if let Some(media_id) = self.cover_id {
|
||||||
let media = Media::get(conn, media_id).expect("Post::into_activity: get cover error");
|
let media = Media::get(conn, media_id).expect("Post::into_activity: get cover error");
|
||||||
let mut cover = Image::default();
|
let mut cover = Image::default();
|
||||||
cover.object_props.set_url_string(media.url(conn)).expect("Post::into_activity: icon.url error");
|
cover
|
||||||
|
.object_props
|
||||||
|
.set_url_string(media.url(conn))
|
||||||
|
.expect("Post::into_activity: icon.url error");
|
||||||
if media.sensitive {
|
if media.sensitive {
|
||||||
cover.object_props.set_summary_string(media.content_warning.unwrap_or(String::new())).expect("Post::into_activity: icon.summary error");
|
cover
|
||||||
|
.object_props
|
||||||
|
.set_summary_string(media.content_warning.unwrap_or(String::new()))
|
||||||
|
.expect("Post::into_activity: icon.summary error");
|
||||||
}
|
}
|
||||||
cover.object_props.set_content_string(media.alt_text).expect("Post::into_activity: icon.content error");
|
cover
|
||||||
cover.object_props.set_attributed_to_link_vec(vec![
|
.object_props
|
||||||
User::get(conn, media.owner_id).expect("Post::into_activity: media owner not found").into_id()
|
.set_content_string(media.alt_text)
|
||||||
]).expect("Post::into_activity: icon.attributedTo error");
|
.expect("Post::into_activity: icon.content error");
|
||||||
article.object_props.set_icon_object(cover).expect("Post::into_activity: icon error");
|
cover
|
||||||
|
.object_props
|
||||||
|
.set_attributed_to_link_vec(vec![
|
||||||
|
User::get(conn, media.owner_id)
|
||||||
|
.expect("Post::into_activity: media owner not found")
|
||||||
|
.into_id(),
|
||||||
|
])
|
||||||
|
.expect("Post::into_activity: icon.attributedTo error");
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.set_icon_object(cover)
|
||||||
|
.expect("Post::into_activity: icon error");
|
||||||
}
|
}
|
||||||
|
|
||||||
article.object_props.set_url_string(self.ap_url.clone()).expect("Post::into_activity: url error");
|
article
|
||||||
article.object_props.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect()).expect("Post::into_activity: to error");
|
.object_props
|
||||||
article.object_props.set_cc_link_vec::<Id>(vec![]).expect("Post::into_activity: cc error");
|
.set_url_string(self.ap_url.clone())
|
||||||
|
.expect("Post::into_activity: url error");
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())
|
||||||
|
.expect("Post::into_activity: to error");
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Post::into_activity: cc error");
|
||||||
article
|
article
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_activity(&self, conn: &Connection) -> Create {
|
pub fn create_activity(&self, conn: &Connection) -> Create {
|
||||||
let article = self.into_activity(conn);
|
let article = self.into_activity(conn);
|
||||||
let mut act = Create::default();
|
let mut act = Create::default();
|
||||||
act.object_props.set_id_string(format!("{}activity", self.ap_url)).expect("Post::create_activity: id error");
|
act.object_props
|
||||||
act.object_props.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::create_activity: Couldn't copy 'to'"))
|
.set_id_string(format!("{}activity", self.ap_url))
|
||||||
|
.expect("Post::create_activity: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link_vec::<Id>(
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.to_link_vec()
|
||||||
|
.expect("Post::create_activity: Couldn't copy 'to'"),
|
||||||
|
)
|
||||||
.expect("Post::create_activity: to error");
|
.expect("Post::create_activity: to error");
|
||||||
act.object_props.set_cc_link_vec::<Id>(article.object_props.cc_link_vec().expect("Post::create_activity: Couldn't copy 'cc'"))
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.cc_link_vec()
|
||||||
|
.expect("Post::create_activity: Couldn't copy 'cc'"),
|
||||||
|
)
|
||||||
.expect("Post::create_activity: cc error");
|
.expect("Post::create_activity: cc error");
|
||||||
act.create_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::create_activity: actor error");
|
act.create_props
|
||||||
act.create_props.set_object_object(article).expect("Post::create_activity: object error");
|
.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url))
|
||||||
|
.expect("Post::create_activity: actor error");
|
||||||
|
act.create_props
|
||||||
|
.set_object_object(article)
|
||||||
|
.expect("Post::create_activity: object error");
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_activity(&self, conn: &Connection) -> Update {
|
pub fn update_activity(&self, conn: &Connection) -> Update {
|
||||||
let article = self.into_activity(conn);
|
let article = self.into_activity(conn);
|
||||||
let mut act = Update::default();
|
let mut act = Update::default();
|
||||||
act.object_props.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp())).expect("Post::update_activity: id error");
|
act.object_props
|
||||||
act.object_props.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::update_activity: Couldn't copy 'to'"))
|
.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp()))
|
||||||
|
.expect("Post::update_activity: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link_vec::<Id>(
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.to_link_vec()
|
||||||
|
.expect("Post::update_activity: Couldn't copy 'to'"),
|
||||||
|
)
|
||||||
.expect("Post::update_activity: to error");
|
.expect("Post::update_activity: to error");
|
||||||
act.object_props.set_cc_link_vec::<Id>(article.object_props.cc_link_vec().expect("Post::update_activity: Couldn't copy 'cc'"))
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.cc_link_vec()
|
||||||
|
.expect("Post::update_activity: Couldn't copy 'cc'"),
|
||||||
|
)
|
||||||
.expect("Post::update_activity: cc error");
|
.expect("Post::update_activity: cc error");
|
||||||
act.update_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::update_activity: actor error");
|
act.update_props
|
||||||
act.update_props.set_object_object(article).expect("Article::update_activity: object error");
|
.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url))
|
||||||
|
.expect("Post::update_activity: actor error");
|
||||||
|
act.update_props
|
||||||
|
.set_object_object(article)
|
||||||
|
.expect("Article::update_activity: object error");
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_update(conn: &Connection, updated: Article) {
|
pub fn handle_update(conn: &Connection, updated: Article) {
|
||||||
let id = updated.object_props.id_string().expect("Post::handle_update: id error");
|
let id = updated
|
||||||
|
.object_props
|
||||||
|
.id_string()
|
||||||
|
.expect("Post::handle_update: id error");
|
||||||
let mut post = Post::find_by_ap_url(conn, id).expect("Post::handle_update: finding error");
|
let mut post = Post::find_by_ap_url(conn, id).expect("Post::handle_update: finding error");
|
||||||
|
|
||||||
if let Ok(title) = updated.object_props.name_string() {
|
if let Ok(title) = updated.object_props.name_string() {
|
||||||
|
@ -431,7 +589,11 @@ impl Post {
|
||||||
post.source = source.content;
|
post.source = source.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut txt_hashtags = md_to_html(&post.source).2.into_iter().map(|s| s.to_camel_case()).collect::<HashSet<_>>();
|
let mut txt_hashtags = md_to_html(&post.source)
|
||||||
|
.2
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_camel_case())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
if let Some(serde_json::Value::Array(mention_tags)) = updated.object_props.tag.clone() {
|
if let Some(serde_json::Value::Array(mention_tags)) = updated.object_props.tag.clone() {
|
||||||
let mut mentions = vec![];
|
let mut mentions = vec![];
|
||||||
let mut tags = vec![];
|
let mut tags = vec![];
|
||||||
|
@ -443,13 +605,16 @@ impl Post {
|
||||||
|
|
||||||
serde_json::from_value::<Hashtag>(tag.clone())
|
serde_json::from_value::<Hashtag>(tag.clone())
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let tag_name = t.name_string().expect("Post::from_activity: tag name error");
|
let tag_name = t
|
||||||
|
.name_string()
|
||||||
|
.expect("Post::from_activity: tag name error");
|
||||||
if txt_hashtags.remove(&tag_name) {
|
if txt_hashtags.remove(&tag_name) {
|
||||||
hashtags.push(t);
|
hashtags.push(t);
|
||||||
} else {
|
} else {
|
||||||
tags.push(t);
|
tags.push(t);
|
||||||
}
|
}
|
||||||
}).ok();
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
post.update_mentions(conn, mentions);
|
post.update_mentions(conn, mentions);
|
||||||
post.update_tags(conn, tags);
|
post.update_tags(conn, tags);
|
||||||
|
@ -460,34 +625,76 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_mentions(&self, conn: &Connection, mentions: Vec<link::Mention>) {
|
pub fn update_mentions(&self, conn: &Connection, mentions: Vec<link::Mention>) {
|
||||||
let mentions = mentions.into_iter().map(|m| (m.link_props.href_string().ok()
|
let mentions = mentions
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| {
|
||||||
|
(
|
||||||
|
m.link_props
|
||||||
|
.href_string()
|
||||||
|
.ok()
|
||||||
.and_then(|ap_url| User::find_by_ap_url(conn, ap_url))
|
.and_then(|ap_url| User::find_by_ap_url(conn, ap_url))
|
||||||
.map(|u| u.id),m))
|
.map(|u| u.id),
|
||||||
.filter_map(|(id, m)| if let Some(id)=id {Some((m,id))} else {None}).collect::<Vec<_>>();
|
m,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter_map(|(id, m)| {
|
||||||
|
if let Some(id) = id {
|
||||||
|
Some((m, id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let old_mentions = Mention::list_for_post(&conn, self.id);
|
let old_mentions = Mention::list_for_post(&conn, self.id);
|
||||||
let old_user_mentioned = old_mentions.iter()
|
let old_user_mentioned = old_mentions
|
||||||
.map(|m| m.mentioned_id).collect::<HashSet<_>>();
|
.iter()
|
||||||
for (m,id) in mentions.iter() {
|
.map(|m| m.mentioned_id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
for (m, id) in mentions.iter() {
|
||||||
if !old_user_mentioned.contains(&id) {
|
if !old_user_mentioned.contains(&id) {
|
||||||
Mention::from_activity(&*conn, m.clone(), self.id, true, true);
|
Mention::from_activity(&*conn, m.clone(), self.id, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_mentions = mentions.into_iter().map(|(_m,id)| id).collect::<HashSet<_>>();
|
let new_mentions = mentions
|
||||||
for m in old_mentions.iter().filter(|m| !new_mentions.contains(&m.mentioned_id)) {
|
.into_iter()
|
||||||
|
.map(|(_m, id)| id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
for m in old_mentions
|
||||||
|
.iter()
|
||||||
|
.filter(|m| !new_mentions.contains(&m.mentioned_id))
|
||||||
|
{
|
||||||
m.delete(&conn);
|
m.delete(&conn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) {
|
pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) {
|
||||||
let tags_name = tags.iter().filter_map(|t| t.name_string().ok()).collect::<HashSet<_>>();
|
let tags_name = tags
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| t.name_string().ok())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let old_tags = Tag::for_post(&*conn, self.id).into_iter().collect::<Vec<_>>();
|
let old_tags = Tag::for_post(&*conn, self.id)
|
||||||
let old_tags_name = old_tags.iter().filter_map(|tag| if !tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>();
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let old_tags_name = old_tags
|
||||||
|
.iter()
|
||||||
|
.filter_map(|tag| {
|
||||||
|
if !tag.is_hashtag {
|
||||||
|
Some(tag.tag.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
for t in tags.into_iter() {
|
for t in tags.into_iter() {
|
||||||
if !t.name_string().map(|n| old_tags_name.contains(&n)).unwrap_or(true) {
|
if !t
|
||||||
|
.name_string()
|
||||||
|
.map(|n| old_tags_name.contains(&n))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
Tag::from_activity(conn, t, self.id, false);
|
Tag::from_activity(conn, t, self.id, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -500,13 +707,31 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) {
|
pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) {
|
||||||
let tags_name = tags.iter().filter_map(|t| t.name_string().ok()).collect::<HashSet<_>>();
|
let tags_name = tags
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| t.name_string().ok())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let old_tags = Tag::for_post(&*conn, self.id).into_iter().collect::<Vec<_>>();
|
let old_tags = Tag::for_post(&*conn, self.id)
|
||||||
let old_tags_name = old_tags.iter().filter_map(|tag| if tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>();
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let old_tags_name = old_tags
|
||||||
|
.iter()
|
||||||
|
.filter_map(|tag| {
|
||||||
|
if tag.is_hashtag {
|
||||||
|
Some(tag.tag.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
for t in tags.into_iter() {
|
for t in tags.into_iter() {
|
||||||
if !t.name_string().map(|n| old_tags_name.contains(&n)).unwrap_or(true) {
|
if !t
|
||||||
|
.name_string()
|
||||||
|
.map(|n| old_tags_name.contains(&n))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
Tag::from_activity(conn, t, self.id, true);
|
Tag::from_activity(conn, t, self.id, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -532,16 +757,26 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute_id(&self, conn: &Connection) -> String {
|
pub fn compute_id(&self, conn: &Connection) -> String {
|
||||||
ap_url(format!("{}/~/{}/{}/", BASE_URL.as_str(), self.get_blog(conn).get_fqn(conn), self.slug))
|
ap_url(format!(
|
||||||
|
"{}/~/{}/{}/",
|
||||||
|
BASE_URL.as_str(),
|
||||||
|
self.get_blog(conn).get_fqn(conn),
|
||||||
|
self.slug
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromActivity<Article, Connection> for Post {
|
impl FromActivity<Article, Connection> for Post {
|
||||||
fn from_activity(conn: &Connection, article: Article, _actor: Id) -> Post {
|
fn from_activity(conn: &Connection, article: Article, _actor: Id) -> Post {
|
||||||
if let Some(post) = Post::find_by_ap_url(conn, article.object_props.id_string().unwrap_or(String::new())) {
|
if let Some(post) = Post::find_by_ap_url(
|
||||||
|
conn,
|
||||||
|
article.object_props.id_string().unwrap_or(String::new()),
|
||||||
|
) {
|
||||||
post
|
post
|
||||||
} else {
|
} else {
|
||||||
let (blog, authors) = article.object_props.attributed_to_link_vec::<Id>()
|
let (blog, authors) = article
|
||||||
|
.object_props
|
||||||
|
.attributed_to_link_vec::<Id>()
|
||||||
.expect("Post::from_activity: attributedTo error")
|
.expect("Post::from_activity: attributedTo error")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||||
|
@ -550,39 +785,78 @@ impl FromActivity<Article, Connection> for Post {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
authors.push(user);
|
authors.push(user);
|
||||||
(blog, authors)
|
(blog, authors)
|
||||||
},
|
}
|
||||||
None => (blog.or_else(|| Blog::from_url(conn, url)), authors)
|
None => (blog.or_else(|| Blog::from_url(conn, url)), authors),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let cover = article.object_props.icon_object::<Image>().ok()
|
let cover = article
|
||||||
|
.object_props
|
||||||
|
.icon_object::<Image>()
|
||||||
|
.ok()
|
||||||
.and_then(|img| Media::from_activity(conn, img).map(|m| m.id));
|
.and_then(|img| Media::from_activity(conn, img).map(|m| m.id));
|
||||||
|
|
||||||
let title = article.object_props.name_string().expect("Post::from_activity: title error");
|
let title = article
|
||||||
let post = Post::insert(conn, NewPost {
|
.object_props
|
||||||
|
.name_string()
|
||||||
|
.expect("Post::from_activity: title error");
|
||||||
|
let post = Post::insert(
|
||||||
|
conn,
|
||||||
|
NewPost {
|
||||||
blog_id: blog.expect("Post::from_activity: blog not found error").id,
|
blog_id: blog.expect("Post::from_activity: blog not found error").id,
|
||||||
slug: title.to_kebab_case(),
|
slug: title.to_kebab_case(),
|
||||||
title: title,
|
title: title,
|
||||||
content: SafeString::new(&article.object_props.content_string().expect("Post::from_activity: content error")),
|
content: SafeString::new(
|
||||||
|
&article
|
||||||
|
.object_props
|
||||||
|
.content_string()
|
||||||
|
.expect("Post::from_activity: content error"),
|
||||||
|
),
|
||||||
published: true,
|
published: true,
|
||||||
license: String::from("CC-BY-SA"), // TODO
|
license: String::from("CC-BY-SA"), // TODO
|
||||||
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
|
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
|
||||||
ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error")),
|
ap_url: article.object_props.url_string().unwrap_or(
|
||||||
creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc()),
|
article
|
||||||
subtitle: article.object_props.summary_string().expect("Post::from_activity: summary error"),
|
.object_props
|
||||||
source: article.ap_object_props.source_object::<Source>().expect("Post::from_activity: source error").content,
|
.id_string()
|
||||||
|
.expect("Post::from_activity: url + id error"),
|
||||||
|
),
|
||||||
|
creation_date: Some(
|
||||||
|
article
|
||||||
|
.object_props
|
||||||
|
.published_utctime()
|
||||||
|
.expect("Post::from_activity: published error")
|
||||||
|
.naive_utc(),
|
||||||
|
),
|
||||||
|
subtitle: article
|
||||||
|
.object_props
|
||||||
|
.summary_string()
|
||||||
|
.expect("Post::from_activity: summary error"),
|
||||||
|
source: article
|
||||||
|
.ap_object_props
|
||||||
|
.source_object::<Source>()
|
||||||
|
.expect("Post::from_activity: source error")
|
||||||
|
.content,
|
||||||
cover_id: cover,
|
cover_id: cover,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
for author in authors.into_iter() {
|
for author in authors.into_iter() {
|
||||||
PostAuthor::insert(conn, NewPostAuthor {
|
PostAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewPostAuthor {
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
author_id: author.id
|
author_id: author.id,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// save mentions and tags
|
// save mentions and tags
|
||||||
let mut hashtags = md_to_html(&post.source).2.into_iter().map(|s| s.to_camel_case()).collect::<HashSet<_>>();
|
let mut hashtags = md_to_html(&post.source)
|
||||||
|
.2
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_camel_case())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
|
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
|
||||||
for tag in tags.into_iter() {
|
for tag in tags.into_iter() {
|
||||||
serde_json::from_value::<link::Mention>(tag.clone())
|
serde_json::from_value::<link::Mention>(tag.clone())
|
||||||
|
@ -591,7 +865,9 @@ impl FromActivity<Article, Connection> for Post {
|
||||||
|
|
||||||
serde_json::from_value::<Hashtag>(tag.clone())
|
serde_json::from_value::<Hashtag>(tag.clone())
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let tag_name = t.name_string().expect("Post::from_activity: tag name error");
|
let tag_name = t
|
||||||
|
.name_string()
|
||||||
|
.expect("Post::from_activity: tag name error");
|
||||||
Tag::from_activity(conn, t, post.id, hashtags.remove(&tag_name));
|
Tag::from_activity(conn, t, post.id, hashtags.remove(&tag_name));
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -605,28 +881,44 @@ impl FromActivity<Article, Connection> for Post {
|
||||||
impl Deletable<Connection, Delete> for Post {
|
impl Deletable<Connection, Delete> for Post {
|
||||||
fn delete(&self, conn: &Connection) -> Delete {
|
fn delete(&self, conn: &Connection) -> Delete {
|
||||||
let mut act = Delete::default();
|
let mut act = Delete::default();
|
||||||
act.delete_props.set_actor_link(self.get_authors(conn)[0].clone().into_id()).expect("Post::delete: actor error");
|
act.delete_props
|
||||||
|
.set_actor_link(self.get_authors(conn)[0].clone().into_id())
|
||||||
|
.expect("Post::delete: actor error");
|
||||||
|
|
||||||
let mut tombstone = Tombstone::default();
|
let mut tombstone = Tombstone::default();
|
||||||
tombstone.object_props.set_id_string(self.ap_url.clone()).expect("Post::delete: object.id error");
|
tombstone
|
||||||
act.delete_props.set_object_object(tombstone).expect("Post::delete: object error");
|
.object_props
|
||||||
|
.set_id_string(self.ap_url.clone())
|
||||||
|
.expect("Post::delete: object.id error");
|
||||||
|
act.delete_props
|
||||||
|
.set_object_object(tombstone)
|
||||||
|
.expect("Post::delete: object error");
|
||||||
|
|
||||||
act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Post::delete: id error");
|
act.object_props
|
||||||
act.object_props.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]).expect("Post::delete: to error");
|
.set_id_string(format!("{}#delete", self.ap_url))
|
||||||
|
.expect("Post::delete: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)])
|
||||||
|
.expect("Post::delete: to error");
|
||||||
|
|
||||||
for m in Mention::list_for_post(&conn, self.id) {
|
for m in Mention::list_for_post(&conn, self.id) {
|
||||||
m.delete(conn);
|
m.delete(conn);
|
||||||
}
|
}
|
||||||
diesel::delete(self).execute(conn).expect("Post::delete: DB error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Post::delete: DB error");
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_id(id: String, actor_id: String, conn: &Connection) {
|
fn delete_id(id: String, actor_id: String, conn: &Connection) {
|
||||||
let actor = User::find_by_ap_url(conn, actor_id);
|
let actor = User::find_by_ap_url(conn, actor_id);
|
||||||
let post = Post::find_by_ap_url(conn, id);
|
let post = Post::find_by_ap_url(conn, id);
|
||||||
let can_delete = actor.and_then(|act|
|
let can_delete = actor
|
||||||
post.clone().map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id))
|
.and_then(|act| {
|
||||||
).unwrap_or(false);
|
post.clone()
|
||||||
|
.map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id))
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
if can_delete {
|
if can_delete {
|
||||||
post.map(|p| p.delete(conn));
|
post.map(|p| p.delete(conn));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
use activitypub::activity::{Announce, Undo};
|
use activitypub::activity::{Announce, Undo};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use plume_common::activity_pub::{Id, IntoId, inbox::{FromActivity, Notify, Deletable}, PUBLIC_VISIBILTY};
|
|
||||||
use Connection;
|
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
|
use plume_common::activity_pub::{
|
||||||
|
inbox::{Deletable, FromActivity, Notify},
|
||||||
|
Id, IntoId, PUBLIC_VISIBILTY,
|
||||||
|
};
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use users::User;
|
|
||||||
use schema::reshares;
|
use schema::reshares;
|
||||||
|
use users::User;
|
||||||
|
use Connection;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)]
|
#[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)]
|
||||||
pub struct Reshare {
|
pub struct Reshare {
|
||||||
|
@ -15,7 +18,7 @@ pub struct Reshare {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub ap_url: String,
|
pub ap_url: String,
|
||||||
pub creation_date: NaiveDateTime
|
pub creation_date: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
|
@ -23,29 +26,40 @@ pub struct Reshare {
|
||||||
pub struct NewReshare {
|
pub struct NewReshare {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub ap_url: String
|
pub ap_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reshare {
|
impl Reshare {
|
||||||
insert!(reshares, NewReshare);
|
insert!(reshares, NewReshare);
|
||||||
get!(reshares);
|
get!(reshares);
|
||||||
find_by!(reshares, find_by_ap_url, ap_url as String);
|
find_by!(reshares, find_by_ap_url, ap_url as String);
|
||||||
find_by!(reshares, find_by_user_on_post, user_id as i32, post_id as i32);
|
find_by!(
|
||||||
|
reshares,
|
||||||
|
find_by_user_on_post,
|
||||||
|
user_id as i32,
|
||||||
|
post_id as i32
|
||||||
|
);
|
||||||
|
|
||||||
pub fn update_ap_url(&self, conn: &Connection) {
|
pub fn update_ap_url(&self, conn: &Connection) {
|
||||||
if self.ap_url.len() == 0 {
|
if self.ap_url.len() == 0 {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(reshares::ap_url.eq(format!(
|
.set(reshares::ap_url.eq(format!(
|
||||||
"{}/reshare/{}",
|
"{}/reshare/{}",
|
||||||
User::get(conn, self.user_id).expect("Reshare::update_ap_url: user error").ap_url,
|
User::get(conn, self.user_id)
|
||||||
Post::get(conn, self.post_id).expect("Reshare::update_ap_url: post error").ap_url
|
.expect("Reshare::update_ap_url: user error")
|
||||||
|
.ap_url,
|
||||||
|
Post::get(conn, self.post_id)
|
||||||
|
.expect("Reshare::update_ap_url: post error")
|
||||||
|
.ap_url
|
||||||
)))
|
)))
|
||||||
.execute(conn).expect("Reshare::update_ap_url: update error");
|
.execute(conn)
|
||||||
|
.expect("Reshare::update_ap_url: update error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Vec<Reshare> {
|
pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Vec<Reshare> {
|
||||||
reshares::table.filter(reshares::user_id.eq(user.id))
|
reshares::table
|
||||||
|
.filter(reshares::user_id.eq(user.id))
|
||||||
.order(reshares::creation_date.desc())
|
.order(reshares::creation_date.desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.load::<Reshare>(conn)
|
.load::<Reshare>(conn)
|
||||||
|
@ -62,13 +76,29 @@ impl Reshare {
|
||||||
|
|
||||||
pub fn into_activity(&self, conn: &Connection) -> Announce {
|
pub fn into_activity(&self, conn: &Connection) -> Announce {
|
||||||
let mut act = Announce::default();
|
let mut act = Announce::default();
|
||||||
act.announce_props.set_actor_link(User::get(conn, self.user_id).expect("Reshare::into_activity: user error").into_id())
|
act.announce_props
|
||||||
|
.set_actor_link(
|
||||||
|
User::get(conn, self.user_id)
|
||||||
|
.expect("Reshare::into_activity: user error")
|
||||||
|
.into_id(),
|
||||||
|
)
|
||||||
.expect("Reshare::into_activity: actor error");
|
.expect("Reshare::into_activity: actor error");
|
||||||
act.announce_props.set_object_link(Post::get(conn, self.post_id).expect("Reshare::into_activity: post error").into_id())
|
act.announce_props
|
||||||
|
.set_object_link(
|
||||||
|
Post::get(conn, self.post_id)
|
||||||
|
.expect("Reshare::into_activity: post error")
|
||||||
|
.into_id(),
|
||||||
|
)
|
||||||
.expect("Reshare::into_activity: object error");
|
.expect("Reshare::into_activity: object error");
|
||||||
act.object_props.set_id_string(self.ap_url.clone()).expect("Reshare::into_activity: id error");
|
act.object_props
|
||||||
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::into_activity: to error");
|
.set_id_string(self.ap_url.clone())
|
||||||
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Reshare::into_activity: cc error");
|
.expect("Reshare::into_activity: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
|
||||||
|
.expect("Reshare::into_activity: to error");
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Reshare::into_activity: cc error");
|
||||||
|
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
@ -76,13 +106,33 @@ impl Reshare {
|
||||||
|
|
||||||
impl FromActivity<Announce, Connection> for Reshare {
|
impl FromActivity<Announce, Connection> for Reshare {
|
||||||
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Reshare {
|
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Reshare {
|
||||||
let user = User::from_url(conn, announce.announce_props.actor_link::<Id>().expect("Reshare::from_activity: actor error").into());
|
let user = User::from_url(
|
||||||
let post = Post::find_by_ap_url(conn, announce.announce_props.object_link::<Id>().expect("Reshare::from_activity: object error").into());
|
conn,
|
||||||
let reshare = Reshare::insert(conn, NewReshare {
|
announce
|
||||||
|
.announce_props
|
||||||
|
.actor_link::<Id>()
|
||||||
|
.expect("Reshare::from_activity: actor error")
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let post = Post::find_by_ap_url(
|
||||||
|
conn,
|
||||||
|
announce
|
||||||
|
.announce_props
|
||||||
|
.object_link::<Id>()
|
||||||
|
.expect("Reshare::from_activity: object error")
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let reshare = Reshare::insert(
|
||||||
|
conn,
|
||||||
|
NewReshare {
|
||||||
post_id: post.expect("Reshare::from_activity: post error").id,
|
post_id: post.expect("Reshare::from_activity: post error").id,
|
||||||
user_id: user.expect("Reshare::from_activity: user error").id,
|
user_id: user.expect("Reshare::from_activity: user error").id,
|
||||||
ap_url: announce.object_props.id_string().unwrap_or(String::from(""))
|
ap_url: announce
|
||||||
});
|
.object_props
|
||||||
|
.id_string()
|
||||||
|
.unwrap_or(String::from("")),
|
||||||
|
},
|
||||||
|
);
|
||||||
reshare.notify(conn);
|
reshare.notify(conn);
|
||||||
reshare
|
reshare
|
||||||
}
|
}
|
||||||
|
@ -92,30 +142,51 @@ impl Notify<Connection> for Reshare {
|
||||||
fn notify(&self, conn: &Connection) {
|
fn notify(&self, conn: &Connection) {
|
||||||
let post = self.get_post(conn).expect("Reshare::notify: post error");
|
let post = self.get_post(conn).expect("Reshare::notify: post error");
|
||||||
for author in post.get_authors(conn) {
|
for author in post.get_authors(conn) {
|
||||||
Notification::insert(conn, NewNotification {
|
Notification::insert(
|
||||||
|
conn,
|
||||||
|
NewNotification {
|
||||||
kind: notification_kind::RESHARE.to_string(),
|
kind: notification_kind::RESHARE.to_string(),
|
||||||
object_id: self.id,
|
object_id: self.id,
|
||||||
user_id: author.id
|
user_id: author.id,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deletable<Connection, Undo> for Reshare {
|
impl Deletable<Connection, Undo> for Reshare {
|
||||||
fn delete(&self, conn: &Connection) -> Undo {
|
fn delete(&self, conn: &Connection) -> Undo {
|
||||||
diesel::delete(self).execute(conn).expect("Reshare::delete: delete error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Reshare::delete: delete error");
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Some(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
|
if let Some(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
|
||||||
diesel::delete(¬if).execute(conn).expect("Reshare::delete: notification error");
|
diesel::delete(¬if)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Reshare::delete: notification error");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut act = Undo::default();
|
let mut act = Undo::default();
|
||||||
act.undo_props.set_actor_link(User::get(conn, self.user_id).expect("Reshare::delete: user error").into_id()).expect("Reshare::delete: actor error");
|
act.undo_props
|
||||||
act.undo_props.set_object_object(self.into_activity(conn)).expect("Reshare::delete: object error");
|
.set_actor_link(
|
||||||
act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Reshare::delete: id error");
|
User::get(conn, self.user_id)
|
||||||
act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::delete: to error");
|
.expect("Reshare::delete: user error")
|
||||||
act.object_props.set_cc_link_vec::<Id>(vec![]).expect("Reshare::delete: cc error");
|
.into_id(),
|
||||||
|
)
|
||||||
|
.expect("Reshare::delete: actor error");
|
||||||
|
act.undo_props
|
||||||
|
.set_object_object(self.into_activity(conn))
|
||||||
|
.expect("Reshare::delete: object error");
|
||||||
|
act.object_props
|
||||||
|
.set_id_string(format!("{}#delete", self.ap_url))
|
||||||
|
.expect("Reshare::delete: id error");
|
||||||
|
act.object_props
|
||||||
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))
|
||||||
|
.expect("Reshare::delete: to error");
|
||||||
|
act.object_props
|
||||||
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
.expect("Reshare::delete: cc error");
|
||||||
|
|
||||||
act
|
act
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
use ammonia::{Builder, UrlRelative};
|
use ammonia::{Builder, UrlRelative};
|
||||||
use serde::{self, Serialize, Deserialize,
|
use diesel::{
|
||||||
Serializer, Deserializer, de::Visitor};
|
self,
|
||||||
use std::{fmt::{self, Display},
|
deserialize::Queryable,
|
||||||
borrow::{Borrow, Cow}, io::Write,
|
serialize::{self, Output},
|
||||||
iter, ops::Deref};
|
|
||||||
use diesel::{self, deserialize::Queryable,
|
|
||||||
types::ToSql,
|
|
||||||
sql_types::Text,
|
sql_types::Text,
|
||||||
serialize::{self, Output}};
|
types::ToSql,
|
||||||
|
};
|
||||||
|
use serde::{self, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use std::{
|
||||||
|
borrow::{Borrow, Cow},
|
||||||
|
fmt::{self, Display},
|
||||||
|
io::Write,
|
||||||
|
iter,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CLEAN: Builder<'static> = {
|
static ref CLEAN: Builder<'static> = {
|
||||||
|
@ -16,17 +22,18 @@ lazy_static! {
|
||||||
.add_tags(iter::once("iframe"))
|
.add_tags(iter::once("iframe"))
|
||||||
.id_prefix(Some("postcontent-"))
|
.id_prefix(Some("postcontent-"))
|
||||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||||
.add_tag_attributes("iframe",
|
.add_tag_attributes(
|
||||||
["width", "height", "src", "frameborder"]
|
"iframe",
|
||||||
.iter()
|
["width", "height", "src", "frameborder"].iter().map(|&v| v),
|
||||||
.map(|&v| v));
|
);
|
||||||
b
|
b
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url_add_prefix(url: &str) -> Option<Cow<str>> {
|
fn url_add_prefix(url: &str) -> Option<Cow<str>> {
|
||||||
if url.starts_with('#') && ! url.starts_with("#postcontent-") {//if start with an #
|
if url.starts_with('#') && !url.starts_with("#postcontent-") {
|
||||||
let mut new_url = "#postcontent-".to_owned();//change to valid id
|
//if start with an #
|
||||||
|
let mut new_url = "#postcontent-".to_owned(); //change to valid id
|
||||||
new_url.push_str(&url[1..]);
|
new_url.push_str(&url[1..]);
|
||||||
Some(Cow::Owned(new_url))
|
Some(Cow::Owned(new_url))
|
||||||
} else {
|
} else {
|
||||||
|
@ -34,15 +41,15 @@ fn url_add_prefix(url: &str) -> Option<Cow<str>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, AsExpression, FromSqlRow, Default)]
|
#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)]
|
||||||
#[sql_type = "Text"]
|
#[sql_type = "Text"]
|
||||||
pub struct SafeString{
|
pub struct SafeString {
|
||||||
value: String,
|
value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SafeString{
|
impl SafeString {
|
||||||
pub fn new(value: &str) -> Self {
|
pub fn new(value: &str) -> Self {
|
||||||
SafeString{
|
SafeString {
|
||||||
value: CLEAN.clean(&value).to_string(),
|
value: CLEAN.clean(&value).to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +63,9 @@ pub fn new(value: &str) -> Self {
|
||||||
|
|
||||||
impl Serialize for SafeString {
|
impl Serialize for SafeString {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where S: Serializer, {
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
serializer.serialize_str(&self.value)
|
serializer.serialize_str(&self.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,22 +75,24 @@ struct SafeStringVisitor;
|
||||||
impl<'de> Visitor<'de> for SafeStringVisitor {
|
impl<'de> Visitor<'de> for SafeStringVisitor {
|
||||||
type Value = SafeString;
|
type Value = SafeString;
|
||||||
|
|
||||||
fn expecting(&self, formatter:&mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
formatter.write_str("a string")
|
formatter.write_str("a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<SafeString, E>
|
fn visit_str<E>(self, value: &str) -> Result<SafeString, E>
|
||||||
where E: serde::de::Error{
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
Ok(SafeString::new(value))
|
Ok(SafeString::new(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for SafeString {
|
impl<'de> Deserialize<'de> for SafeString {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where D: Deserializer<'de>, {
|
where
|
||||||
Ok(
|
D: Deserializer<'de>,
|
||||||
deserializer.deserialize_string(SafeStringVisitor)?
|
{
|
||||||
)
|
Ok(deserializer.deserialize_string(SafeStringVisitor)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,17 +112,16 @@ impl Queryable<Text, diesel::sqlite::Sqlite> for SafeString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl<DB> ToSql<diesel::sql_types::Text, DB> for SafeString
|
impl<DB> ToSql<diesel::sql_types::Text, DB> for SafeString
|
||||||
where
|
where
|
||||||
DB: diesel::backend::Backend,
|
DB: diesel::backend::Backend,
|
||||||
str: ToSql<diesel::sql_types::Text, DB>, {
|
str: ToSql<diesel::sql_types::Text, DB>,
|
||||||
|
{
|
||||||
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
|
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
|
||||||
str::to_sql(&self.value, out)
|
str::to_sql(&self.value, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Borrow<str> for SafeString {
|
impl Borrow<str> for SafeString {
|
||||||
fn borrow(&self) -> &str {
|
fn borrow(&self) -> &str {
|
||||||
&self.value
|
&self.value
|
||||||
|
@ -137,8 +147,8 @@ impl AsRef<str> for SafeString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use rocket::request::FromFormValue;
|
|
||||||
use rocket::http::RawStr;
|
use rocket::http::RawStr;
|
||||||
|
use rocket::request::FromFormValue;
|
||||||
|
|
||||||
impl<'v> FromFormValue<'v> for SafeString {
|
impl<'v> FromFormValue<'v> for SafeString {
|
||||||
type Error = &'v RawStr;
|
type Error = &'v RawStr;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
use diesel::{self, ExpressionMethods, RunQueryDsl, QueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use plume_common::activity_pub::Hashtag;
|
|
||||||
use {ap_url, Connection};
|
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
|
use plume_common::activity_pub::Hashtag;
|
||||||
use schema::tags;
|
use schema::tags;
|
||||||
|
use {ap_url, Connection};
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Serialize, Queryable)]
|
#[derive(Clone, Identifiable, Serialize, Queryable)]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub tag: String,
|
pub tag: String,
|
||||||
pub is_hashtag: bool,
|
pub is_hashtag: bool,
|
||||||
pub post_id: i32
|
pub post_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
|
@ -18,7 +18,7 @@ pub struct Tag {
|
||||||
pub struct NewTag {
|
pub struct NewTag {
|
||||||
pub tag: String,
|
pub tag: String,
|
||||||
pub is_hashtag: bool,
|
pub is_hashtag: bool,
|
||||||
pub post_id: i32
|
pub post_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tag {
|
impl Tag {
|
||||||
|
@ -29,33 +29,46 @@ impl Tag {
|
||||||
|
|
||||||
pub fn into_activity(&self, conn: &Connection) -> Hashtag {
|
pub fn into_activity(&self, conn: &Connection) -> Hashtag {
|
||||||
let mut ht = Hashtag::default();
|
let mut ht = Hashtag::default();
|
||||||
ht.set_href_string(ap_url(format!("{}/tag/{}",
|
ht.set_href_string(ap_url(format!(
|
||||||
Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain,
|
"{}/tag/{}",
|
||||||
self.tag)
|
Instance::get_local(conn)
|
||||||
)).expect("Tag::into_activity: href error");
|
.expect("Tag::into_activity: local instance not found error")
|
||||||
ht.set_name_string(self.tag.clone()).expect("Tag::into_activity: name error");
|
.public_domain,
|
||||||
|
self.tag
|
||||||
|
))).expect("Tag::into_activity: href error");
|
||||||
|
ht.set_name_string(self.tag.clone())
|
||||||
|
.expect("Tag::into_activity: name error");
|
||||||
ht
|
ht
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32, is_hashtag: bool) -> Tag {
|
pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32, is_hashtag: bool) -> Tag {
|
||||||
Tag::insert(conn, NewTag {
|
Tag::insert(
|
||||||
|
conn,
|
||||||
|
NewTag {
|
||||||
tag: tag.name_string().expect("Tag::from_activity: name error"),
|
tag: tag.name_string().expect("Tag::from_activity: name error"),
|
||||||
is_hashtag,
|
is_hashtag,
|
||||||
post_id: post
|
post_id: post,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_activity(conn: &Connection, tag: String) -> Hashtag {
|
pub fn build_activity(conn: &Connection, tag: String) -> Hashtag {
|
||||||
let mut ht = Hashtag::default();
|
let mut ht = Hashtag::default();
|
||||||
ht.set_href_string(ap_url(format!("{}/tag/{}",
|
ht.set_href_string(ap_url(format!(
|
||||||
Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain,
|
"{}/tag/{}",
|
||||||
tag)
|
Instance::get_local(conn)
|
||||||
)).expect("Tag::into_activity: href error");
|
.expect("Tag::into_activity: local instance not found error")
|
||||||
ht.set_name_string(tag).expect("Tag::into_activity: name error");
|
.public_domain,
|
||||||
|
tag
|
||||||
|
))).expect("Tag::into_activity: href error");
|
||||||
|
ht.set_name_string(tag)
|
||||||
|
.expect("Tag::into_activity: name error");
|
||||||
ht
|
ht
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) {
|
pub fn delete(&self, conn: &Connection) {
|
||||||
diesel::delete(self).execute(conn).expect("Tag::delete: database error");
|
diesel::delete(self)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Tag::delete: database error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,15 +1,11 @@
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
#[macro_use] extern crate diesel_migrations;
|
#[macro_use]
|
||||||
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
extern crate plume_models;
|
extern crate plume_models;
|
||||||
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use plume_models::{
|
use plume_models::{Connection as Conn, DATABASE_URL};
|
||||||
DATABASE_URL,
|
|
||||||
Connection as Conn,
|
|
||||||
instance::*,
|
|
||||||
safe_string::SafeString,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
embed_migrations!("../migrations/sqlite");
|
embed_migrations!("../migrations/sqlite");
|
||||||
|
@ -24,24 +20,7 @@ fn db() -> Conn {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn instance_insert() {
|
fn empty_test() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| Ok(()));
|
||||||
Instance::insert(conn, NewInstance {
|
|
||||||
default_license: "WTFPL".to_string(),
|
|
||||||
local: true,
|
|
||||||
long_description: SafeString::new("This is my instance."),
|
|
||||||
long_description_html: "<p>This is my instance</p>".to_string(),
|
|
||||||
short_description: SafeString::new("My instance."),
|
|
||||||
short_description_html: "<p>My instance</p>".to_string(),
|
|
||||||
name: "My instance".to_string(),
|
|
||||||
open_registrations: true,
|
|
||||||
public_domain: "plu.me".to_string(),
|
|
||||||
});
|
|
||||||
let inst = Instance::get_local(conn);
|
|
||||||
assert!(inst.is_some());
|
|
||||||
let inst = inst.unwrap();
|
|
||||||
assert_eq!(inst.name, "My instance".to_string());
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
112
src/inbox.rs
112
src/inbox.rs
|
@ -13,65 +13,102 @@ use activitypub::{
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use plume_common::activity_pub::{Id, inbox::{Deletable, FromActivity, InboxError}};
|
use plume_common::activity_pub::{
|
||||||
|
inbox::{Deletable, FromActivity, InboxError},
|
||||||
|
Id,
|
||||||
|
};
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
Connection,
|
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
|
||||||
comments::Comment,
|
users::User, Connection,
|
||||||
follows::Follow,
|
|
||||||
instance::Instance,
|
|
||||||
likes,
|
|
||||||
reshares::Reshare,
|
|
||||||
posts::Post,
|
|
||||||
users::User
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait Inbox {
|
pub trait Inbox {
|
||||||
fn received(&self, conn: &Connection, act: serde_json::Value) -> Result<(), Error> {
|
fn received(&self, conn: &Connection, act: serde_json::Value) -> Result<(), Error> {
|
||||||
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| act["actor"]["id"].as_str().expect("Inbox::received: actor_id missing error")));
|
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| {
|
||||||
|
act["actor"]["id"]
|
||||||
|
.as_str()
|
||||||
|
.expect("Inbox::received: actor_id missing error")
|
||||||
|
}));
|
||||||
match act["type"].as_str() {
|
match act["type"].as_str() {
|
||||||
Some(t) => {
|
Some(t) => match t {
|
||||||
match t {
|
|
||||||
"Announce" => {
|
"Announce" => {
|
||||||
Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
"Create" => {
|
"Create" => {
|
||||||
let act: Create = serde_json::from_value(act.clone())?;
|
let act: Create = serde_json::from_value(act.clone())?;
|
||||||
if Post::try_from_activity(conn, act.clone()) || Comment::try_from_activity(conn, act) {
|
if Post::try_from_activity(conn, act.clone())
|
||||||
|
|| Comment::try_from_activity(conn, act)
|
||||||
|
{
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(InboxError::InvalidType)?
|
Err(InboxError::InvalidType)?
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"Delete" => {
|
"Delete" => {
|
||||||
let act: Delete = serde_json::from_value(act.clone())?;
|
let act: Delete = serde_json::from_value(act.clone())?;
|
||||||
Post::delete_id(act.delete_props.object_object::<Tombstone>()?.object_props.id_string()?, actor_id.into(), conn);
|
Post::delete_id(
|
||||||
|
act.delete_props
|
||||||
|
.object_object::<Tombstone>()?
|
||||||
|
.object_props
|
||||||
|
.id_string()?,
|
||||||
|
actor_id.into(),
|
||||||
|
conn,
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
"Follow" => {
|
"Follow" => {
|
||||||
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
"Like" => {
|
"Like" => {
|
||||||
likes::Like::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
likes::Like::from_activity(
|
||||||
Ok(())
|
conn,
|
||||||
},
|
serde_json::from_value(act.clone())?,
|
||||||
"Undo" => {
|
actor_id,
|
||||||
let act: Undo = serde_json::from_value(act.clone())?;
|
);
|
||||||
match act.undo_props.object["type"].as_str().expect("Inbox::received: undo without original type error") {
|
|
||||||
"Like" => {
|
|
||||||
likes::Like::delete_id(act.undo_props.object_object::<Like>()?.object_props.id_string()?, actor_id.into(), conn);
|
|
||||||
Ok(())
|
|
||||||
},
|
|
||||||
"Announce" => {
|
|
||||||
Reshare::delete_id(act.undo_props.object_object::<Announce>()?.object_props.id_string()?, actor_id.into(), conn);
|
|
||||||
Ok(())
|
|
||||||
},
|
|
||||||
"Follow" => {
|
|
||||||
Follow::delete_id(act.undo_props.object_object::<FollowAct>()?.object_props.id_string()?, actor_id.into(), conn);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(InboxError::CantUndo)?
|
"Undo" => {
|
||||||
|
let act: Undo = serde_json::from_value(act.clone())?;
|
||||||
|
match act.undo_props.object["type"]
|
||||||
|
.as_str()
|
||||||
|
.expect("Inbox::received: undo without original type error")
|
||||||
|
{
|
||||||
|
"Like" => {
|
||||||
|
likes::Like::delete_id(
|
||||||
|
act.undo_props
|
||||||
|
.object_object::<Like>()?
|
||||||
|
.object_props
|
||||||
|
.id_string()?,
|
||||||
|
actor_id.into(),
|
||||||
|
conn,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
"Announce" => {
|
||||||
|
Reshare::delete_id(
|
||||||
|
act.undo_props
|
||||||
|
.object_object::<Announce>()?
|
||||||
|
.object_props
|
||||||
|
.id_string()?,
|
||||||
|
actor_id.into(),
|
||||||
|
conn,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
"Follow" => {
|
||||||
|
Follow::delete_id(
|
||||||
|
act.undo_props
|
||||||
|
.object_object::<FollowAct>()?
|
||||||
|
.object_props
|
||||||
|
.id_string()?,
|
||||||
|
actor_id.into(),
|
||||||
|
conn,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(InboxError::CantUndo)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Update" => {
|
"Update" => {
|
||||||
|
@ -79,10 +116,9 @@ pub trait Inbox {
|
||||||
Post::handle_update(conn, act.update_props.object_object()?);
|
Post::handle_update(conn, act.update_props.object_object()?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(InboxError::InvalidType)?
|
_ => Err(InboxError::InvalidType)?,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
None => Err(InboxError::NoType)?
|
None => Err(InboxError::NoType)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,27 @@
|
||||||
use activitypub::{
|
use activitypub::{activity::Create, collection::OrderedCollection, object::Article};
|
||||||
activity::Create,
|
|
||||||
collection::OrderedCollection,
|
|
||||||
object::Article
|
|
||||||
};
|
|
||||||
use atom_syndication::{Entry, FeedBuilder};
|
use atom_syndication::{Entry, FeedBuilder};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
http::{ContentType, Cookies},
|
||||||
request::LenientForm,
|
request::LenientForm,
|
||||||
response::{Content, Flash, Redirect, status},
|
response::{status, Content, Flash, Redirect},
|
||||||
http::{ContentType, Cookies}
|
|
||||||
};
|
};
|
||||||
use rocket_contrib::Template;
|
use rocket_contrib::Template;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use validator::{Validate, ValidationError};
|
use validator::{Validate, ValidationError};
|
||||||
use workerpool::thunk::*;
|
use workerpool::thunk::*;
|
||||||
|
|
||||||
|
use inbox::Inbox;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
ActivityStream, broadcast, Id, IntoId, ApRequest,
|
broadcast,
|
||||||
inbox::{FromActivity, Notify, Deletable},
|
inbox::{Deletable, FromActivity, Notify},
|
||||||
sign::{Signable, verify_http_headers}
|
sign::{verify_http_headers, Signable},
|
||||||
|
ActivityStream, ApRequest, Id, IntoId,
|
||||||
};
|
};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
blogs::Blog,
|
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::Post,
|
||||||
db_conn::DbConn,
|
reshares::Reshare, users::*,
|
||||||
follows,
|
|
||||||
headers::Headers,
|
|
||||||
instance::Instance,
|
|
||||||
posts::Post,
|
|
||||||
reshares::Reshare,
|
|
||||||
users::*
|
|
||||||
};
|
};
|
||||||
use inbox::Inbox;
|
|
||||||
use routes::Page;
|
use routes::Page;
|
||||||
use Worker;
|
use Worker;
|
||||||
|
|
||||||
|
@ -38,13 +29,25 @@ use Worker;
|
||||||
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||||
match user {
|
match user {
|
||||||
Some(user) => Ok(Redirect::to(uri!(details: name = user.username))),
|
Some(user) => Ok(Redirect::to(uri!(details: name = user.username))),
|
||||||
None => Err(utils::requires_login("", uri!(me).into()))
|
None => Err(utils::requires_login("", uri!(me).into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>", rank = 2)]
|
#[get("/@/<name>", rank = 2)]
|
||||||
fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fecth_articles_conn: DbConn, fecth_followers_conn: DbConn, update_conn: DbConn) -> Template {
|
fn details(
|
||||||
may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name), "Couldn't find requested user", |user| {
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
account: Option<User>,
|
||||||
|
worker: Worker,
|
||||||
|
fecth_articles_conn: DbConn,
|
||||||
|
fecth_followers_conn: DbConn,
|
||||||
|
update_conn: DbConn,
|
||||||
|
) -> Template {
|
||||||
|
may_fail!(
|
||||||
|
account.map(|a| a.to_json(&*conn)),
|
||||||
|
User::find_by_fqn(&*conn, name),
|
||||||
|
"Couldn't find requested user",
|
||||||
|
|user| {
|
||||||
let recents = Post::get_recents_for_author(&*conn, &user, 6);
|
let recents = Post::get_recents_for_author(&*conn, &user, 6);
|
||||||
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
|
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
|
||||||
let user_id = user.id.clone();
|
let user_id = user.id.clone();
|
||||||
|
@ -57,10 +60,16 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
|
||||||
for create_act in user_clone.fetch_outbox::<Create>() {
|
for create_act in user_clone.fetch_outbox::<Create>() {
|
||||||
match create_act.create_props.object_object::<Article>() {
|
match create_act.create_props.object_object::<Article>() {
|
||||||
Ok(article) => {
|
Ok(article) => {
|
||||||
Post::from_activity(&*fecth_articles_conn, article, user_clone.clone().into_id());
|
Post::from_activity(
|
||||||
|
&*fecth_articles_conn,
|
||||||
|
article,
|
||||||
|
user_clone.clone().into_id(),
|
||||||
|
);
|
||||||
println!("Fetched article from remote user");
|
println!("Fetched article from remote user");
|
||||||
}
|
}
|
||||||
Err(e) => println!("Error while fetching articles in background: {:?}", e)
|
Err(e) => {
|
||||||
|
println!("Error while fetching articles in background: {:?}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@ -69,13 +78,20 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
worker.execute(Thunk::of(move || {
|
worker.execute(Thunk::of(move || {
|
||||||
for user_id in user_clone.fetch_followers_ids() {
|
for user_id in user_clone.fetch_followers_ids() {
|
||||||
let follower = User::find_by_ap_url(&*fecth_followers_conn, user_id.clone())
|
let follower =
|
||||||
.unwrap_or_else(|| User::fetch_from_url(&*fecth_followers_conn, user_id).expect("user::details: Couldn't fetch follower"));
|
User::find_by_ap_url(&*fecth_followers_conn, user_id.clone())
|
||||||
follows::Follow::insert(&*fecth_followers_conn, follows::NewFollow {
|
.unwrap_or_else(|| {
|
||||||
|
User::fetch_from_url(&*fecth_followers_conn, user_id)
|
||||||
|
.expect("user::details: Couldn't fetch follower")
|
||||||
|
});
|
||||||
|
follows::Follow::insert(
|
||||||
|
&*fecth_followers_conn,
|
||||||
|
follows::NewFollow {
|
||||||
follower_id: follower.id,
|
follower_id: follower.id,
|
||||||
following_id: user_clone.id,
|
following_id: user_clone.id,
|
||||||
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
|
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -88,7 +104,9 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Template::render("users/details", json!({
|
Template::render(
|
||||||
|
"users/details",
|
||||||
|
json!({
|
||||||
"user": user.to_json(&*conn),
|
"user": user.to_json(&*conn),
|
||||||
"instance_url": user.get_instance(&*conn).public_domain,
|
"instance_url": user.get_instance(&*conn).public_domain,
|
||||||
"is_remote": user.instance_id != Instance::local_id(&*conn),
|
"is_remote": user.instance_id != Instance::local_id(&*conn),
|
||||||
|
@ -98,25 +116,30 @@ fn details(name: String, conn: DbConn, account: Option<User>, worker: Worker, fe
|
||||||
"reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
"reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||||
"is_self": account.map(|a| a.id == user_id).unwrap_or(false),
|
"is_self": account.map(|a| a.id == user_id).unwrap_or(false),
|
||||||
"n_followers": n_followers
|
"n_followers": n_followers
|
||||||
}))
|
}),
|
||||||
})
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/dashboard")]
|
#[get("/dashboard")]
|
||||||
fn dashboard(user: User, conn: DbConn) -> Template {
|
fn dashboard(user: User, conn: DbConn) -> Template {
|
||||||
let blogs = Blog::find_for_author(&*conn, user.id);
|
let blogs = Blog::find_for_author(&*conn, &user);
|
||||||
Template::render("users/dashboard", json!({
|
Template::render(
|
||||||
|
"users/dashboard",
|
||||||
|
json!({
|
||||||
"account": user.to_json(&*conn),
|
"account": user.to_json(&*conn),
|
||||||
"blogs": blogs,
|
"blogs": blogs,
|
||||||
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/dashboard", rank = 2)]
|
#[get("/dashboard", rank = 2)]
|
||||||
fn dashboard_auth() -> Flash<Redirect> {
|
fn dashboard_auth() -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
"You need to be logged in order to access your dashboard",
|
"You need to be logged in order to access your dashboard",
|
||||||
uri!(dashboard).into()
|
uri!(dashboard).into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,13 +148,18 @@ fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redi
|
||||||
let target = User::find_by_fqn(&*conn, name.clone())?;
|
let target = User::find_by_fqn(&*conn, name.clone())?;
|
||||||
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||||
let delete_act = follow.delete(&*conn);
|
let delete_act = follow.delete(&*conn);
|
||||||
worker.execute(Thunk::of(move || broadcast(&user, delete_act, vec![target])));
|
worker.execute(Thunk::of(move || {
|
||||||
|
broadcast(&user, delete_act, vec![target])
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
let f = follows::Follow::insert(&*conn, follows::NewFollow {
|
let f = follows::Follow::insert(
|
||||||
|
&*conn,
|
||||||
|
follows::NewFollow {
|
||||||
follower_id: user.id,
|
follower_id: user.id,
|
||||||
following_id: target.id,
|
following_id: target.id,
|
||||||
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
|
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
f.notify(&*conn);
|
f.notify(&*conn);
|
||||||
|
|
||||||
let act = f.into_activity(&*conn);
|
let act = f.into_activity(&*conn);
|
||||||
|
@ -144,17 +172,23 @@ fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redi
|
||||||
fn follow_auth(name: String) -> Flash<Redirect> {
|
fn follow_auth(name: String) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
"You need to be logged in order to follow someone",
|
"You need to be logged in order to follow someone",
|
||||||
uri!(follow: name = name).into()
|
uri!(follow: name = name).into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/followers?<page>")]
|
#[get("/@/<name>/followers?<page>")]
|
||||||
fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template {
|
fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template {
|
||||||
may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name.clone()), "Couldn't find requested user", |user| {
|
may_fail!(
|
||||||
|
account.map(|a| a.to_json(&*conn)),
|
||||||
|
User::find_by_fqn(&*conn, name.clone()),
|
||||||
|
"Couldn't find requested user",
|
||||||
|
|user| {
|
||||||
let user_id = user.id.clone();
|
let user_id = user.id.clone();
|
||||||
let followers_count = user.get_followers(&*conn).len();
|
let followers_count = user.get_followers(&*conn).len();
|
||||||
|
|
||||||
Template::render("users/followers", json!({
|
Template::render(
|
||||||
|
"users/followers",
|
||||||
|
json!({
|
||||||
"user": user.to_json(&*conn),
|
"user": user.to_json(&*conn),
|
||||||
"instance_url": user.get_instance(&*conn).public_domain,
|
"instance_url": user.get_instance(&*conn).public_domain,
|
||||||
"is_remote": user.instance_id != Instance::local_id(&*conn),
|
"is_remote": user.instance_id != Instance::local_id(&*conn),
|
||||||
|
@ -165,8 +199,10 @@ fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page:
|
||||||
"n_followers": followers_count,
|
"n_followers": followers_count,
|
||||||
"page": page.page,
|
"page": page.page,
|
||||||
"n_pages": Page::total(followers_count as i32)
|
"n_pages": Page::total(followers_count as i32)
|
||||||
}))
|
}),
|
||||||
})
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/followers", rank = 2)]
|
#[get("/@/<name>/followers", rank = 2)]
|
||||||
|
@ -174,29 +210,38 @@ fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
|
||||||
followers_paginated(name, conn, account, Page::first())
|
followers_paginated(name, conn, account, Page::first())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[get("/@/<name>", rank = 1)]
|
#[get("/@/<name>", rank = 1)]
|
||||||
fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomPerson>> {
|
fn activity_details(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
_ap: ApRequest,
|
||||||
|
) -> Option<ActivityStream<CustomPerson>> {
|
||||||
let user = User::find_local(&*conn, name)?;
|
let user = User::find_local(&*conn, name)?;
|
||||||
Some(ActivityStream::new(user.into_activity(&*conn)))
|
Some(ActivityStream::new(user.into_activity(&*conn)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/new")]
|
#[get("/users/new")]
|
||||||
fn new(user: Option<User>, conn: DbConn) -> Template {
|
fn new(user: Option<User>, conn: DbConn) -> Template {
|
||||||
Template::render("users/new", json!({
|
Template::render(
|
||||||
|
"users/new",
|
||||||
|
json!({
|
||||||
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
||||||
"account": user.map(|u| u.to_json(&*conn)),
|
"account": user.map(|u| u.to_json(&*conn)),
|
||||||
"errors": null,
|
"errors": null,
|
||||||
"form": null
|
"form": null
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/edit")]
|
#[get("/@/<name>/edit")]
|
||||||
fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
|
fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
|
||||||
if user.username == name && !name.contains("@") {
|
if user.username == name && !name.contains("@") {
|
||||||
Some(Template::render("users/edit", json!({
|
Some(Template::render(
|
||||||
|
"users/edit",
|
||||||
|
json!({
|
||||||
"account": user.to_json(&*conn)
|
"account": user.to_json(&*conn)
|
||||||
})))
|
}),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -206,7 +251,7 @@ fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
|
||||||
fn edit_auth(name: String) -> Flash<Redirect> {
|
fn edit_auth(name: String) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
"You need to be logged in order to edit your profile",
|
"You need to be logged in order to edit your profile",
|
||||||
uri!(edit: name = name).into()
|
uri!(edit: name = name).into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,10 +264,22 @@ struct UpdateUserForm {
|
||||||
|
|
||||||
#[put("/@/<_name>/edit", data = "<data>")]
|
#[put("/@/<_name>/edit", data = "<data>")]
|
||||||
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect {
|
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect {
|
||||||
user.update(&*conn,
|
user.update(
|
||||||
data.get().display_name.clone().unwrap_or(user.display_name.to_string()).to_string(),
|
&*conn,
|
||||||
data.get().email.clone().unwrap_or(user.email.clone().unwrap()).to_string(),
|
data.get()
|
||||||
data.get().summary.clone().unwrap_or(user.summary.to_string())
|
.display_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(user.display_name.to_string())
|
||||||
|
.to_string(),
|
||||||
|
data.get()
|
||||||
|
.email
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(user.email.clone().unwrap())
|
||||||
|
.to_string(),
|
||||||
|
data.get()
|
||||||
|
.summary
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(user.summary.to_string()),
|
||||||
);
|
);
|
||||||
Redirect::to(uri!(me))
|
Redirect::to(uri!(me))
|
||||||
}
|
}
|
||||||
|
@ -233,7 +290,9 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
|
||||||
if user.id == account.id {
|
if user.id == account.id {
|
||||||
account.delete(&*conn);
|
account.delete(&*conn);
|
||||||
|
|
||||||
cookies.get_private(AUTH_COOKIE).map(|cookie| cookies.remove_private(cookie));
|
cookies
|
||||||
|
.get_private(AUTH_COOKIE)
|
||||||
|
.map(|cookie| cookies.remove_private(cookie));
|
||||||
|
|
||||||
Some(Redirect::to(uri!(super::instance::index)))
|
Some(Redirect::to(uri!(super::instance::index)))
|
||||||
} else {
|
} else {
|
||||||
|
@ -242,16 +301,32 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm, Serialize, Validate)]
|
#[derive(FromForm, Serialize, Validate)]
|
||||||
#[validate(schema(function = "passwords_match", skip_on_field_errors = "false", message = "Passwords are not matching"))]
|
#[validate(
|
||||||
|
schema(
|
||||||
|
function = "passwords_match",
|
||||||
|
skip_on_field_errors = "false",
|
||||||
|
message = "Passwords are not matching"
|
||||||
|
)
|
||||||
|
)]
|
||||||
struct NewUserForm {
|
struct NewUserForm {
|
||||||
#[validate(length(min = "1", message = "Username can't be empty"))]
|
#[validate(length(min = "1", message = "Username can't be empty"))]
|
||||||
username: String,
|
username: String,
|
||||||
#[validate(email(message = "Invalid email"))]
|
#[validate(email(message = "Invalid email"))]
|
||||||
email: String,
|
email: String,
|
||||||
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
#[validate(
|
||||||
|
length(
|
||||||
|
min = "8",
|
||||||
|
message = "Password should be at least 8 characters long"
|
||||||
|
)
|
||||||
|
)]
|
||||||
password: String,
|
password: String,
|
||||||
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
#[validate(
|
||||||
password_confirmation: String
|
length(
|
||||||
|
min = "8",
|
||||||
|
message = "Password should be at least 8 characters long"
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
password_confirmation: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
||||||
|
@ -264,7 +339,10 @@ fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
||||||
|
|
||||||
#[post("/users/new", data = "<data>")]
|
#[post("/users/new", data = "<data>")]
|
||||||
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> {
|
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> {
|
||||||
if !Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true) {
|
if !Instance::get_local(&*conn)
|
||||||
|
.map(|i| i.open_registrations)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
return Ok(Redirect::to(uri!(new))); // Actually, it is an error
|
return Ok(Redirect::to(uri!(new))); // Actually, it is an error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,15 +356,20 @@ fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Temp
|
||||||
false,
|
false,
|
||||||
String::from(""),
|
String::from(""),
|
||||||
form.email.to_string(),
|
form.email.to_string(),
|
||||||
User::hash_pass(form.password.to_string())
|
User::hash_pass(form.password.to_string()),
|
||||||
).update_boxes(&*conn);
|
).update_boxes(&*conn);
|
||||||
Redirect::to(uri!(super::session::new))
|
Redirect::to(uri!(super::session::new))
|
||||||
})
|
})
|
||||||
.map_err(|e| Template::render("users/new", json!({
|
.map_err(|e| {
|
||||||
|
Template::render(
|
||||||
|
"users/new",
|
||||||
|
json!({
|
||||||
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
||||||
"errors": e.inner(),
|
"errors": e.inner(),
|
||||||
"form": form
|
"form": form
|
||||||
})))
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/outbox")]
|
#[get("/@/<name>/outbox")]
|
||||||
|
@ -296,18 +379,32 @@ fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/@/<name>/inbox", data = "<data>")]
|
#[post("/@/<name>/inbox", data = "<data>")]
|
||||||
fn inbox(name: String, conn: DbConn, data: String, headers: Headers) -> Result<String, Option<status::BadRequest<&'static str>>> {
|
fn inbox(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
data: String,
|
||||||
|
headers: Headers,
|
||||||
|
) -> Result<String, Option<status::BadRequest<&'static str>>> {
|
||||||
let user = User::find_local(&*conn, name).ok_or(None)?;
|
let user = User::find_local(&*conn, name).ok_or(None)?;
|
||||||
let act: serde_json::Value = serde_json::from_str(&data[..]).expect("user::inbox: deserialization error");
|
let act: serde_json::Value =
|
||||||
|
serde_json::from_str(&data[..]).expect("user::inbox: deserialization error");
|
||||||
|
|
||||||
let activity = act.clone();
|
let activity = act.clone();
|
||||||
let actor_id = activity["actor"].as_str()
|
let actor_id = activity["actor"]
|
||||||
.or_else(|| activity["actor"]["id"].as_str()).ok_or(Some(status::BadRequest(Some("Missing actor id for activity"))))?;
|
.as_str()
|
||||||
|
.or_else(|| activity["actor"]["id"].as_str())
|
||||||
|
.ok_or(Some(status::BadRequest(Some(
|
||||||
|
"Missing actor id for activity",
|
||||||
|
))))?;
|
||||||
|
|
||||||
let actor = User::from_url(&conn, actor_id.to_owned()).expect("user::inbox: user error");
|
let actor = User::from_url(&conn, actor_id.to_owned()).expect("user::inbox: user error");
|
||||||
if !verify_http_headers(&actor, headers.0.clone(), data).is_secure() &&
|
if !verify_http_headers(&actor, headers.0.clone(), data).is_secure()
|
||||||
!act.clone().verify(&actor) {
|
&& !act.clone().verify(&actor)
|
||||||
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
|
{
|
||||||
|
println!(
|
||||||
|
"Rejected invalid activity supposedly from {}, with headers {:?}",
|
||||||
|
actor.username, headers.0
|
||||||
|
);
|
||||||
return Err(Some(status::BadRequest(Some("Invalid signature"))));
|
return Err(Some(status::BadRequest(Some("Invalid signature"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,14 +421,28 @@ fn inbox(name: String, conn: DbConn, data: String, headers: Headers) -> Result<S
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/followers")]
|
#[get("/@/<name>/followers")]
|
||||||
fn ap_followers(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<OrderedCollection>> {
|
fn ap_followers(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
_ap: ApRequest,
|
||||||
|
) -> Option<ActivityStream<OrderedCollection>> {
|
||||||
let user = User::find_local(&*conn, name)?;
|
let user = User::find_local(&*conn, name)?;
|
||||||
let followers = user.get_followers(&*conn).into_iter().map(|f| Id::new(f.ap_url)).collect::<Vec<Id>>();
|
let followers = user
|
||||||
|
.get_followers(&*conn)
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| Id::new(f.ap_url))
|
||||||
|
.collect::<Vec<Id>>();
|
||||||
|
|
||||||
let mut coll = OrderedCollection::default();
|
let mut coll = OrderedCollection::default();
|
||||||
coll.object_props.set_id_string(user.followers_endpoint).expect("user::ap_followers: id error");
|
coll.object_props
|
||||||
coll.collection_props.set_total_items_u64(followers.len() as u64).expect("user::ap_followers: totalItems error");
|
.set_id_string(user.followers_endpoint)
|
||||||
coll.collection_props.set_items_link_vec(followers).expect("user::ap_followers items error");
|
.expect("user::ap_followers: id error");
|
||||||
|
coll.collection_props
|
||||||
|
.set_total_items_u64(followers.len() as u64)
|
||||||
|
.expect("user::ap_followers: totalItems error");
|
||||||
|
coll.collection_props
|
||||||
|
.set_items_link_vec(followers)
|
||||||
|
.expect("user::ap_followers items error");
|
||||||
Some(ActivityStream::new(coll))
|
Some(ActivityStream::new(coll))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,12 +451,19 @@ fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||||
let author = User::find_by_fqn(&*conn, name.clone())?;
|
let author = User::find_by_fqn(&*conn, name.clone())?;
|
||||||
let feed = FeedBuilder::default()
|
let feed = FeedBuilder::default()
|
||||||
.title(author.display_name.clone())
|
.title(author.display_name.clone())
|
||||||
.id(Instance::get_local(&*conn).unwrap().compute_box("~", name, "atom.xml"))
|
.id(Instance::get_local(&*conn)
|
||||||
.entries(Post::get_recents_for_author(&*conn, &author, 15)
|
.unwrap()
|
||||||
|
.compute_box("~", name, "atom.xml"))
|
||||||
|
.entries(
|
||||||
|
Post::get_recents_for_author(&*conn, &author, 15)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| super::post_to_atom(p, &*conn))
|
.map(|p| super::post_to_atom(p, &*conn))
|
||||||
.collect::<Vec<Entry>>())
|
.collect::<Vec<Entry>>(),
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
.expect("user::atom_feed: Error building Atom feed");
|
.expect("user::atom_feed: Error building Atom feed");
|
||||||
Some(Content(ContentType::new("application", "atom+xml"), feed.to_string()))
|
Some(Content(
|
||||||
|
ContentType::new("application", "atom+xml"),
|
||||||
|
feed.to_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue