Compare commits

...

61 Commits

Author SHA1 Message Date
Kitaiti Makoto 2fe705a712 Append semicolon at end of SQL 2021-09-24 05:38:16 +09:00
Kitaiti Makoto 3a448e9e17 Sign GET request to external instances 2021-09-24 04:27:22 +09:00
Kitaiti Makoto 6e4def4cc5 Implement Signer for Instance 2021-09-24 01:06:19 +09:00
Kitaiti Makoto 34b8fd83c1 Fix SQL to drop fields 2021-09-12 04:20:10 +09:00
Kitaiti Makoto d5774078e0 Revert "Move Rocket-unreleated code from init_rocket() to main()"
This reverts commit 64f0333497.
2021-09-12 04:16:30 +09:00
Kitaiti Makoto a3623412f9 Use Instance::get_locals() in ensure_local_instance_keys() 2021-09-12 03:57:30 +09:00
Kitaiti Makoto 1ed60537cf Define Instance::get_locals() 2021-09-12 03:57:12 +09:00
Kitaiti Makoto 037d670fb7 Make tests follow field addition to instances table 2021-09-12 03:09:28 +09:00
Kitaiti Makoto 8b817d50c5 Add URI /!/<public_domain> 2021-09-12 02:14:49 +09:00
Kitaiti Makoto fa48060a94 Define Instance::to_activity() 2021-09-12 02:14:17 +09:00
Kitaiti Makoto b41e982daf Run plume_models::migrate_data() on initialization 2021-09-11 23:06:49 +09:00
Kitaiti Makoto 2fcb449ed8 Define plume_models::migrate_data() 2021-09-11 23:06:02 +09:00
Kitaiti Makoto 218bc54a5f Define Instance::set_keypair() 2021-09-11 23:05:31 +09:00
Kitaiti Makoto 64f0333497 Move Rocket-unreleated code from init_rocket() to main() 2021-09-11 22:23:26 +09:00
Kitaiti Makoto 8aa7a5780d Use NewInstance::new_local in cli instance command 2021-09-11 22:20:34 +09:00
Kitaiti Makoto 17c398bcee Define NewInstance::new_local() 2021-09-11 22:17:40 +09:00
Kitaiti Makoto 76f1455372 Follow addition of key fields to instances table 2021-09-11 21:59:48 +09:00
Kitaiti Makoto 1bcad6d7cd Run diesel migration run 2021-09-11 21:58:15 +09:00
Kitaiti Makoto 9a58d9bcb7 Copy instances_add_keys migration for SQLite 2021-09-09 13:57:12 +09:00
Kitaiti Makoto c2dcac4413 Add private_key and public_key fields to instances table 2021-09-09 13:56:28 +09:00
Kitaiti Makoto 394273e866 Generate migration files to add keys to instances
% diesel migration generate instances_add_keys
2021-09-08 22:12:07 +09:00
Kitaiti Makoto 1bcc70c174 [REFACTORING]Use headers utility function for deref() 2021-09-08 20:29:26 +09:00
KitaitiMakoto 188d4ac063 Merge pull request 'Post ap_url as valid IRI' (#947) from post-slug into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/947
2021-09-07 05:36:00 +00:00
Kitaiti Makoto 0fbefe2cdc Percent-encode Post slug on sending Activity to other instances 2021-09-07 13:31:06 +09:00
Kitaiti Makoto eedd5fe4e9 Define utility function to percent-encode for IRI 2021-09-06 17:49:10 +09:00
KitaitiMakoto 27a1a56223 Merge pull request 'Make Feature list more clear' (#946) from github-856 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/946
2021-09-05 12:50:49 +00:00
Kitaiti Makoto 8e3322776a Merge remote-tracking branch 'origin/main' into github-856 2021-09-05 21:49:50 +09:00
KitaitiMakoto 3d4336c548 Merge pull request 'update some dependancies' (#945) from update-deps into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/945
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-09-05 12:48:07 +00:00
Trinity Pointard 0c8c607aa4 update some dependancies 2021-09-05 14:41:36 +02:00
D5k H3h 60d6734fe2 Make Feature list more clear
Say it, when something is currently missing.
Otherwise, people will get confused.
2021-09-05 11:58:02 +02:00
KitaitiMakoto 8c372aa6fc Merge pull request 'Fixes #929 Don't stip shipped binaries' (#942) from dont-strip into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/942
2021-08-15 15:46:02 +00:00
Kitaiti Makoto d1a74ca8e6 Don't stip shipped binaries 2021-08-16 00:44:15 +09:00
KitaitiMakoto c374e0af4c Merge pull request 'Upgrade bcrypt create' (#941) from update-blowfish into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/941
2021-08-14 10:23:41 +00:00
Kitaiti Makoto 58e8569048 Install bcrypt 2021-08-14 19:20:30 +09:00
Kitaiti Makoto 83dbf2a945 Upgrade bcrypt 2021-08-14 19:20:21 +09:00
trinity-1686a d7d6d5f644 Merge pull request 'Draw side line for blockquote on start' (#933) from ahangarha/Plume:ahangarha-patch-1-blockquote-line into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/933
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-07-13 10:16:26 +00:00
trinity-1686a 9aa9885a89 Merge pull request 'Specify rocket version' (#937) from zynnnn/Plume:fix-rocket-version into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/937
Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
2021-07-13 10:15:53 +00:00
Trinity Pointard 7fe1e083e0 use newer pulldown-cmark 2021-07-10 13:34:13 +02:00
Tdxdxoz a946823554 Specify rocket version 2021-07-06 22:36:59 +08:00
ahangarha 3a1872c03e Draw side line for blockquote on start
By this change, the line beside blockquote would appear at start. It means if the text is in LTR language, it appears on left and if it is in RTL, the line appears on right.
2021-06-11 11:53:28 +00:00
KitaitiMakoto 5424f9110f Merge pull request 'Add changelog about change of slug' (#921) from changelog into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/921
2021-04-11 02:45:56 +00:00
Kitaiti Makoto a597816617 Add changelog about change of slug 2021-04-11 11:44:52 +09:00
KitaitiMakoto fa7a44f6bb Merge pull request 'Fix #326 and #721: Keep title in URI' (#920) from keep-title-in-uri into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/920
2021-04-10 08:27:31 +00:00
Kitaiti Makoto dfcdcc1833 Remove heck from plume and plume-models 2021-04-10 16:39:23 +09:00
Kitaiti Makoto 6345a57498 Don't modify article title for slug 2021-04-10 16:37:26 +09:00
Kitaiti Makoto 87457c0ed1 Define Post::slug() 2021-04-10 16:30:50 +09:00
KitaitiMakoto 859a1fd528 Merge pull request 'Set default RUST_LOG to info' (#919) from default-log-level into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/919
2021-04-10 07:04:53 +00:00
Kitaiti Makoto 5069aab584 Set default RUST_LOG to info 2021-04-10 15:59:15 +09:00
KitaitiMakoto 033fde38fe Merge pull request 'Extract Post::ap_url()' (#918) from ap_url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/918
2021-04-09 08:41:04 +00:00
Kitaiti Makoto 7b2bab0f9d Extract Post::ap_url() 2021-04-09 10:55:09 +09:00
KitaitiMakoto a419ef5319 Merge pull request 'Prevent duplicated posts in 'all' timeline' (#917) from dup-timeline into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/917
2021-03-29 15:31:52 +00:00
Kitaiti Makoto f77dce9f12 Add changelog about fix to duplicated posts in all timeline 2021-03-29 23:35:31 +09:00
Kitaiti Makoto 4c2cd92f0d Prevent duplicated posts in 'all' timeline 2021-03-29 23:33:57 +09:00
KitaitiMakoto dd932e1f15 Merge pull request 'Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured' (#916) from media-directory into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/916
2021-03-28 18:18:14 +00:00
Kitaiti Makoto 4376810d96 Add changelog about fix of MEDIA_UPLOAD_DIRECTORY handling 2021-03-29 02:32:02 +09:00
Kitaiti Makoto c961f4751b Complete pull request number to changelog 2021-03-29 02:29:50 +09:00
Kitaiti Makoto 9e5ec0c9df Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured 2021-03-29 02:29:15 +09:00
KitaitiMakoto 112c034e27 Merge pull request 'Update Post.ap_url when published' (#915) from update-ap-url into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/915
2021-03-28 17:27:45 +00:00
Kitaiti Makoto 664a3ddeea Add changelog about update of ap_url 2021-03-29 02:26:30 +09:00
Kitaiti Makoto 2ffd357d95 Update Post.ap_url when published 2021-03-29 01:38:28 +09:00
KitaitiMakoto 2bc4a13964 Merge pull request 'Add migration to add index to medias.file_path for SQLite3' (#914) from upsert-sqlite into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/914
2021-03-28 14:22:10 +00:00
40 changed files with 1627 additions and 944 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ DATABASE_URL=postgres://plume:plume@localhost/plume
BASE_URL=plu.me
# Log level for each crate
RUST_LOG=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`
+4
View File
@@ -19,6 +19,7 @@
- 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
@@ -26,6 +27,9 @@
- 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
+902 -749
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -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"
+1 -1
View File
@@ -30,7 +30,7 @@ A lot of features are still missing, but what is already here should be quite st
- **Media management**: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
- **Federation**: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called *instances*) have their own
rules and thematics, but they can all communicate with each other.
- **Collaborative writing**: invite other people to your blogs, and write articles together.
- **Collaborative writing**: invite other people to your blogs, and write articles together. (Not implemented yet, but will be in 1.0)
## Get involved
+1 -1
View File
@@ -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,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,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;
+2 -16
View File
@@ -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");
}
+1 -1
View File
@@ -14,7 +14,7 @@ heck = "0.3.0"
hex = "0.3"
hyper = "0.12.33"
openssl = "0.10.22"
rocket = "0.4.6"
rocket = "=0.4.6"
reqwest = { version = "0.9", features = ["socks"] }
serde = "1.0"
serde_derive = "1.0"
+138 -29
View File
@@ -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();
+10 -10
View File
@@ -118,8 +118,8 @@ type Path<'a> = &'a str;
type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature<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())
}
}
+11 -6
View File
@@ -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",
+67
View File
@@ -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!(
+2 -3
View File
@@ -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
View File
@@ -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)
}
}
+8 -1
View File
@@ -236,6 +236,7 @@ impl FromId<DbConn> for Comment {
})?,
author_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&note.object_props.attributed_to_link::<Id>()?,
None,
CONFIG.proxy(),
@@ -294,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
+4 -2
View File
@@ -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
View File
@@ -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)]
+142 -1
View File
@@ -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
View File
@@ -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 {
+4 -2
View File
@@ -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(),
+6 -1
View File
@@ -156,7 +156,11 @@ impl Media {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/");
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!(
"{}/{}",
Instance::get_local()?.public_domain,
@@ -268,6 +272,7 @@ impl Media {
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>()
+45 -16
View File
@@ -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::{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)?;
}
@@ -254,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;
@@ -622,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,
),
}
@@ -649,12 +672,12 @@ impl FromId<DbConn> for Post {
.and_then(|mut post| {
let mut updated = false;
let slug = title.to_kebab_case();
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;
post.slug = slug.to_string();
updated = true;
}
if post.title != title {
@@ -693,7 +716,7 @@ impl FromId<DbConn> for Post {
conn,
NewPost {
blog_id: blog?.id,
slug: title.to_kebab_case(),
slug: Self::slug(&title).to_string(),
title,
content: SafeString::new(&article.object_props.content_string()?),
published: true,
@@ -829,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
@@ -838,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;
}
+8 -1
View File
@@ -1,6 +1,7 @@
use crate::{
db_conn::{DbConn, DbPool},
follows,
instance::Instance,
posts::{LicensedArticle, Post},
users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN,
@@ -89,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(
+4 -2
View File
@@ -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(),
+2
View File
@@ -106,6 +106,8 @@ table! {
default_license -> Text,
long_description_html -> Varchar,
short_description_html -> Varchar,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
}
}
+2
View File
@@ -164,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();
+13
View File
@@ -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)]
+15 -8
View File
@@ -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> {
@@ -958,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,
},
)
})?;
@@ -1063,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
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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,
+2
View File
@@ -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
View File
@@ -11,7 +11,7 @@ use validator::{Validate, ValidationErrors};
use crate::inbox;
use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{broadcast, inbox::FromId};
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
View File
@@ -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() {