mirror of
https://git.joinplu.me/Plume/Plume.git
synced 2024-12-23 10:30:41 +00:00
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
30 changed files with 3779 additions and 1123 deletions
|
@ -39,7 +39,7 @@ jobs:
|
|||
name: "Test with potgresql backend"
|
||||
env:
|
||||
- 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
|
||||
script:
|
||||
- |
|
||||
|
@ -49,7 +49,7 @@ jobs:
|
|||
name: "Test with Sqlite backend"
|
||||
env:
|
||||
- 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:
|
||||
- |
|
||||
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;
|
||||
|
||||
|
@ -9,7 +9,7 @@ pub enum InboxError {
|
|||
#[fail(display = "Invalid activity type")]
|
||||
InvalidType,
|
||||
#[fail(display = "Couldn't undo activity")]
|
||||
CantUndo
|
||||
CantUndo,
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
} else {
|
||||
false
|
||||
|
@ -32,7 +38,6 @@ pub trait Notify<C> {
|
|||
pub trait Deletable<C, A> {
|
||||
fn delete(&self, conn: &C) -> A;
|
||||
fn delete_id(id: String, actor_id: String, conn: &C);
|
||||
|
||||
}
|
||||
|
||||
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 reqwest::Client;
|
||||
use rocket::{
|
||||
Outcome, http::Status,
|
||||
response::{Response, Responder},
|
||||
request::{FromRequest, Request}
|
||||
http::Status,
|
||||
request::{FromRequest, Request},
|
||||
response::{Responder, Response},
|
||||
Outcome,
|
||||
};
|
||||
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/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> {
|
||||
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> {
|
||||
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
||||
json["@context"] = context();
|
||||
serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r)
|
||||
.raw_header("Content-Type", "application/activity+json")
|
||||
.finalize())
|
||||
serde_json::to_string(&json).respond_to(request).map(|r| {
|
||||
Response::build_from(r)
|
||||
.raw_header("Content-Type", "application/activity+json")
|
||||
.finalize()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,29 +79,45 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
|
|||
type 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() {
|
||||
// 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/activity+json" |
|
||||
"application/ld+json" => Outcome::Success(ApRequest),
|
||||
"text/html" => Outcome::Forward(true),
|
||||
_ => Outcome::Forward(false)
|
||||
}).fold(Outcome::Forward(false), |out, ct| if out.clone().forwarded().unwrap_or(out.is_success()) {
|
||||
out
|
||||
} else {
|
||||
ct
|
||||
}).map_forward(|_| ())).unwrap_or(Outcome::Forward(()))
|
||||
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
|
||||
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
|
||||
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
|
||||
| "application/activity+json"
|
||||
| "application/ld+json" => Outcome::Success(ApRequest),
|
||||
"text/html" => Outcome::Forward(true),
|
||||
_ => Outcome::Forward(false),
|
||||
})
|
||||
.fold(Outcome::Forward(false), |out, ct| {
|
||||
if out.clone().forwarded().unwrap_or(out.is_success()) {
|
||||
out
|
||||
} else {
|
||||
ct
|
||||
}
|
||||
})
|
||||
.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>) {
|
||||
let boxes = to.into_iter()
|
||||
pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(
|
||||
sender: &S,
|
||||
act: A,
|
||||
to: Vec<T>,
|
||||
) {
|
||||
let boxes = to
|
||||
.into_iter()
|
||||
.filter(|u| !u.is_local())
|
||||
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
|
||||
.collect::<Vec<String>>()
|
||||
.unique();
|
||||
|
||||
|
||||
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
|
||||
act["@context"] = context();
|
||||
let signed = act.sign(sender);
|
||||
|
@ -121,8 +140,8 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(send
|
|||
} else {
|
||||
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")]
|
||||
pub struct ApSignature {
|
||||
#[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)]
|
||||
|
@ -165,7 +184,7 @@ pub struct PublicKey {
|
|||
pub owner: Option<serde_json::Value>,
|
||||
|
||||
#[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)]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use base64;
|
||||
use chrono::{DateTime, offset::Utc};
|
||||
use chrono::{offset::Utc, DateTime};
|
||||
use openssl::hash::{Hasher, MessageDigest};
|
||||
use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT};
|
||||
use std::ops::Deref;
|
||||
|
@ -14,35 +14,52 @@ pub struct Digest(String);
|
|||
|
||||
impl Digest {
|
||||
pub fn digest(body: String) -> HeaderValue {
|
||||
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
||||
hasher.update(&body.into_bytes()[..]).expect("Digest::digest: content insertion error");
|
||||
let mut hasher =
|
||||
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"));
|
||||
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 {
|
||||
if self.algorithm()=="SHA-256" {
|
||||
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
||||
hasher.update(&body.into_bytes()).expect("Digest::digest: content insertion error");
|
||||
self.value().deref()==hasher.finish().expect("Digest::digest: finalizing error").deref()
|
||||
if self.algorithm() == "SHA-256" {
|
||||
let mut hasher =
|
||||
Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
||||
hasher
|
||||
.update(&body.into_bytes())
|
||||
.expect("Digest::digest: content insertion error");
|
||||
self.value().deref()
|
||||
== hasher
|
||||
.finish()
|
||||
.expect("Digest::digest: finalizing error")
|
||||
.deref()
|
||||
} else {
|
||||
false //algorithm not supported
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
pub fn from_header(dig: &str) -> Result<Self, ()> {
|
||||
if let Some(pos) = dig.find('=') {
|
||||
let pos = pos+1;
|
||||
let pos = pos + 1;
|
||||
if let Ok(_) = base64::decode(&dig[pos..]) {
|
||||
Ok(Digest(dig.to_owned()))
|
||||
} else {
|
||||
|
@ -60,15 +77,42 @@ pub fn headers() -> HeaderMap {
|
|||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT));
|
||||
headers.insert(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(
|
||||
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
|
||||
}
|
||||
|
||||
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_headers = headers.iter().map(|(h,_)| h.as_str()).collect::<Vec<&str>>().join(" ").to_lowercase();
|
||||
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_headers = headers
|
||||
.iter()
|
||||
.map(|(h, _)| h.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ")
|
||||
.to_lowercase();
|
||||
|
||||
let data = signer.sign(signed_string);
|
||||
let sign = base64::encode(&data[..]);
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
use super::request;
|
||||
use base64;
|
||||
use chrono::Utc;
|
||||
use hex;
|
||||
use openssl::{
|
||||
pkey::PKey,
|
||||
rsa::Rsa,
|
||||
sha::sha256
|
||||
};
|
||||
use super::request;
|
||||
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
||||
use rocket::http::HeaderMap;
|
||||
use serde_json;
|
||||
|
||||
|
@ -15,14 +11,18 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
|||
let keypair = Rsa::generate(2048).expect("sign::gen_keypair: key generation 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.private_key_to_pem_pkcs8().expect("sign::gen_keypair: private key encoding error")
|
||||
keypair
|
||||
.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"),
|
||||
)
|
||||
}
|
||||
|
||||
pub trait Signer {
|
||||
fn get_key_id(&self) -> String;
|
||||
|
||||
|
||||
/// Sign some data with the signer keypair
|
||||
fn sign(&self, to_sign: String) -> Vec<u8>;
|
||||
/// Verify if the signature is valid
|
||||
|
@ -30,8 +30,12 @@ pub trait Signer {
|
|||
}
|
||||
|
||||
pub trait Signable {
|
||||
fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer;
|
||||
fn verify<T>(self, creator: &T) -> bool where T: Signer;
|
||||
fn sign<T>(&mut self, creator: &T) -> &mut Self
|
||||
where
|
||||
T: Signer;
|
||||
fn verify<T>(self, creator: &T) -> bool
|
||||
where
|
||||
T: Signer;
|
||||
|
||||
fn hash(data: String) -> String {
|
||||
let bytes = data.into_bytes();
|
||||
|
@ -48,10 +52,12 @@ impl Signable for serde_json::Value {
|
|||
"created": creation_date
|
||||
});
|
||||
|
||||
let options_hash = Self::hash(json!({
|
||||
let options_hash = Self::hash(
|
||||
json!({
|
||||
"@context": "https://w3id.org/identity/v1",
|
||||
"created": creation_date
|
||||
}).to_string());
|
||||
}).to_string(),
|
||||
);
|
||||
let document_hash = Self::hash(self.to_string());
|
||||
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 {
|
||||
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
|
||||
} else {
|
||||
//signature not present
|
||||
return false;
|
||||
};
|
||||
let signature = if let Ok(sig) =
|
||||
base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or(""))
|
||||
{
|
||||
sig
|
||||
} else {
|
||||
//signature not present
|
||||
return false
|
||||
};
|
||||
let signature = if let Ok(sig) = base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) {
|
||||
sig
|
||||
} else {
|
||||
return false
|
||||
return false;
|
||||
};
|
||||
let creation_date = &signature_obj["created"];
|
||||
let options_hash = Self::hash(json!({
|
||||
let options_hash = Self::hash(
|
||||
json!({
|
||||
"@context": "https://w3id.org/identity/v1",
|
||||
"created": creation_date
|
||||
}).to_string());
|
||||
}).to_string(),
|
||||
);
|
||||
let document_hash = Self::hash(self.to_string());
|
||||
let to_be_signed = options_hash + &document_hash;
|
||||
creator.verify(to_be_signed, signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Copy,Clone,PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum SignatureValidity {
|
||||
Invalid,
|
||||
ValidNoDigest,
|
||||
|
@ -95,14 +106,18 @@ pub enum SignatureValidity {
|
|||
|
||||
impl SignatureValidity {
|
||||
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");
|
||||
if sig_header.is_none() {
|
||||
return SignatureValidity::Absent
|
||||
return SignatureValidity::Absent;
|
||||
}
|
||||
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;
|
||||
for part in sig_header.split(',') {
|
||||
match part {
|
||||
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("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("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("headers=") => headers = Some(&part[9..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
|
||||
return SignatureValidity::Invalid
|
||||
if signature.is_none() || headers.is_none() {
|
||||
//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 h = headers.iter()
|
||||
.map(|header| (header,all_headers.get_one(header)))
|
||||
let h = headers
|
||||
.iter()
|
||||
.map(|header| (header, all_headers.get_one(header)))
|
||||
.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())) {
|
||||
return SignatureValidity::Invalid
|
||||
return SignatureValidity::Invalid;
|
||||
}
|
||||
if !headers.contains(&"digest") {// signature is valid, but body content is not verified
|
||||
return SignatureValidity::ValidNoDigest
|
||||
if !headers.contains(&"digest") {
|
||||
// signature is valid, but body content is not verified
|
||||
return SignatureValidity::ValidNoDigest;
|
||||
}
|
||||
let digest = all_headers.get_one("digest").unwrap_or("");
|
||||
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
|
||||
} 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;
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use rocket::{
|
||||
Outcome,
|
||||
http::Status,
|
||||
request::{self, FromRequest, Request}
|
||||
request::{self, FromRequest, Request},
|
||||
Outcome,
|
||||
};
|
||||
|
||||
use db_conn::DbConn;
|
||||
|
@ -48,7 +48,7 @@ impl ApiToken {
|
|||
let full_scope = what.to_owned() + ":" + scope;
|
||||
for s in self.scopes.split('+') {
|
||||
if s == what || s == full_scope {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
|
|
23
plume-models/src/apps.rs
Executable file → Normal file
23
plume-models/src/apps.rs
Executable file → Normal file
|
@ -1,11 +1,11 @@
|
|||
use canapi::{Error, Provider};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use plume_api::apps::AppEndpoint;
|
||||
use plume_common::utils::random_hex;
|
||||
use Connection;
|
||||
use schema::apps;
|
||||
use Connection;
|
||||
|
||||
#[derive(Clone, Queryable)]
|
||||
pub struct App {
|
||||
|
@ -19,7 +19,7 @@ pub struct App {
|
|||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name= "apps"]
|
||||
#[table_name = "apps"]
|
||||
pub struct NewApp {
|
||||
pub name: String,
|
||||
pub client_id: String,
|
||||
|
@ -43,13 +43,16 @@ impl Provider<Connection> for App {
|
|||
let client_id = random_hex();
|
||||
|
||||
let client_secret = random_hex();
|
||||
let app = App::insert(conn, NewApp {
|
||||
name: data.name,
|
||||
client_id: client_id,
|
||||
client_secret: client_secret,
|
||||
redirect_uri: data.redirect_uri,
|
||||
website: data.website,
|
||||
});
|
||||
let app = App::insert(
|
||||
conn,
|
||||
NewApp {
|
||||
name: data.name,
|
||||
client_id: client_id,
|
||||
client_secret: client_secret,
|
||||
redirect_uri: data.redirect_uri,
|
||||
website: data.website,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(AppEndpoint {
|
||||
id: Some(app.id),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
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 reqwest::{Client,
|
||||
header::{ACCEPT, HeaderValue}
|
||||
};
|
||||
use serde_json;
|
||||
use url::Url;
|
||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
sign::{Signer,Verifier}
|
||||
sign::{Signer, Verifier},
|
||||
};
|
||||
use reqwest::{
|
||||
header::{HeaderValue, ACCEPT},
|
||||
Client,
|
||||
};
|
||||
use serde_json;
|
||||
use url::Url;
|
||||
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 plume_common::activity_pub::{
|
||||
ap_accept_header,
|
||||
inbox::{Deletable, WithInbox},
|
||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey,
|
||||
};
|
||||
use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
use schema::blogs;
|
||||
use users::User;
|
||||
use {Connection, BASE_URL, USE_HTTPS};
|
||||
|
||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||
|
||||
|
@ -40,7 +41,7 @@ pub struct Blog {
|
|||
pub creation_date: NaiveDateTime,
|
||||
pub ap_url: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: String
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
|
@ -54,7 +55,7 @@ pub struct NewBlog {
|
|||
pub instance_id: i32,
|
||||
pub ap_url: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: String
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
const BLOG_PREFIX: &'static str = "~";
|
||||
|
@ -72,16 +73,22 @@ impl Blog {
|
|||
pub fn list_authors(&self, conn: &Connection) -> Vec<User> {
|
||||
use schema::blog_authors;
|
||||
use schema::users;
|
||||
let authors_ids = blog_authors::table.filter(blog_authors::blog_id.eq(self.id)).select(blog_authors::author_id);
|
||||
users::table.filter(users::id.eq_any(authors_ids))
|
||||
let authors_ids = blog_authors::table
|
||||
.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)
|
||||
.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;
|
||||
let author_ids = blog_authors::table.filter(blog_authors::author_id.eq(author_id)).select(blog_authors::blog_id);
|
||||
blogs::table.filter(blogs::id.eq_any(author_ids))
|
||||
let author_ids = blog_authors::table
|
||||
.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)
|
||||
.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> {
|
||||
if fqn.contains("@") { // remote blog
|
||||
match Instance::find_by_domain(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),
|
||||
None => Blog::fetch_from_webfinger(conn, fqn)
|
||||
}
|
||||
if fqn.contains("@") {
|
||||
// remote blog
|
||||
match Instance::find_by_domain(
|
||||
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),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option<Blog> {
|
||||
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) => {
|
||||
println!("{:?}", details);
|
||||
None
|
||||
|
@ -119,17 +151,37 @@ impl Blog {
|
|||
fn fetch_from_url(conn: &Connection, url: String) -> Option<Blog> {
|
||||
let req = Client::new()
|
||||
.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();
|
||||
match req {
|
||||
Ok(mut res) => {
|
||||
let text = &res.text().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");
|
||||
let text = &res
|
||||
.text()
|
||||
.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
|
||||
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()))
|
||||
},
|
||||
Err(_) => None
|
||||
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(),
|
||||
))
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,49 +189,103 @@ impl Blog {
|
|||
let instance = match Instance::find_by_domain(conn, inst.clone()) {
|
||||
Some(instance) => instance,
|
||||
None => {
|
||||
Instance::insert(conn, NewInstance {
|
||||
public_domain: inst.clone(),
|
||||
name: inst.clone(),
|
||||
local: false,
|
||||
// We don't really care about all the following for remote instances
|
||||
long_description: SafeString::new(""),
|
||||
short_description: SafeString::new(""),
|
||||
default_license: String::new(),
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new()
|
||||
})
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain: inst.clone(),
|
||||
name: inst.clone(),
|
||||
local: false,
|
||||
// We don't really care about all the following for remote instances
|
||||
long_description: SafeString::new(""),
|
||||
short_description: SafeString::new(""),
|
||||
default_license: String::new(),
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
Blog::insert(conn, NewBlog {
|
||||
actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"),
|
||||
title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"),
|
||||
outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"),
|
||||
inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"),
|
||||
summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"),
|
||||
instance_id: instance.id,
|
||||
ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"),
|
||||
public_key: acct.custom_props.public_key_publickey().expect("Blog::from_activity: publicKey error")
|
||||
.public_key_pem_string().expect("Blog::from_activity: publicKey.publicKeyPem error"),
|
||||
private_key: None
|
||||
})
|
||||
Blog::insert(
|
||||
conn,
|
||||
NewBlog {
|
||||
actor_id: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.preferred_username_string()
|
||||
.expect("Blog::from_activity: preferredUsername error"),
|
||||
title: acct
|
||||
.object
|
||||
.object_props
|
||||
.name_string()
|
||||
.expect("Blog::from_activity: name error"),
|
||||
outbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.outbox_string()
|
||||
.expect("Blog::from_activity: outbox error"),
|
||||
inbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.inbox_string()
|
||||
.expect("Blog::from_activity: inbox error"),
|
||||
summary: acct
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()
|
||||
.expect("Blog::from_activity: summary error"),
|
||||
instance_id: instance.id,
|
||||
ap_url: acct
|
||||
.object
|
||||
.object_props
|
||||
.id_string()
|
||||
.expect("Blog::from_activity: id error"),
|
||||
public_key: acct
|
||||
.custom_props
|
||||
.public_key_publickey()
|
||||
.expect("Blog::from_activity: publicKey error")
|
||||
.public_key_pem_string()
|
||||
.expect("Blog::from_activity: publicKey.publicKeyPem error"),
|
||||
private_key: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn into_activity(&self, _conn: &Connection) -> CustomGroup {
|
||||
let mut blog = Group::default();
|
||||
blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error");
|
||||
blog.object_props.set_name_string(self.title.clone()).expect("Blog::into_activity: name error");
|
||||
blog.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("Blog::into_activity: outbox error");
|
||||
blog.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("Blog::into_activity: inbox error");
|
||||
blog.object_props.set_summary_string(self.summary.clone()).expect("Blog::into_activity: summary error");
|
||||
blog.object_props.set_id_string(self.ap_url.clone()).expect("Blog::into_activity: id error");
|
||||
blog.ap_actor_props
|
||||
.set_preferred_username_string(self.actor_id.clone())
|
||||
.expect("Blog::into_activity: preferredUsername error");
|
||||
blog.object_props
|
||||
.set_name_string(self.title.clone())
|
||||
.expect("Blog::into_activity: name error");
|
||||
blog.ap_actor_props
|
||||
.set_outbox_string(self.outbox_url.clone())
|
||||
.expect("Blog::into_activity: outbox error");
|
||||
blog.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())
|
||||
.expect("Blog::into_activity: inbox error");
|
||||
blog.object_props
|
||||
.set_summary_string(self.summary.clone())
|
||||
.expect("Blog::into_activity: summary error");
|
||||
blog.object_props
|
||||
.set_id_string(self.ap_url.clone())
|
||||
.expect("Blog::into_activity: id error");
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error");
|
||||
public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error");
|
||||
public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error");
|
||||
public_key
|
||||
.set_id_string(format!("{}#main-key", self.ap_url))
|
||||
.expect("Blog::into_activity: publicKey.id error");
|
||||
public_key
|
||||
.set_owner_string(self.ap_url.clone())
|
||||
.expect("Blog::into_activity: publicKey.owner error");
|
||||
public_key
|
||||
.set_public_key_pem_string(self.public_key.clone())
|
||||
.expect("Blog::into_activity: publicKey.publicKeyPem error");
|
||||
let mut ap_signature = ApSignature::default();
|
||||
ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error");
|
||||
ap_signature
|
||||
.set_public_key_publickey(public_key)
|
||||
.expect("Blog::into_activity: publicKey error");
|
||||
|
||||
CustomGroup::new(blog, ap_signature)
|
||||
}
|
||||
|
@ -188,27 +294,41 @@ impl Blog {
|
|||
let instance = self.get_instance(conn);
|
||||
if self.outbox_url.len() == 0 {
|
||||
diesel::update(self)
|
||||
.set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox")))
|
||||
.execute(conn).expect("Blog::update_boxes: outbox update error");
|
||||
.set(blogs::outbox_url.eq(instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
self.actor_id.clone(),
|
||||
"outbox",
|
||||
)))
|
||||
.execute(conn)
|
||||
.expect("Blog::update_boxes: outbox update error");
|
||||
}
|
||||
|
||||
if self.inbox_url.len() == 0 {
|
||||
diesel::update(self)
|
||||
.set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox")))
|
||||
.execute(conn).expect("Blog::update_boxes: inbox update error");
|
||||
.set(blogs::inbox_url.eq(instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
self.actor_id.clone(),
|
||||
"inbox",
|
||||
)))
|
||||
.execute(conn)
|
||||
.expect("Blog::update_boxes: inbox update error");
|
||||
}
|
||||
|
||||
if self.ap_url.len() == 0 {
|
||||
diesel::update(self)
|
||||
.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> {
|
||||
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.set_total_items_u64(self.get_activities(conn).len() as u64).expect("Blog::outbox: count serialization error");
|
||||
coll.collection_props.items = serde_json::to_value(self.get_activities(conn))
|
||||
.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)
|
||||
}
|
||||
|
||||
|
@ -217,35 +337,48 @@ impl Blog {
|
|||
}
|
||||
|
||||
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())
|
||||
.expect("Blog::get_keypair: pem parsing error"))
|
||||
.expect("Blog::get_keypair: private key deserialization error")
|
||||
PKey::from_rsa(
|
||||
Rsa::private_key_from_pem(
|
||||
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 {
|
||||
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()],
|
||||
links: vec![
|
||||
Link {
|
||||
rel: String::from("http://webfinger.net/rel/profile-page"),
|
||||
mime_type: None,
|
||||
href: Some(self.ap_url.clone()),
|
||||
template: None
|
||||
template: None,
|
||||
},
|
||||
Link {
|
||||
rel: String::from("http://schemas.google.com/g/2010#updates-from"),
|
||||
mime_type: Some(String::from("application/atom+xml")),
|
||||
href: Some(self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")),
|
||||
template: None
|
||||
href: Some(self.get_instance(conn).compute_box(
|
||||
BLOG_PREFIX,
|
||||
self.actor_id.clone(),
|
||||
"feed.atom",
|
||||
)),
|
||||
template: None,
|
||||
},
|
||||
Link {
|
||||
rel: String::from("self"),
|
||||
mime_type: Some(String::from("application/activity+json")),
|
||||
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(|| {
|
||||
// The requested blog was not in the DB
|
||||
// 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)
|
||||
} else {
|
||||
None
|
||||
|
@ -265,7 +402,11 @@ impl Blog {
|
|||
if self.instance_id == Instance::local_id(conn) {
|
||||
self.actor_id.clone()
|
||||
} else {
|
||||
format!("{}@{}", self.actor_id, self.get_instance(conn).public_domain)
|
||||
format!(
|
||||
"{}@{}",
|
||||
self.actor_id,
|
||||
self.get_instance(conn).public_domain
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,9 +418,11 @@ impl Blog {
|
|||
|
||||
pub fn delete(&self, conn: &Connection) {
|
||||
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> {
|
||||
let key = self.get_keypair();
|
||||
let mut signer = Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error");
|
||||
signer.update(to_sign.as_bytes()).expect("Blog::sign: content insertion error");
|
||||
signer.sign_to_vec().expect("Blog::sign: finalization error")
|
||||
let mut signer =
|
||||
Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization 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 {
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).expect("Blog::verify: pem parsing error"))
|
||||
.expect("Blog::verify: deserialization 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")
|
||||
let key = PKey::from_rsa(
|
||||
Rsa::public_key_from_pem(self.public_key.as_ref())
|
||||
.expect("Blog::verify: pem parsing error"),
|
||||
).expect("Blog::verify: deserialization 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,
|
||||
title: String,
|
||||
summary: String,
|
||||
instance_id: i32
|
||||
instance_id: i32,
|
||||
) -> NewBlog {
|
||||
let (pub_key, priv_key) = sign::gen_keypair();
|
||||
NewBlog {
|
||||
|
@ -344,7 +499,337 @@ impl NewBlog {
|
|||
instance_id: instance_id,
|
||||
ap_url: String::from(""),
|
||||
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::{
|
||||
activity::Create,
|
||||
link,
|
||||
object::{Note}
|
||||
};
|
||||
use activitypub::{activity::Create, link, object::Note};
|
||||
use chrono::{self, NaiveDateTime};
|
||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
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 mentions::Mention;
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{FromActivity, Notify},
|
||||
Id, IntoId, PUBLIC_VISIBILTY,
|
||||
};
|
||||
use plume_common::utils;
|
||||
use posts::Post;
|
||||
use users::User;
|
||||
use schema::comments;
|
||||
use safe_string::SafeString;
|
||||
use schema::comments;
|
||||
use users::User;
|
||||
use Connection;
|
||||
|
||||
#[derive(Queryable, Identifiable, Serialize, Clone)]
|
||||
pub struct Comment {
|
||||
|
@ -31,7 +27,7 @@ pub struct Comment {
|
|||
pub creation_date: NaiveDateTime,
|
||||
pub ap_url: Option<String>,
|
||||
pub sensitive: bool,
|
||||
pub spoiler_text: String
|
||||
pub spoiler_text: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable, Default)]
|
||||
|
@ -43,7 +39,7 @@ pub struct NewComment {
|
|||
pub author_id: i32,
|
||||
pub ap_url: Option<String>,
|
||||
pub sensitive: bool,
|
||||
pub spoiler_text: String
|
||||
pub spoiler_text: String,
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
|
@ -62,24 +58,35 @@ impl Comment {
|
|||
|
||||
pub fn count_local(conn: &Connection) -> usize {
|
||||
use schema::users;
|
||||
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id);
|
||||
comments::table.filter(comments::author_id.eq_any(local_authors))
|
||||
let local_authors = users::table
|
||||
.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)
|
||||
.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 {
|
||||
let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error");
|
||||
json["author"] = self.get_author(conn).to_json(conn);
|
||||
let mentions = Mention::list_for_comment(conn, self.id).into_iter()
|
||||
.map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new()))
|
||||
let mentions = Mention::list_for_comment(conn, self.id)
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
m.get_mentioned(conn)
|
||||
.map(|u| u.get_fqn(conn))
|
||||
.unwrap_or(String::new())
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error");
|
||||
json["responses"] = json!(others.into_iter()
|
||||
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
|
||||
.map(|c| c.to_json(conn, others))
|
||||
.collect::<Vec<_>>());
|
||||
json["responses"] = json!(
|
||||
others
|
||||
.into_iter()
|
||||
.filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false))
|
||||
.map(|c| c.to_json(conn, others))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
json
|
||||
}
|
||||
|
||||
|
@ -106,61 +113,138 @@ impl Comment {
|
|||
let mut note = Note::default();
|
||||
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.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))
|
||||
}))).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");
|
||||
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>>())
|
||||
note.object_props
|
||||
.set_id_string(self.ap_url.clone().unwrap_or(String::new()))
|
||||
.expect("Comment::into_activity: id error");
|
||||
note.object_props
|
||||
.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))
|
||||
},
|
||||
)))
|
||||
.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");
|
||||
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");
|
||||
note
|
||||
}
|
||||
|
||||
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 mut act = Create::default();
|
||||
act.create_props.set_actor_link(author.into_id()).expect("Comment::create_activity: actor error");
|
||||
act.create_props.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.create_props
|
||||
.set_actor_link(author.into_id())
|
||||
.expect("Comment::create_activity: actor error");
|
||||
act.create_props
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
impl FromActivity<Note, Connection> for 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 comm = Comment::insert(conn, 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(),
|
||||
in_response_to_id: previous_comment.clone().map(|c| c.id),
|
||||
post_id: previous_comment
|
||||
.map(|c| c.post_id)
|
||||
.unwrap_or_else(|| Post::find_by_ap_url(conn, previous_url).expect("Comment::from_activity: post error").id),
|
||||
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
|
||||
});
|
||||
let comm = Comment::insert(
|
||||
conn,
|
||||
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(),
|
||||
in_response_to_id: previous_comment.clone().map(|c| c.id),
|
||||
post_id: previous_comment.map(|c| c.post_id).unwrap_or_else(|| {
|
||||
Post::find_by_ap_url(conn, previous_url)
|
||||
.expect("Comment::from_activity: post error")
|
||||
.id
|
||||
}),
|
||||
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
|
||||
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
|
||||
for tag in tags.into_iter() {
|
||||
serde_json::from_value::<link::Mention>(tag)
|
||||
.map(|m| {
|
||||
let author = &Post::get(conn, comm.post_id).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();
|
||||
let author = &Post::get(conn, comm.post_id)
|
||||
.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)
|
||||
}).ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,11 +256,14 @@ impl FromActivity<Note, Connection> for Comment {
|
|||
impl Notify<Connection> for Comment {
|
||||
fn notify(&self, conn: &Connection) {
|
||||
for author in self.get_post(conn).get_authors(conn) {
|
||||
Notification::insert(conn, NewNotification {
|
||||
kind: notification_kind::COMMENT.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id
|
||||
});
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::COMMENT.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use diesel::{
|
||||
r2d2::{ConnectionManager, Pool, PooledConnection}
|
||||
use diesel::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 Connection;
|
||||
|
@ -23,7 +25,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
|
|||
let pool = request.guard::<State<DbPool>>()?;
|
||||
match pool.get() {
|
||||
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 plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer};
|
||||
use {BASE_URL, ap_url, Connection};
|
||||
use blogs::Blog;
|
||||
use notifications::*;
|
||||
use users::User;
|
||||
use plume_common::activity_pub::{
|
||||
broadcast,
|
||||
inbox::{Deletable, FromActivity, Notify, WithInbox},
|
||||
sign::Signer,
|
||||
Id, IntoId,
|
||||
};
|
||||
use schema::follows;
|
||||
use users::User;
|
||||
use {ap_url, Connection, BASE_URL};
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable, Associations)]
|
||||
#[belongs_to(User, foreign_key = "following_id")]
|
||||
|
@ -31,22 +40,35 @@ impl Follow {
|
|||
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))
|
||||
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 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.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
|
||||
}
|
||||
|
||||
|
@ -58,23 +80,41 @@ impl Follow {
|
|||
target: &A,
|
||||
follow: FollowAct,
|
||||
from_id: i32,
|
||||
target_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 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();
|
||||
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");
|
||||
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");
|
||||
broadcast(&*target, accept, vec![from.clone()]);
|
||||
res
|
||||
}
|
||||
|
@ -82,14 +122,41 @@ impl Follow {
|
|||
|
||||
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()) {
|
||||
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");
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -98,27 +165,44 @@ impl FromActivity<FollowAct, Connection> for Follow {
|
|||
|
||||
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
|
||||
});
|
||||
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");
|
||||
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");
|
||||
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.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
|
||||
}
|
||||
|
||||
|
|
235
plume-models/src/follows.rs.orig
Normal file
235
plume-models/src/follows.rs.orig
Normal file
|
@ -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::{http::{Header, HeaderMap}, Outcome};
|
||||
|
||||
use rocket::{
|
||||
http::{Header, HeaderMap},
|
||||
Outcome,
|
||||
};
|
||||
|
||||
pub struct Headers<'r>(pub HeaderMap<'r>);
|
||||
|
||||
|
@ -18,10 +20,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> {
|
|||
} else {
|
||||
ori.path().to_owned()
|
||||
};
|
||||
headers.add(Header::new("(request-target)",
|
||||
format!("{} {}",
|
||||
request.method().as_str().to_lowercase(),
|
||||
uri)));
|
||||
headers.add(Header::new(
|
||||
"(request-target)",
|
||||
format!("{} {}", request.method().as_str().to_lowercase(), uri),
|
||||
));
|
||||
Outcome::Success(Headers(headers))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use std::iter::Iterator;
|
||||
|
||||
use plume_common::utils::md_to_html;
|
||||
use Connection;
|
||||
use safe_string::SafeString;
|
||||
use ap_url;
|
||||
use users::User;
|
||||
use plume_common::utils::md_to_html;
|
||||
use safe_string::SafeString;
|
||||
use schema::{instances, users};
|
||||
use users::User;
|
||||
use Connection;
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable, Serialize)]
|
||||
pub struct Instance {
|
||||
|
@ -20,12 +20,12 @@ pub struct Instance {
|
|||
pub open_registrations: bool,
|
||||
pub short_description: SafeString,
|
||||
pub long_description: SafeString,
|
||||
pub default_license : String,
|
||||
pub default_license: String,
|
||||
pub long_description_html: String,
|
||||
pub short_description_html: String
|
||||
pub short_description_html: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[derive(Clone, Insertable)]
|
||||
#[table_name = "instances"]
|
||||
pub struct NewInstance {
|
||||
pub public_domain: String,
|
||||
|
@ -34,28 +34,32 @@ pub struct NewInstance {
|
|||
pub open_registrations: bool,
|
||||
pub short_description: SafeString,
|
||||
pub long_description: SafeString,
|
||||
pub default_license : String,
|
||||
pub default_license: String,
|
||||
pub long_description_html: String,
|
||||
pub short_description_html: String
|
||||
pub short_description_html: String,
|
||||
}
|
||||
|
||||
impl 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)
|
||||
.load::<Instance>(conn)
|
||||
.expect("Instance::get_local: loading error")
|
||||
.into_iter().nth(0)
|
||||
.into_iter()
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
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)
|
||||
.expect("Instance::get_remotes: loading error")
|
||||
}
|
||||
|
||||
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())
|
||||
.limit((max - min).into())
|
||||
.load::<Instance>(conn)
|
||||
|
@ -63,7 +67,9 @@ impl Instance {
|
|||
}
|
||||
|
||||
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);
|
||||
|
@ -79,10 +85,12 @@ impl Instance {
|
|||
|
||||
/// id: AP object id
|
||||
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)
|
||||
.expect("Instance::is_blocked: loading error") {
|
||||
if id.starts_with(format!("https://{}", block.public_domain).as_str()) {
|
||||
.expect("Instance::is_blocked: loading error")
|
||||
{
|
||||
if id.starts_with(format!("https://{}/", block.public_domain).as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +99,8 @@ impl Instance {
|
|||
}
|
||||
|
||||
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))
|
||||
.load::<User>(conn)
|
||||
.expect("Instance::has_admin: loading error")
|
||||
|
@ -99,14 +108,20 @@ impl Instance {
|
|||
}
|
||||
|
||||
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))
|
||||
.limit(1)
|
||||
.get_result::<User>(conn)
|
||||
.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!(
|
||||
"{instance}/{prefix}/{name}/{box_name}",
|
||||
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 (ld, _, _) = md_to_html(long_description.as_ref());
|
||||
diesel::update(self)
|
||||
|
@ -126,12 +148,258 @@ impl Instance {
|
|||
instances::short_description.eq(short_description),
|
||||
instances::long_description.eq(long_description),
|
||||
instances::short_description_html.eq(sd),
|
||||
instances::long_description_html.eq(ld)
|
||||
)).execute(conn)
|
||||
instances::long_description_html.eq(ld),
|
||||
))
|
||||
.execute(conn)
|
||||
.expect("Instance::update: update error");
|
||||
}
|
||||
|
||||
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 webfinger;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
use std::env;
|
||||
|
||||
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
||||
|
@ -99,11 +103,13 @@ macro_rules! list_by {
|
|||
macro_rules! get {
|
||||
($table:ident) => {
|
||||
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)
|
||||
.load::<Self>(conn)
|
||||
.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 {
|
||||
($table:ident) => {
|
||||
pub fn last(conn: &crate::Connection) -> Self {
|
||||
$table::table.order_by($table::id.desc())
|
||||
$table::table
|
||||
.order_by($table::id.desc())
|
||||
.limit(1)
|
||||
.load::<Self>(conn)
|
||||
.expect(concat!("macro::last: Error getting last ", stringify!($table)))
|
||||
.iter().next()
|
||||
.expect(concat!(
|
||||
"macro::last: Error getting last ",
|
||||
stringify!($table)
|
||||
))
|
||||
.iter()
|
||||
.next()
|
||||
.expect(concat!("macro::last: No last ", stringify!($table)))
|
||||
.clone()
|
||||
}
|
||||
|
@ -189,31 +200,67 @@ macro_rules! last {
|
|||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref BASE_URL: String = env::var("BASE_URL")
|
||||
.unwrap_or(format!("127.0.0.1:{}", env::var("ROCKET_PORT").unwrap_or(String::from("8000"))));
|
||||
|
||||
pub static ref BASE_URL: String = env::var("BASE_URL").unwrap_or(format!(
|
||||
"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);
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
static DB_NAME: &str = "plume";
|
||||
#[cfg(test)]
|
||||
static DB_NAME: &str = "plume_tests";
|
||||
|
||||
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
||||
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")))]
|
||||
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 {
|
||||
let scheme = if *USE_HTTPS {
|
||||
"https"
|
||||
} else {
|
||||
"http"
|
||||
};
|
||||
let scheme = if *USE_HTTPS { "https" } else { "http" };
|
||||
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 api_tokens;
|
||||
pub mod apps;
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
use activitypub::activity;
|
||||
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 plume_common::activity_pub::{
|
||||
inbox::{Deletable, FromActivity, Notify},
|
||||
Id, IntoId, PUBLIC_VISIBILTY,
|
||||
};
|
||||
use posts::Post;
|
||||
use users::User;
|
||||
use schema::likes;
|
||||
use users::User;
|
||||
use Connection;
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable)]
|
||||
pub struct Like {
|
||||
|
@ -20,7 +18,7 @@ pub struct Like {
|
|||
pub user_id: i32,
|
||||
pub post_id: i32,
|
||||
pub creation_date: NaiveDateTime,
|
||||
pub ap_url: String
|
||||
pub ap_url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Insertable)]
|
||||
|
@ -28,7 +26,7 @@ pub struct Like {
|
|||
pub struct NewLike {
|
||||
pub user_id: i32,
|
||||
pub post_id: i32,
|
||||
pub ap_url: String
|
||||
pub ap_url: String,
|
||||
}
|
||||
|
||||
impl Like {
|
||||
|
@ -45,17 +43,36 @@ impl Like {
|
|||
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
|
||||
)))
|
||||
.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 {
|
||||
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.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.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
|
||||
.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
|
||||
}
|
||||
|
@ -63,13 +80,30 @@ impl Like {
|
|||
|
||||
impl FromActivity<activity::Like, Connection> for 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 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,
|
||||
user_id: liker.expect("Like::from_activity: user error").id,
|
||||
ap_url: like.object_props.id_string().unwrap_or(String::from(""))
|
||||
});
|
||||
let liker = User::from_url(
|
||||
conn,
|
||||
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,
|
||||
user_id: liker.expect("Like::from_activity: user error").id,
|
||||
ap_url: like.object_props.id_string().unwrap_or(String::from("")),
|
||||
},
|
||||
);
|
||||
res.notify(conn);
|
||||
res
|
||||
}
|
||||
|
@ -79,30 +113,51 @@ impl Notify<Connection> for Like {
|
|||
fn notify(&self, conn: &Connection) {
|
||||
let post = Post::get(conn, self.post_id).expect("Like::notify: post error");
|
||||
for author in post.get_authors(conn) {
|
||||
Notification::insert(conn, NewNotification {
|
||||
kind: notification_kind::LIKE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id
|
||||
});
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::LIKE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deletable<Connection, activity::Undo> for Like {
|
||||
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
|
||||
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();
|
||||
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.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.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
|
||||
.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
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use activitypub::object::Image;
|
||||
use diesel::{self, QueryDsl, ExpressionMethods, RunQueryDsl};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use guid_create::GUID;
|
||||
use reqwest;
|
||||
use serde_json;
|
||||
|
@ -7,10 +7,10 @@ use std::{fs, path::Path};
|
|||
|
||||
use plume_common::activity_pub::Id;
|
||||
|
||||
use {ap_url, Connection};
|
||||
use instance::Instance;
|
||||
use users::User;
|
||||
use schema::medias;
|
||||
use users::User;
|
||||
use {ap_url, Connection};
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable, Serialize)]
|
||||
pub struct Media {
|
||||
|
@ -21,7 +21,7 @@ pub struct Media {
|
|||
pub remote_url: Option<String>,
|
||||
pub sensitive: bool,
|
||||
pub content_warning: Option<String>,
|
||||
pub owner_id: i32
|
||||
pub owner_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
|
@ -33,7 +33,7 @@ pub struct NewMedia {
|
|||
pub remote_url: Option<String>,
|
||||
pub sensitive: bool,
|
||||
pub content_warning: Option<String>,
|
||||
pub owner_id: i32
|
||||
pub owner_id: i32,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
|
@ -41,29 +41,64 @@ impl Media {
|
|||
get!(medias);
|
||||
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 {
|
||||
let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error");
|
||||
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" => (
|
||||
"image",
|
||||
format!("<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!(
|
||||
"<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),
|
||||
),
|
||||
"mp3" | "wav" | "flac" => (
|
||||
"audio",
|
||||
format!("<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>", url, self.alt_text),
|
||||
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text),
|
||||
format!("<audio src=\"{}\" title=\"{}\"></audio>", url, self.alt_text),
|
||||
format!(
|
||||
"<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>",
|
||||
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" => (
|
||||
"video",
|
||||
format!("<video src=\"{}\" title=\"{}\" class=\"preview\"></video>", url, self.alt_text),
|
||||
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text),
|
||||
format!("<video src=\"{}\" title=\"{}\"></video>", url, self.alt_text),
|
||||
format!(
|
||||
"<video src=\"{}\" title=\"{}\" class=\"preview\"></video>",
|
||||
url, self.alt_text
|
||||
),
|
||||
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())
|
||||
_ => ("unknown", String::new(), String::new(), String::new()),
|
||||
};
|
||||
json["html_preview"] = json!(preview);
|
||||
json["html"] = json!(html);
|
||||
|
@ -77,30 +112,43 @@ impl Media {
|
|||
if self.is_remote {
|
||||
self.remote_url.clone().unwrap_or(String::new())
|
||||
} 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) {
|
||||
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");
|
||||
if !self.is_remote {
|
||||
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");
|
||||
}
|
||||
|
||||
pub fn save_remote(conn: &Connection, url: String) -> Media {
|
||||
Media::insert(conn, NewMedia {
|
||||
file_path: String::new(),
|
||||
alt_text: String::new(),
|
||||
is_remote: true,
|
||||
remote_url: Some(url),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: 1 // It will be owned by the admin during an instant, but set_owner will be called just after
|
||||
})
|
||||
pub fn save_remote(conn: &Connection, url: String, user: &User) -> Media {
|
||||
Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
file_path: String::new(),
|
||||
alt_text: String::new(),
|
||||
is_remote: true,
|
||||
remote_url: Some(url),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
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)
|
||||
.set(medias::owner_id.eq(id))
|
||||
.set(medias::owner_id.eq(user.id))
|
||||
.execute(conn)
|
||||
.expect("Media::set_owner: owner update error");
|
||||
}
|
||||
|
@ -108,21 +156,199 @@ impl Media {
|
|||
// TODO: merge with save_remote?
|
||||
pub fn from_activity(conn: &Connection, image: Image) -> Option<Media> {
|
||||
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 path = Path::new("static").join("media").join(format!("{}.{}", GUID::rand().to_string(), ext));
|
||||
let ext = remote_url
|
||||
.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()?;
|
||||
reqwest::get(remote_url.as_str()).ok()?
|
||||
.copy_to(&mut dest).ok()?;
|
||||
reqwest::get(remote_url.as_str())
|
||||
.ok()?
|
||||
.copy_to(&mut dest)
|
||||
.ok()?;
|
||||
|
||||
Some(Media::insert(conn, NewMedia {
|
||||
file_path: path.to_str()?.to_string(),
|
||||
alt_text: image.object_props.content_string().ok()?,
|
||||
is_remote: true,
|
||||
remote_url: None,
|
||||
sensitive: image.object_props.summary_string().is_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
|
||||
}))
|
||||
Some(Media::insert(
|
||||
conn,
|
||||
NewMedia {
|
||||
file_path: path.to_str()?.to_string(),
|
||||
alt_text: image.object_props.content_string().ok()?,
|
||||
is_remote: true,
|
||||
remote_url: None,
|
||||
sensitive: image.object_props.summary_string().is_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,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[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 diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use plume_common::activity_pub::inbox::Notify;
|
||||
use Connection;
|
||||
use comments::Comment;
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::inbox::Notify;
|
||||
use posts::Post;
|
||||
use users::User;
|
||||
use schema::mentions;
|
||||
use users::User;
|
||||
use Connection;
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)]
|
||||
pub struct Mention {
|
||||
|
@ -15,7 +15,7 @@ pub struct Mention {
|
|||
pub mentioned_id: i32,
|
||||
pub post_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)]
|
||||
|
@ -24,7 +24,7 @@ pub struct NewMention {
|
|||
pub mentioned_id: i32,
|
||||
pub post_id: Option<i32>,
|
||||
pub comment_id: Option<i32>,
|
||||
pub ap_url: String
|
||||
pub ap_url: String,
|
||||
}
|
||||
|
||||
impl Mention {
|
||||
|
@ -50,38 +50,62 @@ impl Mention {
|
|||
pub fn get_user(&self, conn: &Connection) -> Option<User> {
|
||||
match self.get_post(conn) {
|
||||
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 {
|
||||
let user = User::find_by_fqn(conn, ment.clone());
|
||||
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.link_props.set_name_string(format!("@{}", ment)).expect("Mention::build_activity: name error:");
|
||||
mention
|
||||
.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
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> link::Mention {
|
||||
let user = self.get_mentioned(conn);
|
||||
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.link_props.set_name_string(user.map(|u| format!("@{}", u.get_fqn(conn))).unwrap_or(String::new())).expect("Mention::to_activity: mention error");
|
||||
mention
|
||||
.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
|
||||
}
|
||||
|
||||
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 mentioned = User::find_by_ap_url(conn, ap_url)?;
|
||||
|
||||
if in_post {
|
||||
Post::get(conn, inside.clone().into()).map(|post| {
|
||||
let res = Mention::insert(conn, NewMention {
|
||||
mentioned_id: mentioned.id,
|
||||
post_id: Some(post.id),
|
||||
comment_id: None,
|
||||
ap_url: ment.link_props.href_string().unwrap_or(String::new())
|
||||
});
|
||||
let res = Mention::insert(
|
||||
conn,
|
||||
NewMention {
|
||||
mentioned_id: mentioned.id,
|
||||
post_id: Some(post.id),
|
||||
comment_id: None,
|
||||
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
|
||||
},
|
||||
);
|
||||
if notify {
|
||||
res.notify(conn);
|
||||
}
|
||||
|
@ -89,12 +113,15 @@ impl Mention {
|
|||
})
|
||||
} else {
|
||||
Comment::get(conn, inside.into()).map(|comment| {
|
||||
let res = Mention::insert(conn, NewMention {
|
||||
mentioned_id: mentioned.id,
|
||||
post_id: None,
|
||||
comment_id: Some(comment.id),
|
||||
ap_url: ment.link_props.href_string().unwrap_or(String::new())
|
||||
});
|
||||
let res = Mention::insert(
|
||||
conn,
|
||||
NewMention {
|
||||
mentioned_id: mentioned.id,
|
||||
post_id: None,
|
||||
comment_id: Some(comment.id),
|
||||
ap_url: ment.link_props.href_string().unwrap_or(String::new()),
|
||||
},
|
||||
);
|
||||
if notify {
|
||||
res.notify(conn);
|
||||
}
|
||||
|
@ -106,18 +133,23 @@ impl Mention {
|
|||
pub fn delete(&self, conn: &Connection) {
|
||||
//find related notifications and delete them
|
||||
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 {
|
||||
fn notify(&self, conn: &Connection) {
|
||||
self.get_mentioned(conn).map(|m| {
|
||||
Notification::insert(conn, NewNotification {
|
||||
kind: notification_kind::MENTION.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: m.id
|
||||
});
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::MENTION.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: m.id,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use serde_json;
|
||||
|
||||
use Connection;
|
||||
use comments::Comment;
|
||||
use follows::Follow;
|
||||
use likes::Like;
|
||||
use mentions::Mention;
|
||||
use posts::Post;
|
||||
use reshares::Reshare;
|
||||
use users::User;
|
||||
use schema::notifications;
|
||||
use users::User;
|
||||
use Connection;
|
||||
|
||||
pub mod notification_kind {
|
||||
pub const COMMENT: &'static str = "COMMENT";
|
||||
|
@ -26,7 +26,7 @@ pub struct Notification {
|
|||
pub user_id: i32,
|
||||
pub creation_date: NaiveDateTime,
|
||||
pub kind: String,
|
||||
pub object_id: i32
|
||||
pub object_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
|
@ -34,7 +34,7 @@ pub struct Notification {
|
|||
pub struct NewNotification {
|
||||
pub user_id: i32,
|
||||
pub kind: String,
|
||||
pub object_id: i32
|
||||
pub object_id: i32,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
|
@ -42,14 +42,20 @@ impl Notification {
|
|||
get!(notifications);
|
||||
|
||||
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())
|
||||
.load::<Notification>(conn)
|
||||
.expect("Notification::find_for_user: notification loading error")
|
||||
}
|
||||
|
||||
pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Vec<Notification> {
|
||||
notifications::table.filter(notifications::user_id.eq(user.id))
|
||||
pub fn page_for_user(
|
||||
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())
|
||||
.offset(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> {
|
||||
notifications::table.filter(notifications::kind.eq(kind.into()))
|
||||
notifications::table
|
||||
.filter(notifications::kind.eq(kind.into()))
|
||||
.filter(notifications::object_id.eq(obj))
|
||||
.get_result::<Notification>(conn)
|
||||
.ok()
|
||||
|
@ -67,25 +74,23 @@ impl Notification {
|
|||
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
|
||||
let mut json = json!(self);
|
||||
json["object"] = json!(match self.kind.as_ref() {
|
||||
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment|
|
||||
json!({
|
||||
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| json!({
|
||||
"post": comment.get_post(conn).to_json(conn),
|
||||
"user": comment.get_author(conn).to_json(conn),
|
||||
"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!({
|
||||
"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!({
|
||||
"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))
|
||||
})
|
||||
),
|
||||
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention|
|
||||
}),
|
||||
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| {
|
||||
json!({
|
||||
"user": mention.get_user(conn).map(|u| u.to_json(conn)),
|
||||
"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))
|
||||
})
|
||||
})
|
||||
),
|
||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare|
|
||||
}),
|
||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| {
|
||||
json!({
|
||||
"post": reshare.get_post(conn).map(|p| p.to_json(conn)),
|
||||
"user": reshare.get_user(conn).map(|u| u.to_json(conn))
|
||||
})
|
||||
),
|
||||
_ => Some(json!({}))
|
||||
}),
|
||||
_ => Some(json!({})),
|
||||
});
|
||||
json
|
||||
}
|
||||
|
||||
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 users::User;
|
||||
use schema::post_authors;
|
||||
use users::User;
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable, Associations)]
|
||||
#[belongs_to(Post)]
|
||||
|
@ -10,14 +10,14 @@ use schema::post_authors;
|
|||
pub struct PostAuthor {
|
||||
pub id: i32,
|
||||
pub post_id: i32,
|
||||
pub author_id: i32
|
||||
pub author_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "post_authors"]
|
||||
pub struct NewPostAuthor {
|
||||
pub post_id: i32,
|
||||
pub author_id: i32
|
||||
pub author_id: i32,
|
||||
}
|
||||
|
||||
impl PostAuthor {
|
||||
|
|
|
@ -1,36 +1,35 @@
|
|||
use activitypub::{
|
||||
activity::{Create, Delete, Update},
|
||||
link,
|
||||
object::{Article, Image, Tombstone}
|
||||
object::{Article, Image, Tombstone},
|
||||
};
|
||||
use canapi::{Error, Provider};
|
||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods, BelongingToDsl};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use heck::{CamelCase, KebabCase};
|
||||
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 instance::Instance;
|
||||
use likes::Like;
|
||||
use medias::Media;
|
||||
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 reshares::Reshare;
|
||||
use safe_string::SafeString;
|
||||
use schema::posts;
|
||||
use std::collections::HashSet;
|
||||
use tags::Tag;
|
||||
use users::User;
|
||||
use schema::posts;
|
||||
use safe_string::SafeString;
|
||||
use std::collections::HashSet;
|
||||
use {ap_url, Connection, BASE_URL};
|
||||
|
||||
#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
|
@ -68,24 +67,32 @@ pub struct NewPost {
|
|||
impl<'a> Provider<(&'a Connection, Option<i32>)> for Post {
|
||||
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 !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 {
|
||||
id: Some(post.id),
|
||||
title: Some(post.title.clone()),
|
||||
subtitle: Some(post.subtitle.clone()),
|
||||
content: Some(post.content.get().clone())
|
||||
content: Some(post.content.get().clone()),
|
||||
})
|
||||
} else {
|
||||
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();
|
||||
if let Some(title) = filter.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.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 {
|
||||
id: Some(p.id),
|
||||
title: Some(p.title.clone()),
|
||||
subtitle: Some(p.subtitle.clone()),
|
||||
content: Some(p.content.get().clone())
|
||||
query
|
||||
.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 {
|
||||
id: Some(p.id),
|
||||
title: Some(p.title.clone()),
|
||||
subtitle: Some(p.subtitle.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!()
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
|
||||
|
@ -138,7 +158,8 @@ impl Post {
|
|||
use schema::tags;
|
||||
|
||||
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))
|
||||
.order(posts::creation_date.desc())
|
||||
.offset(min.into())
|
||||
|
@ -150,35 +171,45 @@ impl Post {
|
|||
pub fn count_for_tag(conn: &Connection, tag: String) -> i64 {
|
||||
use schema::tags;
|
||||
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))
|
||||
.count()
|
||||
.load(conn)
|
||||
.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 {
|
||||
use schema::post_authors;
|
||||
use schema::users;
|
||||
let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).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))
|
||||
let local_authors = users::table
|
||||
.filter(users::instance_id.eq(Instance::local_id(conn)))
|
||||
.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))
|
||||
.load::<Post>(conn)
|
||||
.expect("Post::count_local: loading error")
|
||||
.len()// TODO count in database?
|
||||
.len() // TODO count in database?
|
||||
}
|
||||
|
||||
pub fn count(conn: &Connection) -> i64 {
|
||||
posts::table.filter(posts::published.eq(true))
|
||||
posts::table
|
||||
.filter(posts::published.eq(true))
|
||||
.count()
|
||||
.get_result(conn)
|
||||
.expect("Post::count: counting error")
|
||||
}
|
||||
|
||||
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))
|
||||
.limit(limit)
|
||||
.load::<Post>(conn)
|
||||
|
@ -189,7 +220,8 @@ impl Post {
|
|||
use schema::post_authors;
|
||||
|
||||
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))
|
||||
.order(posts::creation_date.desc())
|
||||
.limit(limit)
|
||||
|
@ -198,7 +230,8 @@ impl 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))
|
||||
.order(posts::creation_date.desc())
|
||||
.limit(limit)
|
||||
|
@ -206,15 +239,17 @@ impl Post {
|
|||
.expect("Post::get_recents_for_blog: loading error")
|
||||
}
|
||||
|
||||
pub fn get_for_blog(conn: &Connection, blog:&Blog) -> Vec<Post> {
|
||||
posts::table.filter(posts::blog_id.eq(blog.id))
|
||||
pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Vec<Post> {
|
||||
posts::table
|
||||
.filter(posts::blog_id.eq(blog.id))
|
||||
.filter(posts::published.eq(true))
|
||||
.load::<Post>(conn)
|
||||
.expect("Post::get_for_blog:: loading error")
|
||||
}
|
||||
|
||||
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))
|
||||
.order(posts::creation_date.desc())
|
||||
.offset(min.into())
|
||||
|
@ -225,7 +260,8 @@ impl Post {
|
|||
|
||||
/// 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> {
|
||||
posts::table.order(posts::creation_date.desc())
|
||||
posts::table
|
||||
.order(posts::creation_date.desc())
|
||||
.filter(posts::published.eq(true))
|
||||
.offset(min.into())
|
||||
.limit((max - min).into())
|
||||
|
@ -234,12 +270,19 @@ impl Post {
|
|||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
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::blog_id.eq_any(blog_ids))
|
||||
.offset(min.into())
|
||||
|
@ -249,13 +292,18 @@ impl Post {
|
|||
}
|
||||
|
||||
/// 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;
|
||||
let post_ids = post_authors::table
|
||||
.filter(post_authors::author_id.eq_any(followed))
|
||||
.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::id.eq_any(post_ids))
|
||||
.offset(min.into())
|
||||
|
@ -268,7 +316,8 @@ impl Post {
|
|||
use schema::post_authors;
|
||||
|
||||
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::id.eq_any(posts))
|
||||
.load::<Post>(conn)
|
||||
|
@ -276,10 +325,13 @@ impl Post {
|
|||
}
|
||||
|
||||
pub fn get_authors(&self, conn: &Connection) -> Vec<User> {
|
||||
use schema::users;
|
||||
use schema::post_authors;
|
||||
use schema::users;
|
||||
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 {
|
||||
|
@ -293,23 +345,28 @@ impl Post {
|
|||
|
||||
pub fn get_blog(&self, conn: &Connection) -> Blog {
|
||||
use schema::blogs;
|
||||
blogs::table.filter(blogs::id.eq(self.blog_id))
|
||||
blogs::table
|
||||
.filter(blogs::id.eq(self.blog_id))
|
||||
.limit(1)
|
||||
.load::<Blog>(conn)
|
||||
.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> {
|
||||
use schema::likes;
|
||||
likes::table.filter(likes::post_id.eq(self.id))
|
||||
likes::table
|
||||
.filter(likes::post_id.eq(self.id))
|
||||
.load::<Like>(conn)
|
||||
.expect("Post::get_likes: loading error")
|
||||
}
|
||||
|
||||
pub fn get_reshares(&self, conn: &Connection) -> Vec<Reshare> {
|
||||
use schema::reshares;
|
||||
reshares::table.filter(reshares::post_id.eq(self.id))
|
||||
reshares::table
|
||||
.filter(reshares::post_id.eq(self.id))
|
||||
.load::<Reshare>(conn)
|
||||
.expect("Post::get_reshares: loading error")
|
||||
}
|
||||
|
@ -318,7 +375,8 @@ impl Post {
|
|||
if self.ap_url.len() == 0 {
|
||||
diesel::update(self)
|
||||
.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")
|
||||
} else {
|
||||
self.clone()
|
||||
|
@ -326,7 +384,11 @@ impl Post {
|
|||
}
|
||||
|
||||
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| {
|
||||
for x in f {
|
||||
acc.push(x.ap_url);
|
||||
|
@ -340,74 +402,170 @@ impl Post {
|
|||
let mut to = self.get_receivers_urls(conn);
|
||||
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 tags_json = Tag::for_post(conn, self.id).into_iter().map(|t| json!(t.into_activity(conn))).collect::<Vec<serde_json::Value>>();
|
||||
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 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);
|
||||
|
||||
let mut article = Article::default();
|
||||
article.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");
|
||||
article
|
||||
.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
|
||||
article.object_props.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(),
|
||||
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");
|
||||
article.object_props.set_summary_string(self.subtitle.clone()).expect("Post::into_activity: summary error");
|
||||
article
|
||||
.object_props
|
||||
.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(),
|
||||
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");
|
||||
article
|
||||
.object_props
|
||||
.set_summary_string(self.subtitle.clone())
|
||||
.expect("Post::into_activity: summary error");
|
||||
article.object_props.tag = Some(json!(mentions_json));
|
||||
|
||||
if let Some(media_id) = self.cover_id {
|
||||
let media = Media::get(conn, media_id).expect("Post::into_activity: get cover error");
|
||||
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 {
|
||||
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.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");
|
||||
cover
|
||||
.object_props
|
||||
.set_content_string(media.alt_text)
|
||||
.expect("Post::into_activity: icon.content 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.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
|
||||
.object_props
|
||||
.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
|
||||
}
|
||||
|
||||
pub fn create_activity(&self, conn: &Connection) -> Create {
|
||||
let article = self.into_activity(conn);
|
||||
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.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::create_activity: Couldn't copy 'to'"))
|
||||
act.object_props
|
||||
.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");
|
||||
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");
|
||||
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.set_object_object(article).expect("Post::create_activity: object 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
|
||||
.set_object_object(article)
|
||||
.expect("Post::create_activity: object error");
|
||||
act
|
||||
}
|
||||
|
||||
pub fn update_activity(&self, conn: &Connection) -> Update {
|
||||
let article = self.into_activity(conn);
|
||||
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.set_to_link_vec::<Id>(article.object_props.to_link_vec().expect("Post::update_activity: Couldn't copy 'to'"))
|
||||
act.object_props
|
||||
.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");
|
||||
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");
|
||||
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.set_object_object(article).expect("Article::update_activity: object 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
|
||||
.set_object_object(article)
|
||||
.expect("Article::update_activity: object error");
|
||||
act
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if let Ok(title) = updated.object_props.name_string() {
|
||||
|
@ -431,7 +589,11 @@ impl Post {
|
|||
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() {
|
||||
let mut mentions = vec![];
|
||||
let mut tags = vec![];
|
||||
|
@ -443,13 +605,16 @@ impl Post {
|
|||
|
||||
serde_json::from_value::<Hashtag>(tag.clone())
|
||||
.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) {
|
||||
hashtags.push(t);
|
||||
} else {
|
||||
tags.push(t);
|
||||
}
|
||||
}).ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
post.update_mentions(conn, mentions);
|
||||
post.update_tags(conn, tags);
|
||||
|
@ -460,34 +625,76 @@ impl Post {
|
|||
}
|
||||
|
||||
pub fn update_mentions(&self, conn: &Connection, mentions: Vec<link::Mention>) {
|
||||
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))
|
||||
.map(|u| u.id),m))
|
||||
.filter_map(|(id, m)| if let Some(id)=id {Some((m,id))} else {None}).collect::<Vec<_>>();
|
||||
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))
|
||||
.map(|u| u.id),
|
||||
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_user_mentioned = old_mentions.iter()
|
||||
.map(|m| m.mentioned_id).collect::<HashSet<_>>();
|
||||
for (m,id) in mentions.iter() {
|
||||
if !old_user_mentioned.contains(&id) {
|
||||
let old_user_mentioned = old_mentions
|
||||
.iter()
|
||||
.map(|m| m.mentioned_id)
|
||||
.collect::<HashSet<_>>();
|
||||
for (m, id) in mentions.iter() {
|
||||
if !old_user_mentioned.contains(&id) {
|
||||
Mention::from_activity(&*conn, m.clone(), self.id, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
let new_mentions = mentions.into_iter().map(|(_m,id)| id).collect::<HashSet<_>>();
|
||||
for m in old_mentions.iter().filter(|m| !new_mentions.contains(&m.mentioned_id)) {
|
||||
let new_mentions = mentions
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
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_name = old_tags.iter().filter_map(|tag| if !tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>();
|
||||
let old_tags = Tag::for_post(&*conn, self.id)
|
||||
.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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -500,13 +707,31 @@ impl Post {
|
|||
}
|
||||
|
||||
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_name = old_tags.iter().filter_map(|tag| if tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::<HashSet<_>>();
|
||||
let old_tags = Tag::for_post(&*conn, self.id)
|
||||
.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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -532,16 +757,26 @@ impl Post {
|
|||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
} 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")
|
||||
.into_iter()
|
||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||
|
@ -550,39 +785,78 @@ impl FromActivity<Article, Connection> for Post {
|
|||
Some(user) => {
|
||||
authors.push(user);
|
||||
(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));
|
||||
|
||||
let title = article.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,
|
||||
slug: title.to_kebab_case(),
|
||||
title: title,
|
||||
content: SafeString::new(&article.object_props.content_string().expect("Post::from_activity: content error")),
|
||||
published: true,
|
||||
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
|
||||
ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error")),
|
||||
creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc()),
|
||||
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,
|
||||
});
|
||||
let title = article
|
||||
.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,
|
||||
slug: title.to_kebab_case(),
|
||||
title: title,
|
||||
content: SafeString::new(
|
||||
&article
|
||||
.object_props
|
||||
.content_string()
|
||||
.expect("Post::from_activity: content error"),
|
||||
),
|
||||
published: true,
|
||||
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
|
||||
ap_url: article.object_props.url_string().unwrap_or(
|
||||
article
|
||||
.object_props
|
||||
.id_string()
|
||||
.expect("Post::from_activity: url + id error"),
|
||||
),
|
||||
creation_date: Some(
|
||||
article
|
||||
.object_props
|
||||
.published_utctime()
|
||||
.expect("Post::from_activity: published error")
|
||||
.naive_utc(),
|
||||
),
|
||||
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,
|
||||
},
|
||||
);
|
||||
|
||||
for author in authors.into_iter() {
|
||||
PostAuthor::insert(conn, NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id
|
||||
});
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
for tag in tags.into_iter() {
|
||||
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())
|
||||
.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));
|
||||
})
|
||||
.ok();
|
||||
|
@ -605,28 +881,44 @@ impl FromActivity<Article, Connection> for Post {
|
|||
impl Deletable<Connection, Delete> for Post {
|
||||
fn delete(&self, conn: &Connection) -> Delete {
|
||||
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();
|
||||
tombstone.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");
|
||||
tombstone
|
||||
.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.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]).expect("Post::delete: to error");
|
||||
act.object_props
|
||||
.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) {
|
||||
m.delete(conn);
|
||||
}
|
||||
diesel::delete(self).execute(conn).expect("Post::delete: DB error");
|
||||
diesel::delete(self)
|
||||
.execute(conn)
|
||||
.expect("Post::delete: DB error");
|
||||
act
|
||||
}
|
||||
|
||||
fn delete_id(id: String, actor_id: String, conn: &Connection) {
|
||||
let actor = User::find_by_ap_url(conn, actor_id);
|
||||
let post = Post::find_by_ap_url(conn, id);
|
||||
let can_delete = actor.and_then(|act|
|
||||
post.clone().map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id))
|
||||
).unwrap_or(false);
|
||||
let can_delete = actor
|
||||
.and_then(|act| {
|
||||
post.clone()
|
||||
.map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if can_delete {
|
||||
post.map(|p| p.delete(conn));
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use activitypub::activity::{Announce, Undo};
|
||||
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 plume_common::activity_pub::{
|
||||
inbox::{Deletable, FromActivity, Notify},
|
||||
Id, IntoId, PUBLIC_VISIBILTY,
|
||||
};
|
||||
use posts::Post;
|
||||
use users::User;
|
||||
use schema::reshares;
|
||||
use users::User;
|
||||
use Connection;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)]
|
||||
pub struct Reshare {
|
||||
|
@ -15,7 +18,7 @@ pub struct Reshare {
|
|||
pub user_id: i32,
|
||||
pub post_id: i32,
|
||||
pub ap_url: String,
|
||||
pub creation_date: NaiveDateTime
|
||||
pub creation_date: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
|
@ -23,29 +26,40 @@ pub struct Reshare {
|
|||
pub struct NewReshare {
|
||||
pub user_id: i32,
|
||||
pub post_id: i32,
|
||||
pub ap_url: String
|
||||
pub ap_url: String,
|
||||
}
|
||||
|
||||
impl Reshare {
|
||||
insert!(reshares, NewReshare);
|
||||
get!(reshares);
|
||||
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) {
|
||||
if self.ap_url.len() == 0 {
|
||||
diesel::update(self)
|
||||
.set(reshares::ap_url.eq(format!(
|
||||
"{}/reshare/{}",
|
||||
User::get(conn, self.user_id).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");
|
||||
"{}/reshare/{}",
|
||||
User::get(conn, self.user_id)
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
.limit(limit)
|
||||
.load::<Reshare>(conn)
|
||||
|
@ -62,13 +76,29 @@ impl Reshare {
|
|||
|
||||
pub fn into_activity(&self, conn: &Connection) -> Announce {
|
||||
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");
|
||||
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");
|
||||
act.object_props.set_id_string(self.ap_url.clone()).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.object_props
|
||||
.set_id_string(self.ap_url.clone())
|
||||
.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
|
||||
}
|
||||
|
@ -76,13 +106,33 @@ impl Reshare {
|
|||
|
||||
impl FromActivity<Announce, Connection> for 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 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,
|
||||
user_id: user.expect("Reshare::from_activity: user error").id,
|
||||
ap_url: announce.object_props.id_string().unwrap_or(String::from(""))
|
||||
});
|
||||
let user = User::from_url(
|
||||
conn,
|
||||
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,
|
||||
user_id: user.expect("Reshare::from_activity: user error").id,
|
||||
ap_url: announce
|
||||
.object_props
|
||||
.id_string()
|
||||
.unwrap_or(String::from("")),
|
||||
},
|
||||
);
|
||||
reshare.notify(conn);
|
||||
reshare
|
||||
}
|
||||
|
@ -92,30 +142,51 @@ impl Notify<Connection> for Reshare {
|
|||
fn notify(&self, conn: &Connection) {
|
||||
let post = self.get_post(conn).expect("Reshare::notify: post error");
|
||||
for author in post.get_authors(conn) {
|
||||
Notification::insert(conn, NewNotification {
|
||||
kind: notification_kind::RESHARE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id
|
||||
});
|
||||
Notification::insert(
|
||||
conn,
|
||||
NewNotification {
|
||||
kind: notification_kind::RESHARE.to_string(),
|
||||
object_id: self.id,
|
||||
user_id: author.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deletable<Connection, Undo> for Reshare {
|
||||
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
|
||||
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();
|
||||
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.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.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
|
||||
.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
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
use ammonia::{Builder, UrlRelative};
|
||||
use serde::{self, Serialize, Deserialize,
|
||||
Serializer, Deserializer, de::Visitor};
|
||||
use std::{fmt::{self, Display},
|
||||
borrow::{Borrow, Cow}, io::Write,
|
||||
iter, ops::Deref};
|
||||
use diesel::{self, deserialize::Queryable,
|
||||
types::ToSql,
|
||||
use diesel::{
|
||||
self,
|
||||
deserialize::Queryable,
|
||||
serialize::{self, Output},
|
||||
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! {
|
||||
static ref CLEAN: Builder<'static> = {
|
||||
|
@ -16,17 +22,18 @@ lazy_static! {
|
|||
.add_tags(iter::once("iframe"))
|
||||
.id_prefix(Some("postcontent-"))
|
||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||
.add_tag_attributes("iframe",
|
||||
["width", "height", "src", "frameborder"]
|
||||
.iter()
|
||||
.map(|&v| v));
|
||||
.add_tag_attributes(
|
||||
"iframe",
|
||||
["width", "height", "src", "frameborder"].iter().map(|&v| v),
|
||||
);
|
||||
b
|
||||
};
|
||||
}
|
||||
|
||||
fn url_add_prefix(url: &str) -> Option<Cow<str>> {
|
||||
if url.starts_with('#') && ! url.starts_with("#postcontent-") {//if start with an #
|
||||
let mut new_url = "#postcontent-".to_owned();//change to valid id
|
||||
if url.starts_with('#') && !url.starts_with("#postcontent-") {
|
||||
//if start with an #
|
||||
let mut new_url = "#postcontent-".to_owned(); //change to valid id
|
||||
new_url.push_str(&url[1..]);
|
||||
Some(Cow::Owned(new_url))
|
||||
} 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"]
|
||||
pub struct SafeString{
|
||||
pub struct SafeString {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl SafeString{
|
||||
pub fn new(value: &str) -> Self {
|
||||
SafeString{
|
||||
impl SafeString {
|
||||
pub fn new(value: &str) -> Self {
|
||||
SafeString {
|
||||
value: CLEAN.clean(&value).to_string(),
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +63,9 @@ pub fn new(value: &str) -> Self {
|
|||
|
||||
impl Serialize for SafeString {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer, {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.value)
|
||||
}
|
||||
}
|
||||
|
@ -66,22 +75,24 @@ struct SafeStringVisitor;
|
|||
impl<'de> Visitor<'de> for SafeStringVisitor {
|
||||
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")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<SafeString, E>
|
||||
where E: serde::de::Error{
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(SafeString::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SafeString {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: Deserializer<'de>, {
|
||||
Ok(
|
||||
deserializer.deserialize_string(SafeStringVisitor)?
|
||||
)
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
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
|
||||
where
|
||||
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 {
|
||||
str::to_sql(&self.value, out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Borrow<str> for SafeString {
|
||||
fn borrow(&self) -> &str {
|
||||
&self.value
|
||||
|
@ -137,8 +147,8 @@ impl AsRef<str> for SafeString {
|
|||
}
|
||||
}
|
||||
|
||||
use rocket::request::FromFormValue;
|
||||
use rocket::http::RawStr;
|
||||
use rocket::request::FromFormValue;
|
||||
|
||||
impl<'v> FromFormValue<'v> for SafeString {
|
||||
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 plume_common::activity_pub::Hashtag;
|
||||
use schema::tags;
|
||||
use {ap_url, Connection};
|
||||
|
||||
#[derive(Clone, Identifiable, Serialize, Queryable)]
|
||||
pub struct Tag {
|
||||
pub id: i32,
|
||||
pub tag: String,
|
||||
pub is_hashtag: bool,
|
||||
pub post_id: i32
|
||||
pub post_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
|
@ -18,7 +18,7 @@ pub struct Tag {
|
|||
pub struct NewTag {
|
||||
pub tag: String,
|
||||
pub is_hashtag: bool,
|
||||
pub post_id: i32
|
||||
pub post_id: i32,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
|
@ -29,33 +29,46 @@ impl Tag {
|
|||
|
||||
pub fn into_activity(&self, conn: &Connection) -> Hashtag {
|
||||
let mut ht = Hashtag::default();
|
||||
ht.set_href_string(ap_url(format!("{}/tag/{}",
|
||||
Instance::get_local(conn).expect("Tag::into_activity: local instance not found 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.set_href_string(ap_url(format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local(conn)
|
||||
.expect("Tag::into_activity: local instance not found 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
|
||||
}
|
||||
|
||||
pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32, is_hashtag: bool) -> Tag {
|
||||
Tag::insert(conn, NewTag {
|
||||
tag: tag.name_string().expect("Tag::from_activity: name error"),
|
||||
is_hashtag,
|
||||
post_id: post
|
||||
})
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: tag.name_string().expect("Tag::from_activity: name error"),
|
||||
is_hashtag,
|
||||
post_id: post,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_activity(conn: &Connection, tag: String) -> Hashtag {
|
||||
let mut ht = Hashtag::default();
|
||||
ht.set_href_string(ap_url(format!("{}/tag/{}",
|
||||
Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain,
|
||||
tag)
|
||||
)).expect("Tag::into_activity: href error");
|
||||
ht.set_name_string(tag).expect("Tag::into_activity: name error");
|
||||
ht.set_href_string(ap_url(format!(
|
||||
"{}/tag/{}",
|
||||
Instance::get_local(conn)
|
||||
.expect("Tag::into_activity: local instance not found error")
|
||||
.public_domain,
|
||||
tag
|
||||
))).expect("Tag::into_activity: href error");
|
||||
ht.set_name_string(tag)
|
||||
.expect("Tag::into_activity: name error");
|
||||
ht
|
||||
}
|
||||
|
||||
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;
|
||||
#[macro_use] extern crate diesel_migrations;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
extern crate plume_models;
|
||||
|
||||
use diesel::Connection;
|
||||
use plume_models::{
|
||||
DATABASE_URL,
|
||||
Connection as Conn,
|
||||
instance::*,
|
||||
safe_string::SafeString,
|
||||
};
|
||||
use plume_models::{Connection as Conn, DATABASE_URL};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
embed_migrations!("../migrations/sqlite");
|
||||
|
@ -24,24 +20,7 @@ fn db() -> Conn {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn instance_insert() {
|
||||
fn empty_test() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
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(())
|
||||
});
|
||||
conn.test_transaction::<_, (), _>(|| Ok(()));
|
||||
}
|
||||
|
|
160
src/inbox.rs
160
src/inbox.rs
|
@ -13,76 +13,112 @@ use activitypub::{
|
|||
use failure::Error;
|
||||
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::{
|
||||
Connection,
|
||||
comments::Comment,
|
||||
follows::Follow,
|
||||
instance::Instance,
|
||||
likes,
|
||||
reshares::Reshare,
|
||||
posts::Post,
|
||||
users::User
|
||||
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
|
||||
users::User, Connection,
|
||||
};
|
||||
|
||||
pub trait Inbox {
|
||||
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() {
|
||||
Some(t) => {
|
||||
match t {
|
||||
"Announce" => {
|
||||
Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
||||
Ok(())
|
||||
},
|
||||
"Create" => {
|
||||
let act: Create = serde_json::from_value(act.clone())?;
|
||||
if Post::try_from_activity(conn, act.clone()) || Comment::try_from_activity(conn, act) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(InboxError::InvalidType)?
|
||||
}
|
||||
},
|
||||
"Delete" => {
|
||||
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);
|
||||
Ok(())
|
||||
},
|
||||
"Follow" => {
|
||||
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
||||
Ok(())
|
||||
},
|
||||
"Like" => {
|
||||
likes::Like::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
||||
Ok(())
|
||||
},
|
||||
"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" => {
|
||||
let act: Update = serde_json::from_value(act.clone())?;
|
||||
Post::handle_update(conn, act.update_props.object_object()?);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(InboxError::InvalidType)?
|
||||
Some(t) => match t {
|
||||
"Announce" => {
|
||||
Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
||||
Ok(())
|
||||
}
|
||||
"Create" => {
|
||||
let act: Create = serde_json::from_value(act.clone())?;
|
||||
if Post::try_from_activity(conn, act.clone())
|
||||
|| Comment::try_from_activity(conn, act)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(InboxError::InvalidType)?
|
||||
}
|
||||
}
|
||||
"Delete" => {
|
||||
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,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
"Follow" => {
|
||||
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id);
|
||||
Ok(())
|
||||
}
|
||||
"Like" => {
|
||||
likes::Like::from_activity(
|
||||
conn,
|
||||
serde_json::from_value(act.clone())?,
|
||||
actor_id,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
"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" => {
|
||||
let act: Update = serde_json::from_value(act.clone())?;
|
||||
Post::handle_update(conn, act.update_props.object_object()?);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(InboxError::InvalidType)?,
|
||||
},
|
||||
None => Err(InboxError::NoType)?
|
||||
None => Err(InboxError::NoType)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,27 @@
|
|||
use activitypub::{
|
||||
activity::Create,
|
||||
collection::OrderedCollection,
|
||||
object::Article
|
||||
};
|
||||
use activitypub::{activity::Create, collection::OrderedCollection, object::Article};
|
||||
use atom_syndication::{Entry, FeedBuilder};
|
||||
use rocket::{
|
||||
http::{ContentType, Cookies},
|
||||
request::LenientForm,
|
||||
response::{Content, Flash, Redirect, status},
|
||||
http::{ContentType, Cookies}
|
||||
response::{status, Content, Flash, Redirect},
|
||||
};
|
||||
use rocket_contrib::Template;
|
||||
use serde_json;
|
||||
use validator::{Validate, ValidationError};
|
||||
use workerpool::thunk::*;
|
||||
|
||||
use inbox::Inbox;
|
||||
use plume_common::activity_pub::{
|
||||
ActivityStream, broadcast, Id, IntoId, ApRequest,
|
||||
inbox::{FromActivity, Notify, Deletable},
|
||||
sign::{Signable, verify_http_headers}
|
||||
broadcast,
|
||||
inbox::{Deletable, FromActivity, Notify},
|
||||
sign::{verify_http_headers, Signable},
|
||||
ActivityStream, ApRequest, Id, IntoId,
|
||||
};
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
db_conn::DbConn,
|
||||
follows,
|
||||
headers::Headers,
|
||||
instance::Instance,
|
||||
posts::Post,
|
||||
reshares::Reshare,
|
||||
users::*
|
||||
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::Post,
|
||||
reshares::Reshare, users::*,
|
||||
};
|
||||
use inbox::Inbox;
|
||||
use routes::Page;
|
||||
use Worker;
|
||||
|
||||
|
@ -38,57 +29,84 @@ use Worker;
|
|||
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
match user {
|
||||
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)]
|
||||
fn details(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 reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
|
||||
let user_id = user.id.clone();
|
||||
let n_followers = user.get_followers(&*conn).len();
|
||||
fn details(
|
||||
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 reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
|
||||
let user_id = user.id.clone();
|
||||
let n_followers = user.get_followers(&*conn).len();
|
||||
|
||||
if !user.get_instance(&*conn).local {
|
||||
// Fetch new articles
|
||||
let user_clone = user.clone();
|
||||
worker.execute(Thunk::of(move || {
|
||||
for create_act in user_clone.fetch_outbox::<Create>() {
|
||||
match create_act.create_props.object_object::<Article>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(&*fecth_articles_conn, article, user_clone.clone().into_id());
|
||||
println!("Fetched article from remote user");
|
||||
}
|
||||
Err(e) => println!("Error while fetching articles in background: {:?}", e)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Fetch followers
|
||||
let user_clone = user.clone();
|
||||
worker.execute(Thunk::of(move || {
|
||||
for user_id in user_clone.fetch_followers_ids() {
|
||||
let follower = User::find_by_ap_url(&*fecth_followers_conn, user_id.clone())
|
||||
.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,
|
||||
following_id: user_clone.id,
|
||||
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Update profile information if needed
|
||||
let user_clone = user.clone();
|
||||
if user.needs_update() {
|
||||
if !user.get_instance(&*conn).local {
|
||||
// Fetch new articles
|
||||
let user_clone = user.clone();
|
||||
worker.execute(Thunk::of(move || {
|
||||
user_clone.refetch(&*update_conn);
|
||||
}))
|
||||
}
|
||||
}
|
||||
for create_act in user_clone.fetch_outbox::<Create>() {
|
||||
match create_act.create_props.object_object::<Article>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(
|
||||
&*fecth_articles_conn,
|
||||
article,
|
||||
user_clone.clone().into_id(),
|
||||
);
|
||||
println!("Fetched article from remote user");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error while fetching articles in background: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Template::render("users/details", json!({
|
||||
// Fetch followers
|
||||
let user_clone = user.clone();
|
||||
worker.execute(Thunk::of(move || {
|
||||
for user_id in user_clone.fetch_followers_ids() {
|
||||
let follower =
|
||||
User::find_by_ap_url(&*fecth_followers_conn, user_id.clone())
|
||||
.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,
|
||||
following_id: user_clone.id,
|
||||
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
|
||||
},
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
// Update profile information if needed
|
||||
let user_clone = user.clone();
|
||||
if user.needs_update() {
|
||||
worker.execute(Thunk::of(move || {
|
||||
user_clone.refetch(&*update_conn);
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
Template::render(
|
||||
"users/details",
|
||||
json!({
|
||||
"user": user.to_json(&*conn),
|
||||
"instance_url": user.get_instance(&*conn).public_domain,
|
||||
"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>>(),
|
||||
"is_self": account.map(|a| a.id == user_id).unwrap_or(false),
|
||||
"n_followers": n_followers
|
||||
}))
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/dashboard")]
|
||||
fn dashboard(user: User, conn: DbConn) -> Template {
|
||||
let blogs = Blog::find_for_author(&*conn, user.id);
|
||||
Template::render("users/dashboard", json!({
|
||||
let blogs = Blog::find_for_author(&*conn, &user);
|
||||
Template::render(
|
||||
"users/dashboard",
|
||||
json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"blogs": blogs,
|
||||
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/dashboard", rank = 2)]
|
||||
fn dashboard_auth() -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
"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())?;
|
||||
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||
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 {
|
||||
let f = follows::Follow::insert(&*conn, follows::NewFollow {
|
||||
follower_id: user.id,
|
||||
following_id: target.id,
|
||||
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
|
||||
});
|
||||
let f = follows::Follow::insert(
|
||||
&*conn,
|
||||
follows::NewFollow {
|
||||
follower_id: user.id,
|
||||
following_id: target.id,
|
||||
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
|
||||
},
|
||||
);
|
||||
f.notify(&*conn);
|
||||
|
||||
let 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> {
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to follow someone",
|
||||
uri!(follow: name = name).into()
|
||||
uri!(follow: name = name).into(),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/@/<name>/followers?<page>")]
|
||||
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| {
|
||||
let user_id = user.id.clone();
|
||||
let followers_count = user.get_followers(&*conn).len();
|
||||
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 followers_count = user.get_followers(&*conn).len();
|
||||
|
||||
Template::render("users/followers", json!({
|
||||
Template::render(
|
||||
"users/followers",
|
||||
json!({
|
||||
"user": user.to_json(&*conn),
|
||||
"instance_url": user.get_instance(&*conn).public_domain,
|
||||
"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,
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(followers_count as i32)
|
||||
}))
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
|
||||
#[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)?;
|
||||
Some(ActivityStream::new(user.into_activity(&*conn)))
|
||||
}
|
||||
|
||||
#[get("/users/new")]
|
||||
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),
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"errors": null,
|
||||
"form": null
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/@/<name>/edit")]
|
||||
fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
|
||||
if user.username == name && !name.contains("@") {
|
||||
Some(Template::render("users/edit", json!({
|
||||
Some(Template::render(
|
||||
"users/edit",
|
||||
json!({
|
||||
"account": user.to_json(&*conn)
|
||||
})))
|
||||
}),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -206,7 +251,7 @@ fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
|
|||
fn edit_auth(name: String) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
"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>")]
|
||||
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect {
|
||||
user.update(&*conn,
|
||||
data.get().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())
|
||||
user.update(
|
||||
&*conn,
|
||||
data.get()
|
||||
.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))
|
||||
}
|
||||
|
@ -233,7 +290,9 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
|
|||
if user.id == account.id {
|
||||
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)))
|
||||
} else {
|
||||
|
@ -242,16 +301,32 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies) -> Optio
|
|||
}
|
||||
|
||||
#[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 {
|
||||
#[validate(length(min = "1", message = "Username can't be empty"))]
|
||||
username: String,
|
||||
#[validate(email(message = "Invalid email"))]
|
||||
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,
|
||||
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
||||
password_confirmation: String
|
||||
#[validate(
|
||||
length(
|
||||
min = "8",
|
||||
message = "Password should be at least 8 characters long"
|
||||
)
|
||||
)]
|
||||
password_confirmation: String,
|
||||
}
|
||||
|
||||
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
||||
|
@ -264,29 +339,37 @@ fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
|||
|
||||
#[post("/users/new", data = "<data>")]
|
||||
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
|
||||
}
|
||||
|
||||
let form = data.get();
|
||||
form.validate()
|
||||
.map(|_| {
|
||||
NewUser::new_local(
|
||||
NewUser::new_local(
|
||||
&*conn,
|
||||
form.username.to_string(),
|
||||
form.username.to_string(),
|
||||
false,
|
||||
String::from(""),
|
||||
form.email.to_string(),
|
||||
User::hash_pass(form.password.to_string())
|
||||
User::hash_pass(form.password.to_string()),
|
||||
).update_boxes(&*conn);
|
||||
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),
|
||||
"errors": e.inner(),
|
||||
"form": form
|
||||
})))
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/@/<name>/outbox")]
|
||||
|
@ -296,18 +379,32 @@ fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection
|
|||
}
|
||||
|
||||
#[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 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 actor_id = activity["actor"].as_str()
|
||||
.or_else(|| activity["actor"]["id"].as_str()).ok_or(Some(status::BadRequest(Some("Missing actor id for activity"))))?;
|
||||
let actor_id = activity["actor"]
|
||||
.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");
|
||||
if !verify_http_headers(&actor, headers.0.clone(), data).is_secure() &&
|
||||
!act.clone().verify(&actor) {
|
||||
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
|
||||
if !verify_http_headers(&actor, headers.0.clone(), data).is_secure()
|
||||
&& !act.clone().verify(&actor)
|
||||
{
|
||||
println!(
|
||||
"Rejected invalid activity supposedly from {}, with headers {:?}",
|
||||
actor.username, headers.0
|
||||
);
|
||||
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")]
|
||||
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 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();
|
||||
coll.object_props.set_id_string(user.followers_endpoint).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");
|
||||
coll.object_props
|
||||
.set_id_string(user.followers_endpoint)
|
||||
.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))
|
||||
}
|
||||
|
||||
|
@ -340,12 +451,19 @@ fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
|||
let author = User::find_by_fqn(&*conn, name.clone())?;
|
||||
let feed = FeedBuilder::default()
|
||||
.title(author.display_name.clone())
|
||||
.id(Instance::get_local(&*conn).unwrap().compute_box("~", name, "atom.xml"))
|
||||
.entries(Post::get_recents_for_author(&*conn, &author, 15)
|
||||
.into_iter()
|
||||
.map(|p| super::post_to_atom(p, &*conn))
|
||||
.collect::<Vec<Entry>>())
|
||||
.id(Instance::get_local(&*conn)
|
||||
.unwrap()
|
||||
.compute_box("~", name, "atom.xml"))
|
||||
.entries(
|
||||
Post::get_recents_for_author(&*conn, &author, 15)
|
||||
.into_iter()
|
||||
.map(|p| super::post_to_atom(p, &*conn))
|
||||
.collect::<Vec<Entry>>(),
|
||||
)
|
||||
.build()
|
||||
.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 a new issue