Compare commits

..

1 Commits

Author SHA1 Message Date
Trinity Pointard 6ef8ace025 attempt to do non anonymous ldap connect 2021-02-21 15:34:44 +01:00
50 changed files with 1078 additions and 1805 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
BASE_URL=plu.me
# Log level for each crate
RUST_LOG=info
RUST_LOG=warn,html5ever=warn,hyper=warn,tantivy=warn
# The secret key for private cookies and CSRF protection
# You can generate one with `openssl rand -base64 32`
-6
View File
@@ -19,17 +19,11 @@
- Update Rust version to nightly-2021-01-15 (#878)
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
- Run searcher on actor system (#870)
- Use article title as its slug instead of capitalizing and inserting hyphens (#920)
### Fixed
- Percent-encode URI for remote_interact (#866, #857)
- Menu animation not opening on iOS (#876, #897)
- Make actors subscribe to channel once (#913)
- Upsert posts and media instead of trying to insert and fail (#912)
- Update post's ActivityPub id when published by update (#915)
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
- Prevent duplicated posts in 'all' timeline (#917)
## [[0.6.0]] - 2020-12-29
Generated
+744 -899
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -15,11 +15,12 @@ gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699f
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
guid-create = "0.1"
heck = "0.3.0"
lettre = "0.9.2"
lettre_email = "0.9.2"
num_cpus = "1.10"
rocket = "=0.4.6"
rocket_contrib = { version = "=0.4.5", features = ["json"] }
rocket = "0.4.6"
rocket_contrib = { version = "0.4.5", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0"
scheduled-thread-pool = "0.2.2"
+1 -1
View File
@@ -30,7 +30,7 @@ A lot of features are still missing, but what is already here should be quite st
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
rules and thematics, but they can all communicate with each other.
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
- **Collaborative writing**: invite other people to your blogs, and write articles together.
## Get involved
+1 -1
View File
@@ -98,7 +98,7 @@ main article {
}
blockquote {
border-inline-start: 5px solid $gray;
border-left: 5px solid $gray;
margin: 1em auto;
padding: 0em 2em;
}
@@ -1 +0,0 @@
DROP INDEX medias_index_file_path;
@@ -1 +0,0 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -1,2 +0,0 @@
ALTER TABLE instances DROP COLUMN private_key;
ALTER TABLE instances DROP COLUMN public_key;
@@ -1,2 +0,0 @@
ALTER TABLE instances ADD COLUMN private_key TEXT;
ALTER TABLE instances ADD COLUMN public_key TEXT;
@@ -1 +0,0 @@
DROP INDEX medias_index_file_path;
@@ -1 +0,0 @@
CREATE INDEX medias_index_file_path ON medias (file_path);
@@ -1,30 +0,0 @@
CREATE TABLE instances_old (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
public_domain VARCHAR NOT NULL,
name VARCHAR NOT NULL,
local BOOLEAN NOT NULL DEFAULT 'f',
blocked BOOLEAN NOT NULL DEFAULT 'f',
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
open_registrations BOOLEAN NOT NULL DEFAULT 't',
short_description TEXT NOT NULL DEFAULT '',
long_description TEXT NOT NULL DEFAULT '',
default_license TEXT NOT NULL DEFAULT 'CC-0',
long_description_html VARCHAR NOT NULL DEFAULT '',
short_description_html VARCHAR NOT NULL DEFAULT ''
);
INSERT INTO instances_old SELECT
id,
public_domain,
name,
local,
blocked,
creation_date,
open_registrations,
short_description,
long_description,
default_license,
long_description_html,
short_description_html
FROM instances;
DROP TABLE instances;
ALTER TABLE instances_old RENAME TO instances;
@@ -1,2 +0,0 @@
ALTER TABLE instances ADD COLUMN private_key TEXT;
ALTER TABLE instances ADD COLUMN public_key TEXT;
+16 -2
View File
@@ -1,6 +1,6 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::NewInstance, Connection};
use plume_models::{instance::*, safe_string::SafeString, Connection};
use std::env;
pub fn command<'a, 'b>() -> App<'a, 'b> {
@@ -53,5 +53,19 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
.unwrap_or_else(|| String::from("CC-BY-SA"));
let open_reg = !args.is_present("private");
NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
Instance::insert(
conn,
NewInstance {
public_domain: domain,
name,
local: true,
long_description: SafeString::new(""),
short_description: SafeString::new(""),
default_license: license,
open_registrations: open_reg,
short_description_html: String::new(),
long_description_html: String::new(),
},
)
.expect("Couldn't save instance");
}
+1 -1
View File
@@ -14,7 +14,7 @@ heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
openssl = "0.10.22"
rocket = "=0.4.6"
rocket = "0.4.6"
reqwest = { version = "0.9", features = ["socks"] }
serde = "1.0"
serde_derive = "1.0"
+29 -138
View File
@@ -1,11 +1,6 @@
use reqwest::header::{HeaderValue, ACCEPT};
use std::fmt::Debug;
use super::{request, sign::Signer};
use reqwest::{
header::{HeaderValue, HOST},
Url,
};
/// Represents an ActivityPub inbox.
///
/// It routes an incoming Activity through the registered handlers.
@@ -15,8 +10,7 @@ use reqwest::{
/// ```rust
/// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use plume_common::activity_pub::{inbox::*, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer}};
/// # use plume_common::activity_pub::inbox::*;
/// # struct User;
/// # impl FromId<()> for User {
/// # type Error = ();
@@ -65,42 +59,6 @@ use reqwest::{
/// # Ok(())
/// # }
/// # }
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// #
/// # impl MySigner {
/// # fn new() -> Self {
/// # let (pub_key, priv_key) = gen_keypair();
/// # Self {
/// # public_key: String::from_utf8(pub_key).unwrap(),
/// # private_key: String::from_utf8(priv_key).unwrap(),
/// # }
/// # }
/// # }
/// #
/// # impl Signer for MySigner {
/// # fn get_key_id(&self) -> String {
/// # "mysigner".into()
/// # }
/// #
/// # fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
/// # signer.update(to_sign.as_bytes()).unwrap();
/// # signer.sign_to_vec().map_err(|_| SignatureError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
/// # .unwrap();
/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
/// # verifier.update(data.as_bytes()).unwrap();
/// # verifier.verify(&signature).map_err(|_| SignatureError())
/// # }
/// # }
/// #
/// # let mut act = Create::default();
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap();
@@ -111,9 +69,8 @@ use reqwest::{
/// # let activity_json = serde_json::to_value(act).unwrap();
/// #
/// # let conn = ();
/// # let sender = MySigner::new();
/// #
/// let result: Result<(), ()> = Inbox::handle(&conn, &sender, activity_json)
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
/// .with::<User, Announce, Message>(None)
/// .with::<User, Create, Message>(None)
/// .done();
@@ -127,10 +84,9 @@ where
/// # Structure
///
/// - the context to be passed to each handler.
/// - the sender actor to sign request
/// - the activity
/// - the reason it has not been handled yet
NotHandled(&'a C, &'a dyn Signer, serde_json::Value, InboxError<E>),
NotHandled(&'a C, serde_json::Value, InboxError<E>),
/// A matching handler have been found but failed
///
@@ -183,12 +139,8 @@ where
///
/// - `ctx`: the context to pass to each handler
/// - `json`: the JSON representation of the incoming activity
pub fn handle(
ctx: &'a C,
sender: &'a dyn Signer,
json: serde_json::Value,
) -> Inbox<'a, C, E, R> {
Inbox::NotHandled(ctx, sender, json, InboxError::NoMatch)
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> {
Inbox::NotHandled(ctx, json, InboxError::NoMatch)
}
/// Registers an handler on this Inbox.
@@ -199,30 +151,27 @@ where
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
M::Output: Into<R>,
{
if let Inbox::NotHandled(ctx, sender, mut act, e) = self {
if let Inbox::NotHandled(ctx, mut act, e) = self {
if serde_json::from_value::<V>(act.clone()).is_ok() {
let act_clone = act.clone();
let act_id = match act_clone["id"].as_str() {
Some(x) => x,
None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID),
};
// Get the actor ID
let actor_id = match get_id(act["actor"].clone()) {
Some(x) => x,
None => {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidActor(None))
}
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
};
if Self::is_spoofed_activity(&actor_id, &act) {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None));
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None));
}
// Transform this actor to a model (see FromId for details about the from_id function)
let actor = match A::from_id(
ctx,
sender,
&actor_id,
serde_json::from_value(act["actor"].clone()).ok(),
proxy,
@@ -233,25 +182,17 @@ where
if let Some(json) = json {
act["actor"] = json;
}
return Inbox::NotHandled(
ctx,
sender,
act,
InboxError::InvalidActor(Some(e)),
);
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
}
};
// Same logic for "object"
let obj_id = match get_id(act["object"].clone()) {
Some(x) => x,
None => {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None))
}
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)),
};
let obj = match M::from_id(
ctx,
sender,
&obj_id,
serde_json::from_value(act["object"].clone()).ok(),
proxy,
@@ -261,12 +202,7 @@ where
if let Some(json) = json {
act["object"] = json;
}
return Inbox::NotHandled(
ctx,
sender,
act,
InboxError::InvalidObject(Some(e)),
);
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
}
};
@@ -278,7 +214,7 @@ where
} else {
// If the Activity type is not matching the expected one for
// this handler, try with the next one.
Inbox::NotHandled(ctx, sender, act, e)
Inbox::NotHandled(ctx, act, e)
}
} else {
self
@@ -289,7 +225,7 @@ where
pub fn done(self) -> Result<R, E> {
match self {
Inbox::Handled(res) => Ok(res),
Inbox::NotHandled(_, _, _, err) => Err(E::from(err)),
Inbox::NotHandled(_, _, err) => Err(E::from(err)),
Inbox::Failed(err) => Err(err),
}
}
@@ -356,7 +292,6 @@ pub trait FromId<C>: Sized {
/// If absent, the ID will be dereferenced.
fn from_id(
ctx: &C,
sender: &dyn Signer,
id: &str,
object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>,
@@ -365,7 +300,7 @@ pub trait FromId<C>: Sized {
Ok(x) => Ok(x),
_ => match object {
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id, sender, proxy.cloned())?)
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
.map_err(|e| (None, e)),
},
}
@@ -374,17 +309,8 @@ pub trait FromId<C>: Sized {
/// Dereferences an ID
fn deref(
id: &str,
sender: &dyn Signer,
proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
let mut headers = request::headers();
let url = Url::parse(&id).map_err(|_| (None, InboxError::InvalidID.into()))?;
if !url.has_host() {
return Err((None, InboxError::InvalidID.into()));
}
let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"))
.map_err(|_| (None, InboxError::DerefError.into()))?;
headers.insert(HOST, host_header_value);
if let Some(proxy) = proxy {
reqwest::ClientBuilder::new().proxy(proxy)
} else {
@@ -394,11 +320,15 @@ pub trait FromId<C>: Sized {
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers, ("get", url.path(), url.query()))
.map_err(|_| (None, InboxError::DerefError.into()))?,
ACCEPT,
HeaderValue::from_str(
&super::ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)
.map_err(|_| (None, InboxError::DerefError.into()))?,
)
.send()
.map_err(|_| (None, InboxError::DerefError))
@@ -528,10 +458,8 @@ where
#[cfg(test)]
mod tests {
use super::super::sign::{gen_keypair, Error as SignatureError, Result as SignatureResult};
use super::*;
use activitypub::{activity::*, actor::Person, object::Note};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
struct MyActor;
impl FromId<()> for MyActor {
@@ -629,47 +557,10 @@ mod tests {
act
}
struct MySigner {
public_key: String,
private_key: String,
}
impl MySigner {
fn new() -> Self {
let (pub_key, priv_key) = gen_keypair();
Self {
public_key: String::from_utf8(pub_key).unwrap(),
private_key: String::from_utf8(priv_key).unwrap(),
}
}
}
impl Signer for MySigner {
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| SignatureError())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignatureError())
}
}
#[test]
fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Create, MyObject>(None)
.done();
assert!(res.is_ok());
@@ -678,7 +569,7 @@ mod tests {
#[test]
fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Delete, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
@@ -691,7 +582,7 @@ mod tests {
fn test_inbox_failure() {
let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
let res: Result<(), ()> = Inbox::handle(&(), act)
.with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Like, MyObject>(None)
.done();
@@ -740,12 +631,12 @@ mod tests {
fn test_inbox_actor_failure() {
let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.done();
assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.done();
+10 -10
View File
@@ -118,8 +118,8 @@ type Path<'a> = &'a str;
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature(
signer: &dyn Signer,
pub fn signature<S: Signer>(
signer: &S,
headers: &HeaderMap,
request_target: RequestTarget,
) -> Result<HeaderValue, Error> {
@@ -166,10 +166,8 @@ pub fn signature(
#[cfg(test)]
mod tests {
use super::signature;
use crate::activity_pub::sign::{
gen_keypair, Error as SignatureError, Result as SignatureResult, Signer,
};
use super::{signature, Error};
use crate::activity_pub::sign::{gen_keypair, Signer};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap;
@@ -189,24 +187,26 @@ mod tests {
}
impl Signer for MySigner {
type Error = Error;
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| SignatureError())
signer.sign_to_vec().map_err(|_| Error())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignatureError())
verifier.verify(&signature).map_err(|_| Error())
}
}
+6 -11
View File
@@ -19,25 +19,20 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
#[derive(Debug)]
pub struct Error();
pub type Result<T> = std::result::Result<T, Error>;
impl From<openssl::error::ErrorStack> for Error {
fn from(_: openssl::error::ErrorStack) -> Self {
Self()
}
}
pub trait Signer {
type Error;
fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>;
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>;
/// Verify if the signature is valid
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
}
pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self>
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error>
where
T: Signer;
fn verify<T>(self, creator: &T) -> bool
@@ -51,7 +46,7 @@ pub trait Signable {
}
impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value> {
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> {
let creation_date = Utc::now().to_rfc3339();
let mut options = json!({
"type": "RsaSignature2017",
-67
View File
@@ -27,59 +27,6 @@ pub fn make_actor_id(name: &str) -> String {
.collect()
}
/**
* Percent-encode characters which are not allowed in IRI path segments.
*
* Intended to be used for generating Post ap_url.
*/
pub fn iri_percent_encode_seg(segment: &str) -> String {
segment.chars().map(iri_percent_encode_seg_char).collect()
}
pub fn iri_percent_encode_seg_char(c: char) -> String {
if c.is_alphanumeric() {
c.to_string()
} else {
match c {
'-'
| '.'
| '_'
| '~'
| '\u{A0}'..='\u{D7FF}'
| '\u{20000}'..='\u{2FFFD}'
| '\u{30000}'..='\u{3FFFD}'
| '\u{40000}'..='\u{4FFFD}'
| '\u{50000}'..='\u{5FFFD}'
| '\u{60000}'..='\u{6FFFD}'
| '\u{70000}'..='\u{7FFFD}'
| '\u{80000}'..='\u{8FFFD}'
| '\u{90000}'..='\u{9FFFD}'
| '\u{A0000}'..='\u{AFFFD}'
| '\u{B0000}'..='\u{BFFFD}'
| '\u{C0000}'..='\u{CFFFD}'
| '\u{D0000}'..='\u{DFFFD}'
| '\u{E0000}'..='\u{EFFFD}'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| ':'
| '@' => c.to_string(),
_ => {
let s = c.to_string();
Uri::percent_encode(&s).to_string()
}
}
}
}
/**
* Redirects to the login page with a given message.
*
@@ -529,20 +476,6 @@ mod tests {
}
}
#[test]
fn test_iri_percent_encode_seg() {
assert_eq!(
&iri_percent_encode_seg("including whitespace"),
"including%20whitespace"
);
assert_eq!(&iri_percent_encode_seg("%20"), "%2520");
assert_eq!(&iri_percent_encode_seg("é"), "é");
assert_eq!(
&iri_percent_encode_seg("空白入り 日本語"),
"空白入り%20日本語"
);
}
#[test]
fn test_inline() {
assert_eq!(
+3 -2
View File
@@ -8,14 +8,15 @@ edition = "2018"
activitypub = "0.1.1"
ammonia = "2.1.1"
askama_escape = "0.1"
bcrypt = "0.10.1"
bcrypt = "0.5"
guid-create = "0.1"
heck = "0.3.0"
itertools = "0.8.0"
lazy_static = "1.0"
ldap3 = "0.7.1"
migrations_internals= "1.4.0"
openssl = "0.10.22"
rocket = "=0.4.6"
rocket = "0.4.6"
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9"
scheduled-thread-pool = "0.2.2"
+10 -34
View File
@@ -18,8 +18,7 @@ use openssl::{
};
use plume_common::activity_pub::{
inbox::{AsActor, FromId},
sign::{self, Error as SignatureError, Result as SignatureResult},
ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
};
use url::Url;
use webfinger::*;
@@ -150,16 +149,7 @@ impl Blog {
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)
.and_then(|l| {
Blog::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&l.href?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
})
.and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
}
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
@@ -369,8 +359,6 @@ impl FromId<DbConn> for Blog {
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
private_key: None,
public_key: None,
},
)
})?;
@@ -384,14 +372,7 @@ impl FromId<DbConn> for Blog {
Media::save_remote(
conn,
icon.object_props.url_string().ok()?,
&User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&owner,
None,
CONFIG.proxy(),
)
.ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
@@ -407,14 +388,7 @@ impl FromId<DbConn> for Blog {
Media::save_remote(
conn,
banner.object_props.url_string().ok()?,
&User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&owner,
None,
CONFIG.proxy(),
)
.ok()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
@@ -477,22 +451,24 @@ impl AsActor<&PlumeRocket> for Blog {
}
impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(SignatureError::from)
signer.sign_to_vec().map_err(Error::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(SignatureError::from)
verifier.verify(&signature).map_err(Error::from)
}
}
+4 -9
View File
@@ -236,7 +236,6 @@ impl FromId<DbConn> for Comment {
})?,
author_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&note.object_props.attributed_to_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -256,7 +255,9 @@ impl FromId<DbConn> for Comment {
.and_then(|m| {
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
let not_author = m.link_props.href_string()? != author.ap_url.clone();
Mention::from_activity(conn, &m, comm.id, false, not_author)
Ok(Mention::from_activity(
conn, &m, comm.id, false, not_author,
)?)
})
.ok();
}
@@ -295,13 +296,7 @@ impl FromId<DbConn> for Comment {
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
.into_iter()
.map(|v| {
if let Ok(user) = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&v,
None,
CONFIG.proxy(),
) {
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
vec![user]
} else {
vec![] // TODO try to fetch collection
+15 -3
View File
@@ -164,8 +164,11 @@ impl Default for LogoConfig {
};
let mut custom_icons = env::vars()
.filter_map(|(var, val)| {
var.strip_prefix("PLUME_LOGO_")
.map(|size| (size.to_owned(), val))
if let Some(size) = var.strip_prefix("PLUME_LOGO_") {
Some((size.to_owned(), val))
} else {
None
}
})
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
.map(|(dim, src)| Icon {
@@ -251,6 +254,7 @@ pub struct LdapConfig {
pub tls: bool,
pub user_name_attr: String,
pub mail_attr: String,
pub user: Option<(String, String)>,
}
fn get_ldap_config() -> Option<LdapConfig> {
@@ -266,16 +270,24 @@ fn get_ldap_config() -> Option<LdapConfig> {
};
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
let user = var("LDAP_USER").ok();
let password = var("LDAP_PASSWORD").ok();
let user = match (user, password) {
(Some(user), Some(password)) => Some((user, password)),
(None, None) => None,
_ => panic!("Invalid LDAP configuration both or neither of LDAP_USER and LDAP_PASSWORD must be set")
};
Some(LdapConfig {
addr,
base_dn,
tls,
user_name_attr,
mail_attr,
user
})
}
(None, None) => None,
(_, _) => {
_ => {
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
}
}
+2 -4
View File
@@ -1,6 +1,6 @@
use crate::{
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG,
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error,
Result, CONFIG,
};
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
@@ -168,7 +168,6 @@ impl FromId<DbConn> for Follow {
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&follow.follow_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -177,7 +176,6 @@ impl FromId<DbConn> for Follow {
let target = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&follow.follow_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
+15 -21
View File
@@ -3,9 +3,7 @@ use activitypub::activity::*;
use crate::{
comments::Comment,
db_conn::DbConn,
follows,
instance::Instance,
likes,
follows, likes,
posts::{Post, PostUpdate},
reshares::Reshare,
users::User,
@@ -49,24 +47,20 @@ impl_into_inbox_result! {
}
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(
conn,
&Instance::get_local().expect("Failed to get local instance"),
act,
)
.with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy())
.with::<User, Create, Post>(CONFIG.proxy())
.with::<User, Delete, Comment>(CONFIG.proxy())
.with::<User, Delete, Post>(CONFIG.proxy())
.with::<User, Delete, User>(CONFIG.proxy())
.with::<User, Follow, User>(CONFIG.proxy())
.with::<User, Like, Post>(CONFIG.proxy())
.with::<User, Undo, Reshare>(CONFIG.proxy())
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
.with::<User, Undo, likes::Like>(CONFIG.proxy())
.with::<User, Update, PostUpdate>(CONFIG.proxy())
.done()
Inbox::handle(conn, act)
.with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy())
.with::<User, Create, Post>(CONFIG.proxy())
.with::<User, Delete, Comment>(CONFIG.proxy())
.with::<User, Delete, Post>(CONFIG.proxy())
.with::<User, Delete, User>(CONFIG.proxy())
.with::<User, Follow, User>(CONFIG.proxy())
.with::<User, Like, Post>(CONFIG.proxy())
.with::<User, Undo, Reshare>(CONFIG.proxy())
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
.with::<User, Undo, likes::Like>(CONFIG.proxy())
.with::<User, Update, PostUpdate>(CONFIG.proxy())
.done()
}
#[cfg(test)]
+1 -142
View File
@@ -6,26 +6,10 @@ use crate::{
users::{Role, User},
Connection, Error, Result,
};
use activitypub::{actor::Service, CustomObject};
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use openssl::{
hash::MessageDigest,
pkey::{PKey, Private},
rsa::Rsa,
sign,
};
use plume_common::{
activity_pub::{
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
ApSignature, PublicKey,
},
utils::md_to_html,
};
use plume_common::utils::md_to_html;
use std::sync::RwLock;
use tracing::warn;
pub type CustomService = CustomObject<ApSignature, Service>;
#[derive(Clone, Identifiable, Queryable)]
pub struct Instance {
@@ -41,8 +25,6 @@ pub struct Instance {
pub default_license: String,
pub long_description_html: SafeString,
pub short_description_html: SafeString,
pub private_key: Option<String>,
pub public_key: Option<String>,
}
#[derive(Clone, Insertable)]
@@ -57,8 +39,6 @@ pub struct NewInstance {
pub default_license: String,
pub long_description_html: String,
pub short_description_html: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
}
lazy_static! {
@@ -89,13 +69,6 @@ impl Instance {
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok();
}
pub fn get_locals(conn: &Connection) -> Result<Vec<Instance>> {
instances::table
.filter(instances::local.eq(true))
.load::<Instance>(conn)
.map_err(Error::from)
}
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
instances::table
.filter(instances::local.eq(false))
@@ -265,112 +238,6 @@ impl Instance {
})
.map_err(Error::from)
}
pub fn set_keypair(&self, conn: &Connection) -> Result<()> {
let (pub_key, priv_key) = gen_keypair();
let private_key = String::from_utf8(priv_key).or(Err(Error::Signature))?;
let public_key = String::from_utf8(pub_key).or(Err(Error::Signature))?;
diesel::update(self)
.set((
instances::private_key.eq(Some(private_key)),
instances::public_key.eq(Some(public_key)),
))
.execute(conn)
.and(Ok(()))
.map_err(Error::from)
}
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
)?)
.map_err(Error::from)
}
/// This is experimental and might change in the future.
/// Currently "!" sign is used but it's not decided.
pub fn ap_url(&self) -> String {
ap_url(&format!(
"{}/!/{}",
Self::get_local().unwrap().public_domain,
self.public_domain
))
}
pub fn to_activity(&self) -> Result<CustomService> {
let mut actor = Service::default();
let id = self.ap_url();
actor.object_props.set_id_string(id.clone())?;
actor.object_props.set_name_string(self.name.clone())?;
let mut ap_signature = ApSignature::default();
if self.local {
if let Some(pub_key) = self.public_key.clone() {
let mut public_key = PublicKey::default();
public_key.set_id_string(format!("{}#main-key", id))?;
public_key.set_owner_string(id)?;
public_key.set_public_key_pem_string(pub_key)?;
ap_signature.set_public_key_publickey(public_key)?;
}
};
Ok(CustomService::new(actor, ap_signature))
}
}
impl NewInstance {
pub fn new_local(
conn: &Connection,
public_domain: String,
name: String,
open_registrations: bool,
default_license: String,
) -> Result<Instance> {
let (pub_key, priv_key) = gen_keypair();
Instance::insert(
conn,
NewInstance {
public_domain,
name,
local: true,
open_registrations,
short_description: SafeString::new(""),
long_description: SafeString::new(""),
default_license,
long_description_html: String::new(),
short_description_html: String::new(),
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
public_key: Some(String::from_utf8(pub_key).or(Err(Error::Signature))?),
},
)
}
}
impl Signer for Instance {
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url())
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(SignatureError::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
if self.public_key.is_none() {
warn!("missing public key for {}", self.public_domain);
return Err(SignatureError());
}
let key = PKey::from_rsa(Rsa::public_key_from_pem(
self.public_key.clone().unwrap().as_ref(),
)?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(SignatureError::from)
}
}
#[cfg(test)]
@@ -392,8 +259,6 @@ pub(crate) mod tests {
name: "My instance".to_string(),
open_registrations: true,
public_domain: "plu.me".to_string(),
private_key: None,
public_key: None,
},
NewInstance {
default_license: "WTFPL".to_string(),
@@ -405,8 +270,6 @@ pub(crate) mod tests {
name: "An instance".to_string(),
open_registrations: true,
public_domain: "1plu.me".to_string(),
private_key: None,
public_key: None,
},
NewInstance {
default_license: "CC-0".to_string(),
@@ -418,8 +281,6 @@ pub(crate) mod tests {
name: "Someone instance".to_string(),
open_registrations: false,
public_domain: "2plu.me".to_string(),
private_key: None,
public_key: None,
},
NewInstance {
default_license: "CC-0-BY-SA".to_string(),
@@ -431,8 +292,6 @@ pub(crate) mod tests {
name: "Nice day".to_string(),
open_registrations: true,
public_domain: "3plu.me".to_string(),
private_key: None,
public_key: None,
},
]
.into_iter()
+1 -26
View File
@@ -17,10 +17,8 @@ extern crate serde_json;
#[macro_use]
extern crate tantivy;
use db_conn::DbPool;
use instance::Instance;
use once_cell::sync::Lazy;
use plume_common::activity_pub::{inbox::InboxError, sign};
use plume_common::activity_pub::inbox::InboxError;
use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent;
@@ -82,12 +80,6 @@ impl From<openssl::error::ErrorStack> for Error {
}
}
impl From<sign::Error> for Error {
fn from(_: sign::Error) -> Self {
Error::Signature
}
}
impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
@@ -168,12 +160,6 @@ impl From<InboxError<Error>> for Error {
pub type Result<T> = std::result::Result<T, Error>;
impl From<Error> for sign::Error {
fn from(_: Error) -> Self {
Self()
}
}
/// Adds a function to a model, that returns the first
/// matching row for a given list of fields.
///
@@ -309,17 +295,6 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url)
}
pub fn migrate_data(dbpool: &DbPool) -> Result<()> {
ensure_local_instance_keys(&dbpool.get().unwrap())
}
fn ensure_local_instance_keys(conn: &Connection) -> Result<()> {
for instance in Instance::get_locals(conn)? {
instance.set_keypair(conn)?;
}
Ok(())
}
#[cfg(test)]
#[macro_use]
mod tests {
+2 -4
View File
@@ -1,6 +1,6 @@
use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG,
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
Connection, Error, Result, CONFIG,
};
use activitypub::activity;
use chrono::NaiveDateTime;
@@ -117,7 +117,6 @@ impl FromId<DbConn> for Like {
NewLike {
post_id: Post::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.like_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -126,7 +125,6 @@ impl FromId<DbConn> for Like {
.id,
user_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.like_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
+1 -2
View File
@@ -285,8 +285,7 @@ impl List {
.select(list_elems::word)
.load::<Option<String>>(conn)
.map_err(Error::from)
// .map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
.map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
}
pub fn clear(&self, conn: &Connection) -> Result<()> {
+30 -68
View File
@@ -12,14 +12,14 @@ use plume_common::{
};
use std::{
fs::{self, DirBuilder},
path::{self, Path, PathBuf},
path::{Path, PathBuf},
};
use tracing::warn;
use url::Url;
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
#[derive(Clone, Identifiable, Queryable)]
pub struct Media {
pub id: i32,
pub file_path: String,
@@ -65,7 +65,6 @@ impl MediaCategory {
impl Media {
insert!(medias, NewMedia);
get!(medias);
find_by!(medias, find_by_file_path, file_path as &str);
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
medias::table
@@ -156,15 +155,12 @@ impl Media {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
&CONFIG.media_directory,
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
let p = Path::new(&self.file_path);
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
Ok(ap_url(&format!(
"{}/{}",
"{}/static/media/{}",
Instance::get_local()?.public_domain,
&file_path
&filename
)))
}
}
@@ -228,66 +224,32 @@ impl Media {
.copy_to(&mut dest)
.ok()?;
Media::find_by_file_path(conn, &path.to_str()?)
.and_then(|mut media| {
let mut updated = false;
let alt_text = image.object_props.content_string().ok()?;
let sensitive = image.object_props.summary_string().is_ok();
let content_warning = image.object_props.summary_string().ok();
if media.alt_text != alt_text {
media.alt_text = alt_text;
updated = true;
}
if media.is_remote {
media.is_remote = false;
updated = true;
}
if media.remote_url.is_some() {
media.remote_url = None;
updated = true;
}
if media.sensitive != sensitive {
media.sensitive = sensitive;
updated = true;
}
if media.content_warning != content_warning {
media.content_warning = content_warning;
updated = true;
}
if updated {
diesel::update(&media).set(&media).execute(&**conn)?;
}
Ok(media)
})
.or_else(|_| {
Media::insert(
// TODO: upsert
Media::insert(
conn,
NewMedia {
file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?,
is_remote: false,
remote_url: None,
sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id(
conn,
NewMedia {
file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?,
is_remote: false,
remote_url: None,
sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?
.id,
},
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None,
CONFIG.proxy(),
)
})
.map_err(|(_, e)| e)?
.id,
},
)
}
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
+52 -126
View File
@@ -11,13 +11,14 @@ use activitypub::{
};
use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::KebabCase;
use once_cell::sync::Lazy;
use plume_common::{
activity_pub::{
inbox::{AsActor, AsObject, FromId},
inbox::{AsObject, FromId},
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
},
utils::{iri_percent_encode_seg, md_to_html},
utils::md_to_html,
};
use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet};
@@ -72,7 +73,12 @@ impl Post {
.execute(conn)?;
let mut post = Self::last(conn)?;
if post.ap_url.is_empty() {
post.ap_url = Self::ap_url(post.get_blog(conn)?, &post.slug);
post.ap_url = ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
post.get_blog(conn)?.fqn,
post.slug
));
let _: Post = post.save_changes(conn)?;
}
@@ -88,10 +94,7 @@ impl Post {
let post = Self::get(conn, self.id)?;
// TODO: Call publish_published() when newly published
if post.published {
let blog = post.get_blog(conn);
if blog.is_ok() && blog.unwrap().is_local() {
self.publish_updated();
}
self.publish_updated();
}
Ok(post)
}
@@ -248,20 +251,6 @@ impl Post {
.map_err(Error::from)
}
pub fn ap_url(blog: Blog, slug: &str) -> String {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
blog.fqn,
iri_percent_encode_seg(slug)
))
}
// It's better to calc slug in insert and update
pub fn slug(title: &str) -> &str {
title
}
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use crate::schema::post_authors;
use crate::schema::users;
@@ -454,7 +443,13 @@ impl Post {
m,
)
})
.filter_map(|(id, m)| id.map(|id| (m, id)))
.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)?;
@@ -630,28 +625,13 @@ impl FromId<DbConn> for Post {
.into_iter()
.fold((None, vec![]), |(blog, mut authors), link| {
let url = link;
match User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&url,
None,
CONFIG.proxy(),
) {
match User::from_id(conn, &url, None, CONFIG.proxy()) {
Ok(u) => {
authors.push(u);
(blog, authors)
}
Err(_) => (
blog.or_else(|| {
Blog::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&url,
None,
CONFIG.proxy(),
)
.ok()
}),
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
authors,
),
}
@@ -664,85 +644,37 @@ impl FromId<DbConn> for Post {
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?;
let ap_url = article
.object_props
.url_string()
.or_else(|_| article.object_props.id_string())?;
let post = Post::from_db(conn, &ap_url)
.and_then(|mut post| {
let mut updated = false;
// TODO: upsert
let post = Post::insert(
conn,
NewPost {
blog_id: blog?.id,
slug: title.to_kebab_case(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// 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()
.or_else(|_| article.object_props.id_string())?,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
)?;
let slug = Self::slug(&title);
let content = SafeString::new(&article.object_props.content_string()?);
let subtitle = article.object_props.summary_string()?;
let source = article.ap_object_props.source_object::<Source>()?.content;
if post.slug != slug {
post.slug = slug.to_string();
updated = true;
}
if post.title != title {
post.title = title.clone();
updated = true;
}
if post.content != content {
post.content = content;
updated = true;
}
if post.license != license {
post.license = license.clone();
updated = true;
}
if post.subtitle != subtitle {
post.subtitle = subtitle;
updated = true;
}
if post.source != source {
post.source = source;
updated = true;
}
if post.cover_id != cover {
post.cover_id = cover;
updated = true;
}
if updated {
post.update(conn)?;
}
Ok(post)
})
.or_else(|_| {
Post::insert(
conn,
NewPost {
blog_id: blog?.id,
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url,
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
subtitle: article.object_props.summary_string()?,
source: article.ap_object_props.source_object::<Source>()?.content,
cover_id: cover,
},
)
.and_then(|post| {
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
Ok(post)
})
})?;
for author in authors {
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: author.id,
},
)?;
}
// save mentions and tags
let mut hashtags = md_to_html(&post.source, None, false, None)
@@ -852,14 +784,8 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let mut post = Post::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&self.ap_url,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)?;
let mut post =
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime
@@ -867,7 +793,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
}
if let Some(title) = self.title {
post.slug = Post::slug(&title).to_string();
post.slug = title.to_kebab_case();
post.title = title;
}
+12 -17
View File
@@ -1,7 +1,6 @@
use crate::{
db_conn::{DbConn, DbPool},
follows,
instance::Instance,
posts::{LicensedArticle, Post},
users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN,
@@ -18,23 +17,25 @@ pub struct RemoteFetchActor {
impl RemoteFetchActor {
pub fn init(conn: DbPool) {
let actor = ACTOR_SYS
ACTOR_SYS
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
.expect("Failed to initialize remote fetch actor");
USER_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for RemoteFetchActor {
type Msg = UserEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
USER_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
topic: "*".into(),
},
None,
)
}
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use UserEvent::*;
@@ -90,13 +91,7 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
match follower_ids {
Ok(user_ids) => {
for user_id in user_ids {
let follower = User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&user_id,
None,
CONFIG.proxy(),
);
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
match follower {
Ok(follower) => {
let inserted = follows::Follow::insert(
+2 -4
View File
@@ -1,6 +1,6 @@
use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG,
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
Connection, Error, Result, CONFIG,
};
use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime;
@@ -142,7 +142,6 @@ impl FromId<DbConn> for Reshare {
NewReshare {
post_id: Post::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.announce_props.object_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -151,7 +150,6 @@ impl FromId<DbConn> for Reshare {
.id,
user_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.announce_props.actor_link::<Id>()?,
None,
CONFIG.proxy(),
+1 -1
View File
@@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for SafeString {
where
D: Deserializer<'de>,
{
deserializer.deserialize_string(SafeStringVisitor)
Ok(deserializer.deserialize_string(SafeStringVisitor)?)
}
}
-2
View File
@@ -106,8 +106,6 @@ table! {
default_license -> Text,
long_description_html -> Varchar,
short_description_html -> Varchar,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
}
}
+11 -11
View File
@@ -13,23 +13,25 @@ pub struct SearchActor {
impl SearchActor {
pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
let actor = ACTOR_SYS
ACTOR_SYS
.actor_of_args::<SearchActor, _>("search", (searcher, conn))
.expect("Failed to initialize searcher actor");
POST_CHAN.tell(
Subscribe {
actor: Box::new(actor),
topic: "*".into(),
},
None,
)
}
}
impl Actor for SearchActor {
type Msg = PostEvent;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {
POST_CHAN.tell(
Subscribe {
actor: Box::new(ctx.myself()),
topic: "*".into(),
},
None,
)
}
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
use PostEvent::*;
@@ -164,8 +166,6 @@ mod tests {
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex().to_string(),
private_key: None,
public_key: None,
},
)
.unwrap();
-13
View File
@@ -223,9 +223,6 @@ impl Timeline {
}
pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> {
if self.includes_post(conn, post)? {
return Ok(());
}
diesel::insert_into(timeline::table)
.values(TimelineEntry {
post_id: post.id,
@@ -239,16 +236,6 @@ impl Timeline {
let query = TimelineQuery::parse(&self.query)?;
query.matches(conn, self, post, kind)
}
fn includes_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
diesel::dsl::select(diesel::dsl::exists(
timeline::table
.filter(timeline::timeline_id.eq(self.id))
.filter(timeline::post_id.eq(post.id)),
))
.get_result(conn)
.map_err(Error::from)
}
}
#[cfg(test)]
+3 -2
View File
@@ -601,10 +601,11 @@ fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Lis
}
fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> {
let mut res: Vec<&str> = vec![match stream.get(0)? {
let mut res: Vec<&str> = Vec::new();
res.push(match stream.get(0)? {
Token::Word(_, _, w) => w,
t => return t.get_error(Token::Word(0, 0, "any word")),
}];
});
stream = &stream[1..];
while let Token::Comma(_) = stream[0] {
res.push(match stream.get(1)? {
+33 -16
View File
@@ -24,7 +24,7 @@ use plume_common::{
activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId},
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
sign::{gen_keypair, Signer},
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
},
utils,
@@ -210,14 +210,7 @@ impl User {
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?;
User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
link.href.as_ref()?,
None,
CONFIG.proxy(),
)
.map_err(|(_, e)| e)
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
}
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@@ -300,6 +293,21 @@ impl User {
bcrypt::hash(pass, 10).map_err(Error::from)
}
fn ldap_preconn(ldap_conn: &mut LdapConn) -> Result<()> {
let ldap = CONFIG.ldap.as_ref().unwrap();
if let Some((user, password)) = ldap.user.as_ref() {
let bind = ldap_conn
.simple_bind(user, password)
.map_err(|_| Error::NotFound)?;
if bind.success().is_err() {
return Err(Error::NotFound);
}
}
Ok(())
}
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
if CONFIG.ldap.is_none() {
return Err(Error::NotFound);
@@ -307,6 +315,9 @@ impl User {
let ldap = CONFIG.ldap.as_ref().unwrap();
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
User::ldap_preconn(&mut ldap_conn)?;
let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
let bind = ldap_conn
.simple_bind(&ldap_name, password)
@@ -353,6 +364,9 @@ impl User {
} else {
return false;
};
if User::ldap_preconn(&mut conn).is_err() {
return false;
}
let name = format!(
"{}={},{}",
ldap.user_name_attr, &self.username, ldap.base_dn
@@ -493,7 +507,10 @@ impl User {
.filter_map(|j| serde_json::from_value(j.clone()).ok())
.collect::<Vec<T>>();
let next = json.get("next").map(|x| x.as_str().unwrap().to_owned());
let next = match json.get("next") {
Some(x) => Some(x.as_str().unwrap().to_owned()),
None => None,
};
Ok((items, next))
}
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
@@ -965,8 +982,6 @@ impl FromId<DbConn> for User {
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
private_key: None,
public_key: None,
},
)
})?;
@@ -1072,22 +1087,24 @@ impl AsObject<User, Delete, &DbConn> for User {
}
impl Signer for User {
type Error = Error;
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url)
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(SignatureError::from)
signer.sign_to_vec().map_err(Error::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(SignatureError::from)
verifier.verify(&signature).map_err(Error::from)
}
}
+52 -52
View File
@@ -60,83 +60,83 @@ msgstr ""
msgid "Optional"
msgstr ""
# src/routes/blogs.rs:67
# src/routes/blogs.rs:63
msgid "To create a new blog, you need to be logged in"
msgstr ""
# src/routes/blogs.rs:109
# src/routes/blogs.rs:102
msgid "A blog with the same name already exists."
msgstr ""
# src/routes/blogs.rs:147
# src/routes/blogs.rs:140
msgid "Your blog was successfully created!"
msgstr ""
# src/routes/blogs.rs:165
# src/routes/blogs.rs:159
msgid "Your blog was deleted."
msgstr ""
# src/routes/blogs.rs:173
# src/routes/blogs.rs:167
msgid "You are not allowed to delete this blog."
msgstr ""
# src/routes/blogs.rs:223
# src/routes/blogs.rs:218
msgid "You are not allowed to edit this blog."
msgstr ""
# src/routes/blogs.rs:279
# src/routes/blogs.rs:274
msgid "You can't use this media as a blog icon."
msgstr ""
# src/routes/blogs.rs:297
# src/routes/blogs.rs:292
msgid "You can't use this media as a blog banner."
msgstr ""
# src/routes/blogs.rs:331
# src/routes/blogs.rs:326
msgid "Your blog information have been updated."
msgstr ""
# src/routes/comments.rs:100
# src/routes/comments.rs:99
msgid "Your comment has been posted."
msgstr ""
# src/routes/comments.rs:177
# src/routes/comments.rs:178
msgid "Your comment has been deleted."
msgstr ""
# src/routes/instance.rs:147
# src/routes/instance.rs:118
msgid "Instance settings have been saved."
msgstr ""
# src/routes/instance.rs:180
# src/routes/instance.rs:150
msgid "{} has been unblocked."
msgstr ""
# src/routes/instance.rs:182
# src/routes/instance.rs:152
msgid "{} has been blocked."
msgstr ""
# src/routes/instance.rs:233
# src/routes/instance.rs:201
msgid "Blocks deleted"
msgstr ""
# src/routes/instance.rs:249
# src/routes/instance.rs:216
msgid "Email already blocked"
msgstr ""
# src/routes/instance.rs:254
# src/routes/instance.rs:221
msgid "Email Blocked"
msgstr ""
# src/routes/instance.rs:347
# src/routes/instance.rs:312
msgid "You can't change your own rights."
msgstr ""
# src/routes/instance.rs:358
# src/routes/instance.rs:323
msgid "You are not allowed to take this action."
msgstr ""
# src/routes/instance.rs:393
# src/routes/instance.rs:359
msgid "Done."
msgstr ""
@@ -144,23 +144,23 @@ msgstr ""
msgid "To like a post, you need to be logged in"
msgstr ""
# src/routes/medias.rs:158
# src/routes/medias.rs:145
msgid "Your media have been deleted."
msgstr ""
# src/routes/medias.rs:163
# src/routes/medias.rs:150
msgid "You are not allowed to delete this media."
msgstr ""
# src/routes/medias.rs:180
# src/routes/medias.rs:167
msgid "Your avatar has been updated."
msgstr ""
# src/routes/medias.rs:185
# src/routes/medias.rs:172
msgid "You are not allowed to use this media."
msgstr ""
# src/routes/notifications.rs:29
# src/routes/notifications.rs:28
msgid "To see your notifications, you need to be logged in"
msgstr ""
@@ -168,51 +168,51 @@ msgstr ""
msgid "This post isn't published yet."
msgstr ""
# src/routes/posts.rs:125
# src/routes/posts.rs:126
msgid "To write a new post, you need to be logged in"
msgstr ""
# src/routes/posts.rs:146
# src/routes/posts.rs:143
msgid "You are not an author of this blog."
msgstr ""
# src/routes/posts.rs:153
# src/routes/posts.rs:150
msgid "New post"
msgstr ""
# src/routes/posts.rs:198
# src/routes/posts.rs:195
msgid "Edit {0}"
msgstr ""
# src/routes/posts.rs:267
# src/routes/posts.rs:264
msgid "You are not allowed to publish on this blog."
msgstr ""
# src/routes/posts.rs:367
# src/routes/posts.rs:363
msgid "Your article has been updated."
msgstr ""
# src/routes/posts.rs:556
# src/routes/posts.rs:553
msgid "Your article has been saved."
msgstr ""
# src/routes/posts.rs:563
# src/routes/posts.rs:560
msgid "New article"
msgstr ""
# src/routes/posts.rs:601
# src/routes/posts.rs:597
msgid "You are not allowed to delete this article."
msgstr ""
# src/routes/posts.rs:625
# src/routes/posts.rs:622
msgid "Your article has been deleted."
msgstr ""
# src/routes/posts.rs:630
# src/routes/posts.rs:627
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
msgstr ""
# src/routes/posts.rs:672
# src/routes/posts.rs:667
msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
msgstr ""
@@ -220,63 +220,63 @@ msgstr ""
msgid "To reshare a post, you need to be logged in"
msgstr ""
# src/routes/session.rs:95
# src/routes/session.rs:88
msgid "You are now connected."
msgstr ""
# src/routes/session.rs:116
# src/routes/session.rs:109
msgid "You are now logged off."
msgstr ""
# src/routes/session.rs:162
# src/routes/session.rs:154
msgid "Password reset"
msgstr ""
# src/routes/session.rs:163
# src/routes/session.rs:155
msgid "Here is the link to reset your password: {0}"
msgstr ""
# src/routes/session.rs:235
# src/routes/session.rs:215
msgid "Your password was successfully reset."
msgstr ""
# src/routes/user.rs:74
# src/routes/user.rs:142
msgid "To access your dashboard, you need to be logged in"
msgstr ""
# src/routes/user.rs:96
# src/routes/user.rs:164
msgid "You are no longer following {}."
msgstr ""
# src/routes/user.rs:113
# src/routes/user.rs:181
msgid "You are now following {}."
msgstr ""
# src/routes/user.rs:190
# src/routes/user.rs:261
msgid "To subscribe to someone, you need to be logged in"
msgstr ""
# src/routes/user.rs:299
# src/routes/user.rs:365
msgid "To edit your profile, you need to be logged in"
msgstr ""
# src/routes/user.rs:345
# src/routes/user.rs:411
msgid "Your profile has been updated."
msgstr ""
# src/routes/user.rs:373
# src/routes/user.rs:438
msgid "Your account has been deleted."
msgstr ""
# src/routes/user.rs:379
# src/routes/user.rs:444
msgid "You can't delete someone else's account."
msgstr ""
# src/routes/user.rs:463
# src/routes/user.rs:528
msgid "Registrations are closed on this instance."
msgstr ""
# src/routes/user.rs:486
# src/routes/user.rs:551
msgid "Your account has been created. Now you just need to log in, before you can use it."
msgstr ""
+1 -1
View File
@@ -1 +1 @@
nightly-2021-03-25
nightly-2021-01-15
+1
View File
@@ -1,5 +1,6 @@
#!/bin/bash
mkdir bin
cp target/release/{plume,plm} bin
strip -s bin/*
tar -cvzf plume.tar.gz bin/ static/
tar -cvzf wasm.tar.gz static/plume_front{.js,_bg.wasm}
+1 -1
View File
@@ -25,7 +25,7 @@ parts:
plume:
plugin: rust
source: .
rust-revision: nightly-2021-03-25
rust-revision: nightly-2020-01-15
build-packages:
- libssl-dev
- pkg-config
+2 -1
View File
@@ -1,4 +1,5 @@
use chrono::NaiveDateTime;
use heck::KebabCase;
use rocket_contrib::json::Json;
use crate::api::{authorization::*, Api};
@@ -108,7 +109,7 @@ pub fn create(
let author = User::get(&conn, auth.0.user_id)?;
let slug = Post::slug(&payload.title);
let slug = &payload.title.clone().to_kebab_case();
let date = payload.creation_date.clone().and_then(|d| {
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok()
});
+2 -8
View File
@@ -26,14 +26,8 @@ pub fn handle_incoming(
.or_else(|| activity["actor"]["id"].as_str())
.ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
let actor = User::from_id(
&conn,
&Instance::get_local().expect("Failed to get local instance"),
actor_id,
None,
CONFIG.proxy(),
)
.expect("instance::shared_inbox: user error");
let actor = User::from_id(&conn, actor_id, None, CONFIG.proxy())
.expect("instance::shared_inbox: user error");
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
// maybe we just know an old key?
actor
-3
View File
@@ -15,7 +15,6 @@ use diesel::r2d2::ConnectionManager;
use plume_models::{
db_conn::{DbPool, PragmaForeignKey},
instance::Instance,
migrate_data,
migrations::IMPORTED_MIGRATIONS,
remote_fetch_actor::RemoteFetchActor,
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
@@ -100,7 +99,6 @@ Then try to restart Plume.
"#
)
}
migrate_data(&dbpool).expect("Failed to migrate data");
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
// we want a fast exit here, so
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
@@ -149,7 +147,6 @@ Then try to restart Plume.
routes::comments::delete,
routes::comments::activity_pub,
routes::instance::index,
routes::instance::activity_details,
routes::instance::admin,
routes::instance::admin_mod,
routes::instance::admin_instances,
+2 -4
View File
@@ -348,7 +348,7 @@ pub fn update(
#[get("/~/<name>/outbox")]
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
blog.outbox(&conn).ok()
Some(blog.outbox(&conn).ok()?)
}
#[allow(unused_variables)]
#[get("/~/<name>/outbox?<page>")]
@@ -358,7 +358,7 @@ pub fn outbox_page(
conn: DbConn,
) -> Option<ActivityStream<OrderedCollectionPage>> {
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
blog.outbox_page(&conn, page.limits()).ok()
Some(blog.outbox_page(&conn, page.limits()).ok()?)
}
#[get("/~/<name>/atom.xml")]
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
@@ -438,8 +438,6 @@ mod tests {
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex().to_string(),
private_key: None,
public_key: None,
},
)
.unwrap();
+3 -45
View File
@@ -11,7 +11,7 @@ use validator::{Validate, ValidationErrors};
use crate::inbox;
use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{broadcast, inbox::FromId, ActivityStream, ApRequest};
use plume_common::activity_pub::{broadcast, inbox::FromId};
use plume_models::{
admin::*,
blocklisted_emails::*,
@@ -49,36 +49,6 @@ pub fn index(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
)))
}
/// Experimental
/// "!" might be change in the future
///
/// This exists for communication with Mastodon.
/// Secure mode Mastodon requires HTTP signature for event GET requests.
/// To sign requests, public key of request sender is required. We decided
/// the sender is local instance.
#[get("/!/<public_domain>")]
pub fn activity_details(
public_domain: String,
conn: DbConn,
_ap: ApRequest,
) -> Option<ActivityStream<CustomService>> {
if let Ok(instance) = Instance::find_by_domain(&conn, &public_domain) {
if !instance.local {
return None;
}
match instance.to_activity() {
Ok(activity) => Some(ActivityStream::new(activity)),
Err(plume_models::Error::NotFound) => None,
Err(err) => {
tracing::error!("{:?}", err);
panic!();
}
}
} else {
None
}
}
#[get("/admin")]
pub fn admin(_admin: Admin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let local_inst = Instance::get_local()?;
@@ -434,13 +404,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
return Some(Redirect::to(uri!(super::user::details: name = target)));
}
if let Ok(post) = Post::from_id(
&conn,
&Instance::get_local().expect("Failed to get local instance"),
&target,
None,
CONFIG.proxy(),
) {
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
return Some(Redirect::to(uri!(
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
slug = &post.slug,
@@ -448,13 +412,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
)));
}
if let Ok(comment) = Comment::from_id(
&conn,
&Instance::get_local().expect("Failed to get local instance"),
&target,
None,
CONFIG.proxy(),
) {
if let Ok(comment) = Comment::from_id(&conn, &target, None, CONFIG.proxy()) {
if comment.can_see(&conn, user.as_ref()) {
let post = comment.get_post(&conn).expect("Can't retrieve post");
return Some(Redirect::to(uri!(
+4 -4
View File
@@ -1,4 +1,5 @@
use chrono::Utc;
use heck::KebabCase;
use rocket::http::uri::Uri;
use rocket::request::LenientForm;
use rocket::response::{Flash, Redirect};
@@ -235,7 +236,7 @@ pub fn update(
let intl = &rockets.intl.catalog;
let new_slug = if !post.published {
Post::slug(&form.title).to_string()
form.title.to_string().to_kebab_case()
} else {
post.slug.clone()
};
@@ -289,7 +290,6 @@ pub fn update(
let newly_published = if !post.published && !form.draft {
post.published = true;
post.creation_date = Utc::now().naive_utc();
post.ap_url = Post::ap_url(post.get_blog(&conn).unwrap(), &new_slug);
true
} else {
false
@@ -399,7 +399,7 @@ pub struct NewPostForm {
}
pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
let slug = Post::slug(title);
let slug = title.to_string().to_kebab_case();
if slug.is_empty() {
Err(ValidationError::new("empty_slug"))
} else if slug == "new" {
@@ -418,7 +418,7 @@ pub fn create(
rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> {
let blog = Blog::find_by_fqn(&conn, &blog_name).expect("post::create: blog error");
let slug = Post::slug(&form.title);
let slug = form.title.to_string().to_kebab_case();
let user = rockets.user.clone().unwrap();
let mut errors = match form.validate() {