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