Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fe705a712 | |||
| 3a448e9e17 | |||
| 6e4def4cc5 | |||
| 34b8fd83c1 | |||
| d5774078e0 | |||
| a3623412f9 | |||
| 1ed60537cf | |||
| 037d670fb7 | |||
| 8b817d50c5 | |||
| fa48060a94 | |||
| b41e982daf | |||
| 2fcb449ed8 | |||
| 218bc54a5f | |||
| 64f0333497 | |||
| 8aa7a5780d | |||
| 17c398bcee | |||
| 76f1455372 | |||
| 1bcad6d7cd | |||
| 9a58d9bcb7 | |||
| c2dcac4413 | |||
| 394273e866 | |||
| 1bcc70c174 | |||
| 188d4ac063 | |||
| 0fbefe2cdc | |||
| eedd5fe4e9 | |||
| 27a1a56223 | |||
| 8e3322776a | |||
| 3d4336c548 | |||
| 0c8c607aa4 | |||
| 60d6734fe2 | |||
| 8c372aa6fc | |||
| d1a74ca8e6 | |||
| c374e0af4c | |||
| 58e8569048 | |||
| 83dbf2a945 | |||
| d7d6d5f644 | |||
| 9aa9885a89 | |||
| 7fe1e083e0 | |||
| a946823554 | |||
| 3a1872c03e | |||
| 5424f9110f | |||
| a597816617 | |||
| fa7a44f6bb | |||
| dfcdcc1833 | |||
| 6345a57498 | |||
| 87457c0ed1 | |||
| 859a1fd528 | |||
| 5069aab584 | |||
| 033fde38fe | |||
| 7b2bab0f9d | |||
| a419ef5319 | |||
| f77dce9f12 | |||
| 4c2cd92f0d | |||
| dd932e1f15 | |||
| 4376810d96 | |||
| c961f4751b | |||
| 9e5ec0c9df | |||
| 112c034e27 | |||
| 664a3ddeea | |||
| 2ffd357d95 | |||
| 2bc4a13964 | |||
| 58324945cc | |||
| 16953ea907 | |||
| d3c2dc8286 | |||
| ebb0b45299 | |||
| 702aa11ecf | |||
| 33221d386e | |||
| 589c159eb9 | |||
| ea1f4d48d5 | |||
| 462c5a1d42 | |||
| f90d7ddee3 | |||
| 175055cf9d | |||
| fe92d95f6c | |||
| 8e50d95a7a | |||
| 9ed36b2aa3 | |||
| 722165a734 | |||
| 74f99e2588 | |||
| 77c08845b5 | |||
| 2bfc26faf2 |
+1
-1
@@ -16,7 +16,7 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
|
||||
BASE_URL=plu.me
|
||||
|
||||
# Log level for each crate
|
||||
RUST_LOG=warn,html5ever=warn,hyper=warn,tantivy=warn
|
||||
RUST_LOG=info
|
||||
|
||||
# The secret key for private cookies and CSRF protection
|
||||
# You can generate one with `openssl rand -base64 32`
|
||||
|
||||
@@ -19,11 +19,17 @@
|
||||
- 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
+905
-750
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -15,12 +15,11 @@ 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"
|
||||
|
||||
@@ -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.
|
||||
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
|
||||
|
||||
## Get involved
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ main article {
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 5px solid $gray;
|
||||
border-inline-start: 5px solid $gray;
|
||||
margin: 1em auto;
|
||||
padding: 0em 2em;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX medias_index_file_path;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX medias_index_file_path ON medias (file_path);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances DROP COLUMN private_key;
|
||||
ALTER TABLE instances DROP COLUMN public_key;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX medias_index_file_path;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX medias_index_file_path ON medias (file_path);
|
||||
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
||||
@@ -1,6 +1,6 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{instance::*, safe_string::SafeString, Connection};
|
||||
use plume_models::{instance::NewInstance, Connection};
|
||||
use std::env;
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -53,19 +53,5 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
.unwrap_or_else(|| String::from("CC-BY-SA"));
|
||||
let open_reg = !args.is_present("private");
|
||||
|
||||
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");
|
||||
NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
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.
|
||||
@@ -10,7 +15,8 @@ use std::fmt::Debug;
|
||||
/// ```rust
|
||||
/// # extern crate activitypub;
|
||||
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
|
||||
/// # use plume_common::activity_pub::inbox::*;
|
||||
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
/// # use plume_common::activity_pub::{inbox::*, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer}};
|
||||
/// # struct User;
|
||||
/// # impl FromId<()> for User {
|
||||
/// # type Error = ();
|
||||
@@ -59,6 +65,42 @@ use std::fmt::Debug;
|
||||
/// # 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();
|
||||
@@ -69,8 +111,9 @@ use std::fmt::Debug;
|
||||
/// # let activity_json = serde_json::to_value(act).unwrap();
|
||||
/// #
|
||||
/// # let conn = ();
|
||||
/// # let sender = MySigner::new();
|
||||
/// #
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, &sender, activity_json)
|
||||
/// .with::<User, Announce, Message>(None)
|
||||
/// .with::<User, Create, Message>(None)
|
||||
/// .done();
|
||||
@@ -84,9 +127,10 @@ 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, serde_json::Value, InboxError<E>),
|
||||
NotHandled(&'a C, &'a dyn Signer, serde_json::Value, InboxError<E>),
|
||||
|
||||
/// A matching handler have been found but failed
|
||||
///
|
||||
@@ -139,8 +183,12 @@ where
|
||||
///
|
||||
/// - `ctx`: the context to pass to each handler
|
||||
/// - `json`: the JSON representation of the incoming activity
|
||||
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> {
|
||||
Inbox::NotHandled(ctx, json, InboxError::NoMatch)
|
||||
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)
|
||||
}
|
||||
|
||||
/// Registers an handler on this Inbox.
|
||||
@@ -151,27 +199,30 @@ where
|
||||
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
|
||||
M::Output: Into<R>,
|
||||
{
|
||||
if let Inbox::NotHandled(ctx, mut act, e) = self {
|
||||
if let Inbox::NotHandled(ctx, sender, 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, act, InboxError::InvalidID),
|
||||
None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
|
||||
};
|
||||
|
||||
// Get the actor ID
|
||||
let actor_id = match get_id(act["actor"].clone()) {
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidActor(None))
|
||||
}
|
||||
};
|
||||
|
||||
if Self::is_spoofed_activity(&actor_id, &act) {
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None));
|
||||
return Inbox::NotHandled(ctx, sender, 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,
|
||||
@@ -182,17 +233,25 @@ where
|
||||
if let Some(json) = json {
|
||||
act["actor"] = json;
|
||||
}
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
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, act, InboxError::InvalidObject(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None))
|
||||
}
|
||||
};
|
||||
let obj = match M::from_id(
|
||||
ctx,
|
||||
sender,
|
||||
&obj_id,
|
||||
serde_json::from_value(act["object"].clone()).ok(),
|
||||
proxy,
|
||||
@@ -202,7 +261,12 @@ where
|
||||
if let Some(json) = json {
|
||||
act["object"] = json;
|
||||
}
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
act,
|
||||
InboxError::InvalidObject(Some(e)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -214,7 +278,7 @@ where
|
||||
} else {
|
||||
// If the Activity type is not matching the expected one for
|
||||
// this handler, try with the next one.
|
||||
Inbox::NotHandled(ctx, act, e)
|
||||
Inbox::NotHandled(ctx, sender, act, e)
|
||||
}
|
||||
} else {
|
||||
self
|
||||
@@ -225,7 +289,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),
|
||||
}
|
||||
}
|
||||
@@ -292,6 +356,7 @@ 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>,
|
||||
@@ -300,7 +365,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, proxy.cloned())?)
|
||||
None => Self::from_activity(ctx, Self::deref(id, sender, proxy.cloned())?)
|
||||
.map_err(|e| (None, e)),
|
||||
},
|
||||
}
|
||||
@@ -309,8 +374,17 @@ 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 {
|
||||
@@ -320,15 +394,11 @@ pub trait FromId<C>: Sized {
|
||||
.build()
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?
|
||||
.get(id)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
ACCEPT,
|
||||
HeaderValue::from_str(
|
||||
&super::ap_accept_header()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?,
|
||||
"Signature",
|
||||
request::signature(sender, &headers, ("get", url.path(), url.query()))
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?,
|
||||
)
|
||||
.send()
|
||||
.map_err(|_| (None, InboxError::DerefError))
|
||||
@@ -458,8 +528,10 @@ 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 {
|
||||
@@ -557,10 +629,47 @@ 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(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_ok());
|
||||
@@ -569,7 +678,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_inbox_multi_handlers() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Delete, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
@@ -582,7 +691,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(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Like, MyObject>(None)
|
||||
.done();
|
||||
@@ -631,12 +740,12 @@ mod tests {
|
||||
fn test_inbox_actor_failure() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_err());
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
|
||||
@@ -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<S: Signer>(
|
||||
signer: &S,
|
||||
pub fn signature(
|
||||
signer: &dyn Signer,
|
||||
headers: &HeaderMap,
|
||||
request_target: RequestTarget,
|
||||
) -> Result<HeaderValue, Error> {
|
||||
@@ -166,8 +166,10 @@ pub fn signature<S: Signer>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{signature, Error};
|
||||
use crate::activity_pub::sign::{gen_keypair, Signer};
|
||||
use super::signature;
|
||||
use crate::activity_pub::sign::{
|
||||
gen_keypair, Error as SignatureError, Result as SignatureResult, Signer,
|
||||
};
|
||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
@@ -187,26 +189,24 @@ mod tests {
|
||||
}
|
||||
|
||||
impl Signer for MySigner {
|
||||
type Error = Error;
|
||||
|
||||
fn get_key_id(&self) -> String {
|
||||
"mysigner".into()
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error> {
|
||||
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(|_| Error())
|
||||
signer.sign_to_vec().map_err(|_| SignatureError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error> {
|
||||
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(|_| Error())
|
||||
verifier.verify(&signature).map_err(|_| SignatureError())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,20 +19,25 @@ 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>, Self::Error>;
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>;
|
||||
/// Verify if the signature is valid
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
|
||||
}
|
||||
|
||||
pub trait Signable {
|
||||
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error>
|
||||
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self>
|
||||
where
|
||||
T: Signer;
|
||||
fn verify<T>(self, creator: &T) -> bool
|
||||
@@ -46,7 +51,7 @@ pub trait Signable {
|
||||
}
|
||||
|
||||
impl Signable for serde_json::Value {
|
||||
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> {
|
||||
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value> {
|
||||
let creation_date = Utc::now().to_rfc3339();
|
||||
let mut options = json!({
|
||||
"type": "RsaSignature2017",
|
||||
|
||||
@@ -27,6 +27,59 @@ 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.
|
||||
*
|
||||
@@ -476,6 +529,20 @@ 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!(
|
||||
|
||||
@@ -8,15 +8,14 @@ edition = "2018"
|
||||
activitypub = "0.1.1"
|
||||
ammonia = "2.1.1"
|
||||
askama_escape = "0.1"
|
||||
bcrypt = "0.5"
|
||||
bcrypt = "0.10.1"
|
||||
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"
|
||||
|
||||
+34
-10
@@ -18,7 +18,8 @@ use openssl::{
|
||||
};
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, FromId},
|
||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
sign::{self, Error as SignatureError, Result as SignatureResult},
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
};
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
@@ -149,7 +150,16 @@ 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, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
|
||||
.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)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||
@@ -359,6 +369,8 @@ impl FromId<DbConn> for Blog {
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
@@ -372,7 +384,14 @@ impl FromId<DbConn> for Blog {
|
||||
Media::save_remote(
|
||||
conn,
|
||||
icon.object_props.url_string().ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@@ -388,7 +407,14 @@ impl FromId<DbConn> for Blog {
|
||||
Media::save_remote(
|
||||
conn,
|
||||
banner.object_props.url_string().ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@@ -451,24 +477,22 @@ 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) -> Result<Vec<u8>> {
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<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(Error::from)
|
||||
signer.sign_to_vec().map_err(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(Error::from)
|
||||
verifier.verify(&signature).map_err(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,7 @@ impl FromId<DbConn> for Comment {
|
||||
})?,
|
||||
author_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
¬e.object_props.attributed_to_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@@ -255,9 +256,7 @@ 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();
|
||||
Ok(Mention::from_activity(
|
||||
conn, &m, comm.id, false, not_author,
|
||||
)?)
|
||||
Mention::from_activity(conn, &m, comm.id, false, not_author)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -296,7 +295,13 @@ 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, &v, None, CONFIG.proxy()) {
|
||||
if let Ok(user) = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&v,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![] // TODO try to fetch collection
|
||||
|
||||
@@ -164,11 +164,8 @@ impl Default for LogoConfig {
|
||||
};
|
||||
let mut custom_icons = env::vars()
|
||||
.filter_map(|(var, val)| {
|
||||
if let Some(size) = var.strip_prefix("PLUME_LOGO_") {
|
||||
Some((size.to_owned(), val))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
var.strip_prefix("PLUME_LOGO_")
|
||||
.map(|size| (size.to_owned(), val))
|
||||
})
|
||||
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
|
||||
.map(|(dim, src)| Icon {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error,
|
||||
Result, CONFIG,
|
||||
ap_url, db_conn::DbConn, instance::Instance, 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,6 +168,7 @@ 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(),
|
||||
@@ -176,6 +177,7 @@ 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(),
|
||||
|
||||
+21
-15
@@ -3,7 +3,9 @@ use activitypub::activity::*;
|
||||
use crate::{
|
||||
comments::Comment,
|
||||
db_conn::DbConn,
|
||||
follows, likes,
|
||||
follows,
|
||||
instance::Instance,
|
||||
likes,
|
||||
posts::{Post, PostUpdate},
|
||||
reshares::Reshare,
|
||||
users::User,
|
||||
@@ -47,20 +49,24 @@ impl_into_inbox_result! {
|
||||
}
|
||||
|
||||
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||
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()
|
||||
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()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -6,10 +6,26 @@ use crate::{
|
||||
users::{Role, User},
|
||||
Connection, Error, Result,
|
||||
};
|
||||
use activitypub::{actor::Service, CustomObject};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::utils::md_to_html;
|
||||
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 std::sync::RwLock;
|
||||
use tracing::warn;
|
||||
|
||||
pub type CustomService = CustomObject<ApSignature, Service>;
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
pub struct Instance {
|
||||
@@ -25,6 +41,8 @@ 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)]
|
||||
@@ -39,6 +57,8 @@ 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! {
|
||||
@@ -69,6 +89,13 @@ 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))
|
||||
@@ -238,6 +265,112 @@ 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)]
|
||||
@@ -259,6 +392,8 @@ 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(),
|
||||
@@ -270,6 +405,8 @@ 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(),
|
||||
@@ -281,6 +418,8 @@ 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(),
|
||||
@@ -292,6 +431,8 @@ 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()
|
||||
|
||||
+26
-1
@@ -17,8 +17,10 @@ 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;
|
||||
use plume_common::activity_pub::{inbox::InboxError, sign};
|
||||
use posts::PostEvent;
|
||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||
use users::UserEvent;
|
||||
@@ -80,6 +82,12 @@ 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)
|
||||
@@ -160,6 +168,12 @@ 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.
|
||||
///
|
||||
@@ -295,6 +309,17 @@ 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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
|
||||
users::User, Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity;
|
||||
use chrono::NaiveDateTime;
|
||||
@@ -117,6 +117,7 @@ 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(),
|
||||
@@ -125,6 +126,7 @@ 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(),
|
||||
|
||||
@@ -285,7 +285,8 @@ 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().filter_map(|o| o).collect::<Vec<String>>())
|
||||
.map(|r| r.into_iter().flatten().collect::<Vec<String>>())
|
||||
}
|
||||
|
||||
pub fn clear(&self, conn: &Connection) -> Result<()> {
|
||||
|
||||
+68
-30
@@ -12,14 +12,14 @@ use plume_common::{
|
||||
};
|
||||
use std::{
|
||||
fs::{self, DirBuilder},
|
||||
path::{Path, PathBuf},
|
||||
path::{self, Path, PathBuf},
|
||||
};
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
|
||||
pub struct Media {
|
||||
pub id: i32,
|
||||
pub file_path: String,
|
||||
@@ -65,6 +65,7 @@ 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
|
||||
@@ -155,12 +156,15 @@ impl Media {
|
||||
if self.is_remote {
|
||||
Ok(self.remote_url.clone().unwrap_or_default())
|
||||
} else {
|
||||
let p = Path::new(&self.file_path);
|
||||
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
|
||||
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()
|
||||
Ok(ap_url(&format!(
|
||||
"{}/static/media/{}",
|
||||
"{}/{}",
|
||||
Instance::get_local()?.public_domain,
|
||||
&filename
|
||||
&file_path
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -224,32 +228,66 @@ impl Media {
|
||||
.copy_to(&mut dest)
|
||||
.ok()?;
|
||||
|
||||
// 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(
|
||||
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(
|
||||
conn,
|
||||
image
|
||||
.object_props
|
||||
.attributed_to_link_vec::<Id>()
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.next()?
|
||||
.as_ref(),
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
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,
|
||||
},
|
||||
)
|
||||
.map_err(|(_, e)| e)?
|
||||
.id,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
|
||||
|
||||
+126
-52
@@ -11,14 +11,13 @@ 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::{AsObject, FromId},
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
|
||||
},
|
||||
utils::md_to_html,
|
||||
utils::{iri_percent_encode_seg, md_to_html},
|
||||
};
|
||||
use riker::actors::{Publish, Tell};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -73,12 +72,7 @@ impl Post {
|
||||
.execute(conn)?;
|
||||
let mut post = Self::last(conn)?;
|
||||
if post.ap_url.is_empty() {
|
||||
post.ap_url = ap_url(&format!(
|
||||
"{}/~/{}/{}/",
|
||||
CONFIG.base_url,
|
||||
post.get_blog(conn)?.fqn,
|
||||
post.slug
|
||||
));
|
||||
post.ap_url = Self::ap_url(post.get_blog(conn)?, &post.slug);
|
||||
let _: Post = post.save_changes(conn)?;
|
||||
}
|
||||
|
||||
@@ -94,7 +88,10 @@ impl Post {
|
||||
let post = Self::get(conn, self.id)?;
|
||||
// TODO: Call publish_published() when newly published
|
||||
if post.published {
|
||||
self.publish_updated();
|
||||
let blog = post.get_blog(conn);
|
||||
if blog.is_ok() && blog.unwrap().is_local() {
|
||||
self.publish_updated();
|
||||
}
|
||||
}
|
||||
Ok(post)
|
||||
}
|
||||
@@ -251,6 +248,20 @@ 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;
|
||||
@@ -443,13 +454,7 @@ impl Post {
|
||||
m,
|
||||
)
|
||||
})
|
||||
.filter_map(|(id, m)| {
|
||||
if let Some(id) = id {
|
||||
Some((m, id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|(id, m)| id.map(|id| (m, id)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let old_mentions = Mention::list_for_post(&conn, self.id)?;
|
||||
@@ -625,13 +630,28 @@ impl FromId<DbConn> for Post {
|
||||
.into_iter()
|
||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||
let url = link;
|
||||
match User::from_id(conn, &url, None, CONFIG.proxy()) {
|
||||
match User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
Ok(u) => {
|
||||
authors.push(u);
|
||||
(blog, authors)
|
||||
}
|
||||
Err(_) => (
|
||||
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
|
||||
blog.or_else(|| {
|
||||
Blog::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()
|
||||
}),
|
||||
authors,
|
||||
),
|
||||
}
|
||||
@@ -644,37 +664,85 @@ 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()?;
|
||||
// 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 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;
|
||||
|
||||
for author in authors {
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
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)
|
||||
})
|
||||
})?;
|
||||
|
||||
// save mentions and tags
|
||||
let mut hashtags = md_to_html(&post.source, None, false, None)
|
||||
@@ -784,8 +852,14 @@ 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, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
||||
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)?;
|
||||
|
||||
if !post.is_author(conn, actor.id)? {
|
||||
// TODO: maybe the author was added in the meantime
|
||||
@@ -793,7 +867,7 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
|
||||
}
|
||||
|
||||
if let Some(title) = self.title {
|
||||
post.slug = title.to_kebab_case();
|
||||
post.slug = Post::slug(&title).to_string();
|
||||
post.title = title;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
db_conn::{DbConn, DbPool},
|
||||
follows,
|
||||
instance::Instance,
|
||||
posts::{LicensedArticle, Post},
|
||||
users::{User, UserEvent},
|
||||
ACTOR_SYS, CONFIG, USER_CHAN,
|
||||
@@ -17,24 +18,22 @@ pub struct RemoteFetchActor {
|
||||
|
||||
impl RemoteFetchActor {
|
||||
pub fn init(conn: DbPool) {
|
||||
ACTOR_SYS
|
||||
let actor = ACTOR_SYS
|
||||
.actor_of_args::<RemoteFetchActor, _>("remote-fetch", conn)
|
||||
.expect("Failed to initialize remote fetch actor");
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
actor: Box::new(actor),
|
||||
topic: "*".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for RemoteFetchActor {
|
||||
type Msg = UserEvent;
|
||||
|
||||
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
|
||||
use UserEvent::*;
|
||||
@@ -91,7 +90,13 @@ 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, &user_id, None, CONFIG.proxy());
|
||||
let follower = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&user_id,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
);
|
||||
match follower {
|
||||
Ok(follower) => {
|
||||
let inserted = follows::Follow::insert(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
|
||||
Connection, Error, Result, CONFIG,
|
||||
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
|
||||
timeline::*, users::User, Connection, Error, Result, CONFIG,
|
||||
};
|
||||
use activitypub::activity::{Announce, Undo};
|
||||
use chrono::NaiveDateTime;
|
||||
@@ -142,6 +142,7 @@ 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(),
|
||||
@@ -150,6 +151,7 @@ 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(),
|
||||
|
||||
@@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for SafeString {
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(deserializer.deserialize_string(SafeStringVisitor)?)
|
||||
deserializer.deserialize_string(SafeStringVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ table! {
|
||||
default_license -> Text,
|
||||
long_description_html -> Varchar,
|
||||
short_description_html -> Varchar,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,24 +13,22 @@ pub struct SearchActor {
|
||||
|
||||
impl SearchActor {
|
||||
pub fn init(searcher: Arc<Searcher>, conn: DbPool) {
|
||||
ACTOR_SYS
|
||||
let actor = ACTOR_SYS
|
||||
.actor_of_args::<SearchActor, _>("search", (searcher, conn))
|
||||
.expect("Failed to initialize searcher actor");
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
actor: Box::new(actor),
|
||||
topic: "*".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for SearchActor {
|
||||
type Msg = PostEvent;
|
||||
|
||||
fn recv(&mut self, _ctx: &Context<Self::Msg>, msg: Self::Msg, _sender: Sender) {
|
||||
use PostEvent::*;
|
||||
@@ -166,6 +164,8 @@ mod tests {
|
||||
name: random_hex().to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -223,6 +223,9 @@ 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,
|
||||
@@ -236,6 +239,16 @@ 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)]
|
||||
|
||||
@@ -601,11 +601,10 @@ 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::new();
|
||||
res.push(match stream.get(0)? {
|
||||
let mut res: Vec<&str> = vec![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)? {
|
||||
|
||||
+16
-12
@@ -24,7 +24,7 @@ use plume_common::{
|
||||
activity_pub::{
|
||||
ap_accept_header,
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::{gen_keypair, Signer},
|
||||
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
|
||||
},
|
||||
utils,
|
||||
@@ -210,7 +210,14 @@ impl User {
|
||||
.into_iter()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
.ok_or(Error::Webfinger)?;
|
||||
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
|
||||
User::from_id(
|
||||
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> {
|
||||
@@ -486,10 +493,7 @@ impl User {
|
||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||
.collect::<Vec<T>>();
|
||||
|
||||
let next = match json.get("next") {
|
||||
Some(x) => Some(x.as_str().unwrap().to_owned()),
|
||||
None => None,
|
||||
};
|
||||
let next = json.get("next").map(|x| x.as_str().unwrap().to_owned());
|
||||
Ok((items, next))
|
||||
}
|
||||
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
|
||||
@@ -961,6 +965,8 @@ impl FromId<DbConn> for User {
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
@@ -1066,24 +1072,22 @@ 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) -> Result<Vec<u8>> {
|
||||
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(Error::from)
|
||||
signer.sign_to_vec().map_err(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(Error::from)
|
||||
verifier.verify(&signature).map_err(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+52
-52
@@ -60,83 +60,83 @@ msgstr ""
|
||||
msgid "Optional"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:63
|
||||
# src/routes/blogs.rs:67
|
||||
msgid "To create a new blog, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:102
|
||||
# src/routes/blogs.rs:109
|
||||
msgid "A blog with the same name already exists."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:140
|
||||
# src/routes/blogs.rs:147
|
||||
msgid "Your blog was successfully created!"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:159
|
||||
# src/routes/blogs.rs:165
|
||||
msgid "Your blog was deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:167
|
||||
# src/routes/blogs.rs:173
|
||||
msgid "You are not allowed to delete this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:218
|
||||
# src/routes/blogs.rs:223
|
||||
msgid "You are not allowed to edit this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:274
|
||||
# src/routes/blogs.rs:279
|
||||
msgid "You can't use this media as a blog icon."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:292
|
||||
# src/routes/blogs.rs:297
|
||||
msgid "You can't use this media as a blog banner."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:326
|
||||
# src/routes/blogs.rs:331
|
||||
msgid "Your blog information have been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/comments.rs:99
|
||||
# src/routes/comments.rs:100
|
||||
msgid "Your comment has been posted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/comments.rs:178
|
||||
# src/routes/comments.rs:177
|
||||
msgid "Your comment has been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:118
|
||||
# src/routes/instance.rs:147
|
||||
msgid "Instance settings have been saved."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:150
|
||||
# src/routes/instance.rs:180
|
||||
msgid "{} has been unblocked."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:152
|
||||
# src/routes/instance.rs:182
|
||||
msgid "{} has been blocked."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:201
|
||||
# src/routes/instance.rs:233
|
||||
msgid "Blocks deleted"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:216
|
||||
# src/routes/instance.rs:249
|
||||
msgid "Email already blocked"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:221
|
||||
# src/routes/instance.rs:254
|
||||
msgid "Email Blocked"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:312
|
||||
# src/routes/instance.rs:347
|
||||
msgid "You can't change your own rights."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:323
|
||||
# src/routes/instance.rs:358
|
||||
msgid "You are not allowed to take this action."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:359
|
||||
# src/routes/instance.rs:393
|
||||
msgid "Done."
|
||||
msgstr ""
|
||||
|
||||
@@ -144,23 +144,23 @@ msgstr ""
|
||||
msgid "To like a post, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:145
|
||||
# src/routes/medias.rs:158
|
||||
msgid "Your media have been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:150
|
||||
# src/routes/medias.rs:163
|
||||
msgid "You are not allowed to delete this media."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:167
|
||||
# src/routes/medias.rs:180
|
||||
msgid "Your avatar has been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:172
|
||||
# src/routes/medias.rs:185
|
||||
msgid "You are not allowed to use this media."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/notifications.rs:28
|
||||
# src/routes/notifications.rs:29
|
||||
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:126
|
||||
# src/routes/posts.rs:125
|
||||
msgid "To write a new post, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:143
|
||||
# src/routes/posts.rs:146
|
||||
msgid "You are not an author of this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:150
|
||||
# src/routes/posts.rs:153
|
||||
msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:195
|
||||
# src/routes/posts.rs:198
|
||||
msgid "Edit {0}"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:264
|
||||
# src/routes/posts.rs:267
|
||||
msgid "You are not allowed to publish on this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:363
|
||||
# src/routes/posts.rs:367
|
||||
msgid "Your article has been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:553
|
||||
# src/routes/posts.rs:556
|
||||
msgid "Your article has been saved."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:560
|
||||
# src/routes/posts.rs:563
|
||||
msgid "New article"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:597
|
||||
# src/routes/posts.rs:601
|
||||
msgid "You are not allowed to delete this article."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:622
|
||||
# src/routes/posts.rs:625
|
||||
msgid "Your article has been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:627
|
||||
# src/routes/posts.rs:630
|
||||
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:667
|
||||
# src/routes/posts.rs:672
|
||||
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:88
|
||||
# src/routes/session.rs:95
|
||||
msgid "You are now connected."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:109
|
||||
# src/routes/session.rs:116
|
||||
msgid "You are now logged off."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:154
|
||||
# src/routes/session.rs:162
|
||||
msgid "Password reset"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:155
|
||||
# src/routes/session.rs:163
|
||||
msgid "Here is the link to reset your password: {0}"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:215
|
||||
# src/routes/session.rs:235
|
||||
msgid "Your password was successfully reset."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:142
|
||||
# src/routes/user.rs:74
|
||||
msgid "To access your dashboard, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:164
|
||||
# src/routes/user.rs:96
|
||||
msgid "You are no longer following {}."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:181
|
||||
# src/routes/user.rs:113
|
||||
msgid "You are now following {}."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:261
|
||||
# src/routes/user.rs:190
|
||||
msgid "To subscribe to someone, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:365
|
||||
# src/routes/user.rs:299
|
||||
msgid "To edit your profile, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:411
|
||||
# src/routes/user.rs:345
|
||||
msgid "Your profile has been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:438
|
||||
# src/routes/user.rs:373
|
||||
msgid "Your account has been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:444
|
||||
# src/routes/user.rs:379
|
||||
msgid "You can't delete someone else's account."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:528
|
||||
# src/routes/user.rs:463
|
||||
msgid "Registrations are closed on this instance."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:551
|
||||
# src/routes/user.rs:486
|
||||
msgid "Your account has been created. Now you just need to log in, before you can use it."
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
nightly-2021-01-15
|
||||
nightly-2021-03-25
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/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
@@ -25,7 +25,7 @@ parts:
|
||||
plume:
|
||||
plugin: rust
|
||||
source: .
|
||||
rust-revision: nightly-2020-01-15
|
||||
rust-revision: nightly-2021-03-25
|
||||
build-packages:
|
||||
- libssl-dev
|
||||
- pkg-config
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use heck::KebabCase;
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use crate::api::{authorization::*, Api};
|
||||
@@ -109,7 +108,7 @@ pub fn create(
|
||||
|
||||
let author = User::get(&conn, auth.0.user_id)?;
|
||||
|
||||
let slug = &payload.title.clone().to_kebab_case();
|
||||
let slug = Post::slug(&payload.title);
|
||||
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()
|
||||
});
|
||||
|
||||
+8
-2
@@ -26,8 +26,14 @@ 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, actor_id, None, CONFIG.proxy())
|
||||
.expect("instance::shared_inbox: user error");
|
||||
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");
|
||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
|
||||
// maybe we just know an old key?
|
||||
actor
|
||||
|
||||
@@ -15,6 +15,7 @@ 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},
|
||||
@@ -99,6 +100,7 @@ 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(
|
||||
@@ -147,6 +149,7 @@ 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,
|
||||
|
||||
+4
-2
@@ -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()?;
|
||||
Some(blog.outbox(&conn).ok()?)
|
||||
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()?;
|
||||
Some(blog.outbox_page(&conn, page.limits()).ok()?)
|
||||
blog.outbox_page(&conn, page.limits()).ok()
|
||||
}
|
||||
#[get("/~/<name>/atom.xml")]
|
||||
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
@@ -438,6 +438,8 @@ mod tests {
|
||||
name: random_hex().to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
+45
-3
@@ -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};
|
||||
use plume_common::activity_pub::{broadcast, inbox::FromId, ActivityStream, ApRequest};
|
||||
use plume_models::{
|
||||
admin::*,
|
||||
blocklisted_emails::*,
|
||||
@@ -49,6 +49,36 @@ 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()?;
|
||||
@@ -404,7 +434,13 @@ 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, &target, None, CONFIG.proxy()) {
|
||||
if let Ok(post) = Post::from_id(
|
||||
&conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&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,
|
||||
@@ -412,7 +448,13 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(comment) = Comment::from_id(&conn, &target, None, CONFIG.proxy()) {
|
||||
if let Ok(comment) = Comment::from_id(
|
||||
&conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&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
@@ -1,5 +1,4 @@
|
||||
use chrono::Utc;
|
||||
use heck::KebabCase;
|
||||
use rocket::http::uri::Uri;
|
||||
use rocket::request::LenientForm;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
@@ -236,7 +235,7 @@ pub fn update(
|
||||
let intl = &rockets.intl.catalog;
|
||||
|
||||
let new_slug = if !post.published {
|
||||
form.title.to_string().to_kebab_case()
|
||||
Post::slug(&form.title).to_string()
|
||||
} else {
|
||||
post.slug.clone()
|
||||
};
|
||||
@@ -290,6 +289,7 @@ 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 = title.to_string().to_kebab_case();
|
||||
let slug = Post::slug(title);
|
||||
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 = form.title.to_string().to_kebab_case();
|
||||
let slug = Post::slug(&form.title);
|
||||
let user = rockets.user.clone().unwrap();
|
||||
|
||||
let mut errors = match form.validate() {
|
||||
|
||||
Reference in New Issue
Block a user