Compare commits

..

22 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
191 changed files with 10966 additions and 14472 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ executors:
default: false default: false
docker: docker:
- image: plumeorg/plume-buildenv:v0.4.0 - image: plumeorg/plume-buildenv:v0.4.0
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>> - image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_DB: plume POSTGRES_DB: plume
+1 -1
View File
@@ -11,7 +11,7 @@ RUN apt update &&\
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
#install and configure rust #install and configure rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-11-27 -y &&\ RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2021-01-15 -y &&\
rustup component add rustfmt clippy &&\ rustup component add rustfmt clippy &&\
rustup component add rust-std --target wasm32-unknown-unknown rustup component add rust-std --target wasm32-unknown-unknown
-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: daily
@@ -1,30 +0,0 @@
name: cd
on:
push:
branches:
- 'main'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: plumeorg/plume:latest
-36
View File
@@ -1,36 +0,0 @@
name: cd
on:
push:
tags:
- '*.*.*'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: plumeorg/plume
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
+2 -57
View File
@@ -4,52 +4,6 @@
## [Unreleased] - ReleaseDate ## [Unreleased] - ReleaseDate
## [[0.7.2]] - 2022-05-11
### Added
- Basque language (#1013)
- Unit tests for ActivityPub (#1021)
- Move to action area after liking/boosting/commenting (#1074)
### Changed
- Bump Rust to nightly 2022-01-26 (#1015)
- Remove "Latest articles" timeline (#1069)
- Change order of timeline tabs (#1069, #1070, #1072)
- Migrate ActivityPub-related crates from activitypub 0.1 to activitystreams 0.7 (#1022)
### Fixed
- Add explanation of sign-up step at sign-up page when email sign-up mode (#1012)
- Add NOT NULL constraint to email_blocklist table fields (#1016)
- Don't fill empty content when switching rich editor (#1017)
- Fix accept header (#1058)
- Render 404 page instead of 500 when data is not found (#1062)
- Reuse reqwest client on broadcasting (#1059)
- Reduce broadcasting HTTP request at once to prevent them being timed out (#1068, #1071)
- Some ActivityPub data (#1021)
## [[0.7.1]] - 2022-01-12
### Added
- Introduce environment variable `MAIL_PORT` (#980)
- Introduce email sign-up feature (#636, #1002)
### Changed
- Some styling improvements (#976, #977, #978)
- Respond with error status code when error (#1002)
### Fiexed
- Fix comment link (#974)
- Fix a bug that prevents posting articles (#975)
- Fix a bug that notification page doesn't show (#981)
## [[0.7.0]] - 2022-01-02
### Added ### Added
- Allow `dir` attributes for LtoR text in RtoL document (#860) - Allow `dir` attributes for LtoR text in RtoL document (#860)
@@ -57,18 +11,15 @@
- Proxy support (#829) - Proxy support (#829)
- Riker a actor system library (#870) - Riker a actor system library (#870)
- (request-target) and Host header in HTTP Signature (#872) - (request-target) and Host header in HTTP Signature (#872)
- Default log levels for RUST_LOG (#885, #886, #919)
### Changed ### Changed
- Upgrade some dependent crates (#858) - Upgrade some dependent crates (#858)
- Use tracing crate (#868) - Use tracing crate (#868)
- Update Rust version to nightly-2021-11-27 (#961) - Update Rust version to nightly-2021-01-15 (#878)
- Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878) - Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878)
- Run searcher on actor system (#870) - Run searcher on actor system (#870)
- Extract a function to calculate posts' ap_url and share it with some places (#918)
- Use article title as its slug instead of capitalizing and inserting hyphens (#920) - Use article title as its slug instead of capitalizing and inserting hyphens (#920)
- Sign GET requests to other instances (#957)
### Fixed ### Fixed
@@ -79,9 +30,6 @@
- Update post's ActivityPub id when published by update (#915) - Update post's ActivityPub id when published by update (#915)
- Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916) - Calculate media URI properly even when MEDIA_UPLOAD_DIRECTORY configured (#916)
- Prevent duplicated posts in 'all' timeline (#917) - Prevent duplicated posts in 'all' timeline (#917)
- Draw side line for blockquote on start (#933)
- Fix URIs of posts on Mastodon (#947)
- Place edit link proper position (#956, #963, #964)
## [[0.6.0]] - 2020-12-29 ## [[0.6.0]] - 2020-12-29
@@ -262,10 +210,7 @@
- Ability to create multiple blogs - Ability to create multiple blogs
<!-- next-url --> <!-- next-url -->
[Unreleased]: https://github.com/Plume-org/Plume/compare/0.7.2...HEAD [Unreleased]: https://github.com/Plume-org/Plume/compare/0.6.0...HEAD
[[0.7.2]]: https://github.com/Plume-org/Plume/compare/0.7.1...0.7.2
[[0.7.1]]: https://github.com/Plume-org/Plume/compare/0.7.0...0.7.1
[[0.7.0]]: https://github.com/Plume-org/Plume/compare/0.6.0...0.7.0
[[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0 [[0.6.0]]: https://github.com/Plume-org/Plume/compare/0.5.0...0.6.0
[0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0 [0.5.0]: https://github.com/Plume-org/Plume/compare/0.4.0-alpha-4...0.5.0
[0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4 [0.4.0]: https://github.com/Plume-org/Plume/compare/0.3.0-alpha-2...0.4.0-alpha-4
Generated
+1002 -1238
View File
File diff suppressed because it is too large Load Diff
+23 -19
View File
@@ -1,33 +1,37 @@
[package] [package]
authors = ["Plume contributors"] authors = ["Plume contributors"]
name = "plume" name = "plume"
version = "0.7.2" version = "0.6.1-dev"
repository = "https://github.com/Plume-org/Plume" repository = "https://github.com/Plume-org/Plume"
edition = "2018" edition = "2018"
[dependencies] [dependencies]
atom_syndication = "0.11.0" activitypub = "0.1.3"
askama_escape = "0.1"
atom_syndication = "0.6"
clap = "2.33" clap = "2.33"
dotenv = "0.15.0" dotenv = "0.15.0"
gettext = "0.4.0" gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = "0.6.1" gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = "0.1.0" gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
guid-create = "0.2" guid-create = "0.1"
lettre = "0.9.2"
lettre_email = "0.9.2" lettre_email = "0.9.2"
num_cpus = "1.10" num_cpus = "1.10"
rocket = "0.4.6" rocket = "=0.4.6"
rocket_contrib = { version = "0.4.5", features = ["json"] } rocket_contrib = { version = "=0.4.5", features = ["json"] }
rocket_i18n = "0.4.1" rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_json = "1.0.81" serde_json = "1.0"
shrinkwraprs = "0.3.0" shrinkwraprs = "0.2.1"
validator = { version = "0.15", features = ["derive"] } validator = "0.8"
validator_derive = "0.8"
webfinger = "0.4.1" webfinger = "0.4.1"
tracing = "0.1.34" tracing = "0.1.22"
tracing-subscriber = "0.3.10" tracing-subscriber = "0.2.15"
riker = "0.4.2" riker = "0.4.2"
activitystreams = "0.7.0-alpha.18"
[[bin]] [[bin]]
name = "plume" name = "plume"
@@ -39,7 +43,7 @@ version = "0.4"
[dependencies.ctrlc] [dependencies.ctrlc]
features = ["termination"] features = ["termination"]
version = "3.2.2" version = "3.1.2"
[dependencies.diesel] [dependencies.diesel]
features = ["r2d2", "chrono"] features = ["r2d2", "chrono"]
@@ -48,7 +52,7 @@ version = "1.4.5"
[dependencies.multipart] [dependencies.multipart]
default-features = false default-features = false
features = ["server"] features = ["server"]
version = "0.18" version = "0.16"
[dependencies.plume-api] [dependencies.plume-api]
path = "plume-api" path = "plume-api"
@@ -64,8 +68,8 @@ git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "29910f2829e7e590a540da3804336577b48c7b31" rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies] [build-dependencies]
ructe = "0.14.0" ructe = "0.13.0"
rsass = "0.24" rsass = "0.9"
[features] [features]
default = ["postgres"] default = ["postgres"]
-1
View File
@@ -53,4 +53,3 @@ As we want the various spaces related to the project (GitHub, Matrix, Loomio, et
We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost. We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost.
For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/). For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/).
-7
View File
@@ -516,11 +516,4 @@ input:checked ~ .cw-container > .cw-text {
main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * { main .article-meta > *, main .article-meta .comments, main .article-meta > .banner > * {
margin: 0 5%; margin: 0 5%;
} }
.bottom-bar {
align-items: center;
& > div:nth-child(2) {
margin: 0;
}
}
} }
+15 -23
View File
@@ -219,23 +219,15 @@ p.error {
margin: 20px; margin: 20px;
} }
.cover-link {
margin: 0;
&:hover {
opacity: 0.9;
}
}
.cover { .cover {
min-height: 10em; min-height: 10em;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
margin: 0px; margin: 0px;
}
header { &:hover {
display: flex; opacity: 0.9;
}
} }
h3 { h3 {
@@ -244,14 +236,9 @@ p.error {
font-family: $playfair; font-family: $playfair;
font-size: 1.75em; font-size: 1.75em;
font-weight: normal; font-weight: normal;
line-height: 1.10; line-height: 1.75;
display: inline-block;
position: relative;
a { a {
display: block; display: block;
width: 100%;
height: 100%;
padding-block-start: 0.5em;
transition: color 0.1s ease-in; transition: color 0.1s ease-in;
color: $text-color; color: $text-color;
@@ -260,8 +247,7 @@ p.error {
} }
.controls { .controls {
flex-shrink: 0; float: right;
text-align: end;
.button { .button {
margin-top: 0; margin-top: 0;
@@ -275,7 +261,7 @@ p.error {
font-family: $lora; font-family: $lora;
font-size: 1em; font-size: 1em;
line-height: 1.25; line-height: 1.25;
text-align: initial; text-align: left;
overflow: hidden; overflow: hidden;
} }
} }
@@ -479,10 +465,9 @@ figure {
/// Avatars /// Avatars
.avatar { .avatar {
background-position: center !important; background-position: center;
background-size: cover; background-size: cover;
border-radius: 100%; border-radius: 100%;
flex-shrink: 0;
&.small { &.small {
width: 50px; width: 50px;
@@ -507,7 +492,6 @@ figure {
margin: auto $horizontal-margin 2em; margin: auto $horizontal-margin 2em;
overflow: auto; overflow: auto;
display: flex; display: flex;
justify-content: center;
a { a {
display: inline-block; display: inline-block;
@@ -577,6 +561,14 @@ figure {
} }
} }
.bottom-bar {
flex-direction: column;
align-items: center;
& > div {
margin: 0;
}
}
main .article-meta .comments .comment { main .article-meta .comments .comment {
header { header {
flex-direction: column; flex-direction: column;
+7 -13
View File
@@ -41,9 +41,9 @@ fn main() {
.expect("compile templates"); .expect("compile templates");
compile_themes().expect("Theme compilation error"); compile_themes().expect("Theme compilation error");
recursive_copy(&Path::new("assets").join("icons"), Path::new("static")) recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
.expect("Couldn't copy icons"); .expect("Couldn't copy icons");
recursive_copy(&Path::new("assets").join("images"), Path::new("static")) recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
.expect("Couldn't copy images"); .expect("Couldn't copy images");
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory"); create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
@@ -97,12 +97,12 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
.components() .components()
.skip_while(|c| *c != Component::Normal(OsStr::new("themes"))) .skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
.skip(1) .skip(1)
.map(|c| { .filter_map(|c| {
c.as_os_str() c.as_os_str()
.to_str() .to_str()
.unwrap_or_default() .unwrap_or_default()
.split_once('.') .splitn(2, '.')
.map_or(c.as_os_str().to_str().unwrap_or_default(), |x| x.0) .next()
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("-"); .join("-");
@@ -120,14 +120,8 @@ fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
// compile the .scss/.sass file // compile the .scss/.sass file
let mut out = File::create(out.join("theme.css"))?; let mut out = File::create(out.join("theme.css"))?;
out.write_all( out.write_all(
&rsass::compile_scss_path( &rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
path, .expect("SCSS compilation error"),
rsass::output::Format {
style: rsass::output::Style::Compressed,
..rsass::output::Format::default()
},
)
.expect("SCSS compilation error"),
)?; )?;
Ok(()) Ok(())
@@ -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;
@@ -1 +0,0 @@
DROP TABLE email_signups;
@@ -1,9 +0,0 @@
CREATE TABLE email_signups (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
@@ -1,4 +0,0 @@
ALTER TABLE email_blocklist ALTER COLUMN notification_text DROP NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notify_user DROP NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN note DROP NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN email_address DROP NOT NULL;
@@ -1,4 +0,0 @@
ALTER TABLE email_blocklist ALTER COLUMN email_address SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN note SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notify_user SET NOT NULL;
ALTER TABLE email_blocklist ALTER COLUMN notification_text SET NOT NULL;
@@ -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 +0,0 @@
DROP TABLE email_signups;
@@ -1,9 +0,0 @@
CREATE TABLE email_signups (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email VARCHAR NOT NULL,
token VARCHAR NOT NULL,
expiration_date TIMESTAMP NOT NULL
);
CREATE INDEX email_signups_token ON email_signups (token);
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
@@ -1,9 +0,0 @@
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
email_address TEXT UNIQUE,
note TEXT,
notify_user BOOLEAN DEFAULT FALSE,
notification_text TEXT);
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
DROP TABLE email_blocklist;
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
@@ -1,9 +0,0 @@
CREATE TABLE email_blocklist2(id INTEGER PRIMARY KEY,
email_address TEXT UNIQUE NOT NULL,
note TEXT NOT NULL,
notify_user BOOLEAN DEFAULT FALSE NOT NULL,
notification_text TEXT NOT NULL);
INSERT INTO email_blocklist2 SELECT * FROM email_blocklist;
DROP TABLE email_blocklist;
ALTER TABLE email_blocklist2 RENAME TO email_blocklist;
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-api" name = "plume-api"
version = "0.7.2" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-cli" name = "plume-cli"
version = "0.7.2" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@@ -10,8 +10,8 @@ path = "src/main.rs"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
dotenv = "0.15" dotenv = "0.14"
rpassword = "6.0.1" rpassword = "5.0.0"
[dependencies.diesel] [dependencies.diesel]
features = ["r2d2", "chrono"] features = ["r2d2", "chrono"]
+2 -18
View File
@@ -1,6 +1,6 @@
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::*, safe_string::SafeString, Connection}; use plume_models::{instance::NewInstance, Connection};
use std::env; use std::env;
pub fn command<'a, 'b>() -> App<'a, 'b> { pub fn command<'a, 'b>() -> App<'a, 'b> {
@@ -53,21 +53,5 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
.unwrap_or_else(|| String::from("CC-BY-SA")); .unwrap_or_else(|| String::from("CC-BY-SA"));
let open_reg = !args.is_present("private"); let open_reg = !args.is_present("private");
Instance::insert( NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
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");
Instance::cache_local(conn);
Instance::create_local_instance_user(conn).expect("Couldn't save local instance user");
} }
+1 -1
View File
@@ -25,7 +25,7 @@ fn main() {
e => e.map(|_| ()).unwrap(), e => e.map(|_| ()).unwrap(),
} }
let conn = Conn::establish(CONFIG.database_url.as_str()); let conn = Conn::establish(CONFIG.database_url.as_str());
let _ = conn.as_ref().map(Instance::cache_local); let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
match matches.subcommand() { match matches.subcommand() {
("instance", Some(args)) => { ("instance", Some(args)) => {
+14 -22
View File
@@ -1,31 +1,29 @@
[package] [package]
name = "plume-common" name = "plume-common"
version = "0.7.2" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1"
activitystreams-derive = "0.1.1"
activitystreams-traits = "0.1.0"
array_tool = "1.0" array_tool = "1.0"
base64 = "0.13" base64 = "0.10"
heck = "0.4.0" heck = "0.3.0"
hex = "0.4" hex = "0.3"
openssl = "0.10.40" hyper = "0.12.33"
rocket = "0.4.6" openssl = "0.10.22"
reqwest = { version = "0.11.10", features = ["blocking", "json", "socks"] } rocket = "=0.4.6"
reqwest = { version = "0.9", features = ["socks"] }
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0.81" serde_json = "1.0"
shrinkwraprs = "0.3.0" shrinkwraprs = "0.3.0"
syntect = "4.5.0" syntect = "4.5.0"
tokio = "0.1.22"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.34" tracing = "0.1.22"
askama_escape = "0.10.3"
activitystreams = "0.7.0-alpha.18"
activitystreams-ext = "0.1.0-alpha.2"
url = "2.2.2"
flume = "0.10.12"
tokio = { version = "1.18.2", features = ["full"] }
futures = "0.3.21"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@@ -35,9 +33,3 @@ version = "0.4"
default-features = false default-features = false
git = "https://git.joinplu.me/Plume/pulldown-cmark" git = "https://git.joinplu.me/Plume/pulldown-cmark"
branch = "bidi-plume" branch = "bidi-plume"
[dev-dependencies]
assert-json-diff = "2.0.1"
once_cell = "1.10.0"
[features]
+201 -238
View File
@@ -1,7 +1,10 @@
use reqwest;
use std::fmt::Debug; use std::fmt::Debug;
use super::{request, sign::Signer}; use super::{request, sign::Signer};
use reqwest::{
header::{HeaderValue, HOST},
Url,
};
/// Represents an ActivityPub inbox. /// Represents an ActivityPub inbox.
/// ///
@@ -10,51 +13,10 @@ use super::{request, sign::Signer};
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # use activitystreams::{prelude::*, base::Base, actor::Person, activity::{Announce, Create}, object::Note, iri_string::types::IriString}; /// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; /// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy; /// # use plume_common::activity_pub::{inbox::*, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer}};
/// # use plume_common::activity_pub::inbox::*;
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # 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) -> SignResult<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(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<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(|_| SignError())
/// # }
/// # }
/// #
/// # struct User; /// # struct User;
/// # impl FromId<()> for User { /// # impl FromId<()> for User {
/// # type Error = (); /// # type Error = ();
@@ -67,10 +29,6 @@ use super::{request, sign::Signer};
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> { /// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(User) /// # Ok(User)
/// # } /// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # } /// # }
/// # impl AsActor<&()> for User { /// # impl AsActor<&()> for User {
/// # fn get_inbox_url(&self) -> String { /// # fn get_inbox_url(&self) -> String {
@@ -90,10 +48,6 @@ use super::{request, sign::Signer};
/// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> { /// # fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// # Ok(Message) /// # Ok(Message)
/// # } /// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # } /// # }
/// # impl AsObject<User, Create, &()> for Message { /// # impl AsObject<User, Create, &()> for Message {
/// # type Error = (); /// # type Error = ();
@@ -111,19 +65,55 @@ use super::{request, sign::Signer};
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// # } /// # }
/// # struct MySigner {
/// # public_key: String,
/// # private_key: String,
/// # }
/// # /// #
/// # let mut person = Person::new(); /// # impl MySigner {
/// # person.set_id("https://test.ap/actor".parse::<IriString>().unwrap()); /// # fn new() -> Self {
/// # let mut act = Create::new( /// # let (pub_key, priv_key) = gen_keypair();
/// # Base::retract(person).unwrap().into_generic().unwrap(), /// # Self {
/// # Base::retract(Note::new()).unwrap().into_generic().unwrap() /// # public_key: String::from_utf8(pub_key).unwrap(),
/// # ); /// # private_key: String::from_utf8(priv_key).unwrap(),
/// # act.set_id("https://test.ap/activity".parse::<IriString>().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();
/// # let mut person = Person::default();
/// # person.object_props.set_id_string(String::from("https://test.ap/actor")).unwrap();
/// # act.create_props.set_actor_object(person).unwrap();
/// # act.create_props.set_object_object(Note::default()).unwrap();
/// # let activity_json = serde_json::to_value(act).unwrap(); /// # let activity_json = serde_json::to_value(act).unwrap();
/// # /// #
/// # let conn = (); /// # let conn = ();
/// # let sender = MySigner::new();
/// # /// #
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json) /// let result: Result<(), ()> = Inbox::handle(&conn, &sender, activity_json)
/// .with::<User, Announce, Message>(None) /// .with::<User, Announce, Message>(None)
/// .with::<User, Create, Message>(None) /// .with::<User, Create, Message>(None)
/// .done(); /// .done();
@@ -137,9 +127,10 @@ where
/// # Structure /// # Structure
/// ///
/// - the context to be passed to each handler. /// - the context to be passed to each handler.
/// - the sender actor to sign request
/// - the activity /// - the activity
/// - the reason it has not been handled yet /// - the reason it has not been handled yet
NotHandled(&'a C, serde_json::Value, InboxError<E>), NotHandled(&'a C, &'a dyn Signer, serde_json::Value, InboxError<E>),
/// A matching handler have been found but failed /// A matching handler have been found but failed
/// ///
@@ -192,39 +183,46 @@ where
/// ///
/// - `ctx`: the context to pass to each handler /// - `ctx`: the context to pass to each handler
/// - `json`: the JSON representation of the incoming activity /// - `json`: the JSON representation of the incoming activity
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> { pub fn handle(
Inbox::NotHandled(ctx, json, InboxError::NoMatch) 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. /// Registers an handler on this Inbox.
pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Self pub fn with<A, V, M>(self, proxy: Option<&reqwest::Proxy>) -> Inbox<'a, C, E, R>
where where
A: AsActor<&'a C> + FromId<C, Error = E>, A: AsActor<&'a C> + FromId<C, Error = E>,
V: activitystreams::markers::Activity + serde::de::DeserializeOwned, V: activitypub::Activity,
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>, M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
M::Output: Into<R>, M::Output: Into<R>,
{ {
if let Self::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() { if serde_json::from_value::<V>(act.clone()).is_ok() {
let act_clone = act.clone(); let act_clone = act.clone();
let act_id = match act_clone["id"].as_str() { let act_id = match act_clone["id"].as_str() {
Some(x) => x, Some(x) => x,
None => return Self::NotHandled(ctx, act, InboxError::InvalidID), None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
}; };
// Get the actor ID // Get the actor ID
let actor_id = match get_id(act["actor"].clone()) { let actor_id = match get_id(act["actor"].clone()) {
Some(x) => x, Some(x) => x,
None => return Self::NotHandled(ctx, act, InboxError::InvalidActor(None)), None => {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidActor(None))
}
}; };
if Self::is_spoofed_activity(&actor_id, &act) { if Self::is_spoofed_activity(&actor_id, &act) {
return Self::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) // Transform this actor to a model (see FromId for details about the from_id function)
let actor = match A::from_id( let actor = match A::from_id(
ctx, ctx,
sender,
&actor_id, &actor_id,
serde_json::from_value(act["actor"].clone()).ok(), serde_json::from_value(act["actor"].clone()).ok(),
proxy, proxy,
@@ -235,17 +233,25 @@ where
if let Some(json) = json { if let Some(json) = json {
act["actor"] = json; act["actor"] = json;
} }
return Self::NotHandled(ctx, act, InboxError::InvalidActor(Some(e))); return Inbox::NotHandled(
ctx,
sender,
act,
InboxError::InvalidActor(Some(e)),
);
} }
}; };
// Same logic for "object" // Same logic for "object"
let obj_id = match get_id(act["object"].clone()) { let obj_id = match get_id(act["object"].clone()) {
Some(x) => x, Some(x) => x,
None => return Self::NotHandled(ctx, act, InboxError::InvalidObject(None)), None => {
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None))
}
}; };
let obj = match M::from_id( let obj = match M::from_id(
ctx, ctx,
sender,
&obj_id, &obj_id,
serde_json::from_value(act["object"].clone()).ok(), serde_json::from_value(act["object"].clone()).ok(),
proxy, proxy,
@@ -255,19 +261,24 @@ where
if let Some(json) = json { if let Some(json) = json {
act["object"] = json; act["object"] = json;
} }
return Self::NotHandled(ctx, act, InboxError::InvalidObject(Some(e))); return Inbox::NotHandled(
ctx,
sender,
act,
InboxError::InvalidObject(Some(e)),
);
} }
}; };
// Handle the activity // Handle the activity
match obj.activity(ctx, actor, act_id) { match obj.activity(ctx, actor, &act_id) {
Ok(res) => Self::Handled(res.into()), Ok(res) => Inbox::Handled(res.into()),
Err(e) => Self::Failed(e), Err(e) => Inbox::Failed(e),
} }
} else { } else {
// If the Activity type is not matching the expected one for // If the Activity type is not matching the expected one for
// this handler, try with the next one. // this handler, try with the next one.
Self::NotHandled(ctx, act, e) Inbox::NotHandled(ctx, sender, act, e)
} }
} else { } else {
self self
@@ -278,7 +289,7 @@ where
pub fn done(self) -> Result<R, E> { pub fn done(self) -> Result<R, E> {
match self { match self {
Inbox::Handled(res) => Ok(res), Inbox::Handled(res) => Ok(res),
Inbox::NotHandled(_, _, err) => Err(E::from(err)), Inbox::NotHandled(_, _, _, err) => Err(E::from(err)),
Inbox::Failed(err) => Err(err), Inbox::Failed(err) => Err(err),
} }
} }
@@ -333,7 +344,7 @@ pub trait FromId<C>: Sized {
type Error: From<InboxError<Self::Error>> + Debug; type Error: From<InboxError<Self::Error>> + Debug;
/// The ActivityPub object type representing Self /// The ActivityPub object type representing Self
type Object: activitystreams::markers::Object + serde::de::DeserializeOwned; type Object: activitypub::Object;
/// Tries to get an instance of `Self` from an ActivityPub ID. /// Tries to get an instance of `Self` from an ActivityPub ID.
/// ///
@@ -345,6 +356,7 @@ pub trait FromId<C>: Sized {
/// If absent, the ID will be dereferenced. /// If absent, the ID will be dereferenced.
fn from_id( fn from_id(
ctx: &C, ctx: &C,
sender: &dyn Signer,
id: &str, id: &str,
object: Option<Self::Object>, object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>, proxy: Option<&reqwest::Proxy>,
@@ -353,7 +365,7 @@ pub trait FromId<C>: Sized {
Ok(x) => Ok(x), Ok(x) => Ok(x),
_ => match object { _ => match object {
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)), Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?) None => Self::from_activity(ctx, Self::deref(id, sender, proxy.cloned())?)
.map_err(|e| (None, e)), .map_err(|e| (None, e)),
}, },
} }
@@ -362,18 +374,42 @@ pub trait FromId<C>: Sized {
/// Dereferences an ID /// Dereferences an ID
fn deref( fn deref(
id: &str, id: &str,
sender: &dyn Signer,
proxy: Option<reqwest::Proxy>, proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> { ) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
request::get(id, Self::get_sender(), proxy) let mut headers = request::headers();
.map_err(|_| (None, InboxError::DerefError)) let url = Url::parse(&id).map_err(|_| (None, InboxError::InvalidID.into()))?;
.and_then(|r| { if !url.has_host() {
let json: serde_json::Value = r return Err((None, InboxError::InvalidID.into()));
.json() }
.map_err(|_| (None, InboxError::InvalidObject(None)))?; let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"))
serde_json::from_value(json.clone()) .map_err(|_| (None, InboxError::DerefError.into()))?;
.map_err(|_| (Some(json), InboxError::InvalidObject(None))) headers.insert(HOST, host_header_value);
}) if let Some(proxy) = proxy {
.map_err(|(json, e)| (json, e.into())) reqwest::ClientBuilder::new().proxy(proxy)
} else {
reqwest::ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()
.map_err(|_| (None, InboxError::DerefError.into()))?
.get(id)
.headers(headers.clone())
.header(
"Signature",
request::signature(sender, &headers, ("get", url.path(), url.query()))
.map_err(|_| (None, InboxError::DerefError.into()))?,
)
.send()
.map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| {
let json: serde_json::Value = r
.json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?;
serde_json::from_value(json.clone())
.map_err(|_| (Some(json), InboxError::InvalidObject(None)))
})
.map_err(|(json, e)| (json, e.into()))
} }
/// Builds a `Self` from its ActivityPub representation /// Builds a `Self` from its ActivityPub representation
@@ -381,8 +417,6 @@ pub trait FromId<C>: Sized {
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database) /// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>; fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
fn get_sender() -> &'static dyn Signer;
} }
/// Should be implemented by anything representing an ActivityPub actor. /// Should be implemented by anything representing an ActivityPub actor.
@@ -418,51 +452,9 @@ pub trait AsActor<C> {
/// representing the Note by a Message type, without any specific context. /// representing the Note by a Message type, without any specific context.
/// ///
/// ```rust /// ```rust
/// # use activitystreams::{prelude::*, activity::Create, actor::Person, object::Note}; /// # extern crate activitypub;
/// # use activitypub::{activity::Create, actor::Person, object::Note};
/// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId}; /// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId};
/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer};
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
/// # use once_cell::sync::Lazy;
/// #
/// # static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
/// #
/// # 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) -> SignResult<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(|_| SignError())
/// # }
/// #
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult<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(|_| SignError())
/// # }
/// # }
/// #
/// # struct Account; /// # struct Account;
/// # impl FromId<()> for Account { /// # impl FromId<()> for Account {
/// # type Error = (); /// # type Error = ();
@@ -475,10 +467,6 @@ pub trait AsActor<C> {
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> { /// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
/// # Ok(Account) /// # Ok(Account)
/// # } /// # }
/// #
/// # fn get_sender() -> &'static dyn Signer {
/// # &*MY_SIGNER
/// # }
/// # } /// # }
/// # impl AsActor<()> for Account { /// # impl AsActor<()> for Account {
/// # fn get_inbox_url(&self) -> String { /// # fn get_inbox_url(&self) -> String {
@@ -500,14 +488,7 @@ pub trait AsActor<C> {
/// } /// }
/// ///
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> { /// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
/// Ok(Message { /// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
/// text: obj.content()
/// .and_then(|content| content.to_owned().single_xsd_string()).ok_or(())?
/// })
/// }
///
/// fn get_sender() -> &'static dyn Signer {
/// &*MY_SIGNER
/// } /// }
/// } /// }
/// ///
@@ -523,7 +504,7 @@ pub trait AsActor<C> {
/// ``` /// ```
pub trait AsObject<A, V, C> pub trait AsObject<A, V, C>
where where
V: activitystreams::markers::Activity, V: activitypub::Activity,
{ {
/// What kind of error is returned when something fails /// What kind of error is returned when something fails
type Error; type Error;
@@ -547,74 +528,22 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::sign::{gen_keypair, Error as SignatureError, Result as SignatureResult};
use super::*; use super::*;
use crate::activity_pub::sign::{ use activitypub::{activity::*, actor::Person, object::Note};
gen_keypair, Error as SignError, Result as SignResult, Signer,
};
use activitystreams::{
activity::{Announce, Create, Delete, Like},
actor::Person,
base::Base,
object::Note,
prelude::*,
};
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
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) -> SignResult<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(|_| SignError())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<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(|_| SignError())
}
}
struct MyActor; struct MyActor;
impl FromId<()> for MyActor { impl FromId<()> for MyActor {
type Error = (); type Error = ();
type Object = Person; type Object = Person;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> { fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(Self) Ok(MyActor)
} }
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Ok(Self) Ok(MyActor)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
} }
} }
@@ -634,15 +563,11 @@ mod tests {
type Object = Note; type Object = Note;
fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> { fn from_db(_: &(), _id: &str) -> Result<Self, Self::Error> {
Ok(Self) Ok(MyObject)
} }
fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Note) -> Result<Self, Self::Error> {
Ok(Self) Ok(MyObject)
}
fn get_sender() -> &'static dyn Signer {
&*MY_SIGNER
} }
} }
impl AsObject<MyActor, Create, &()> for MyObject { impl AsObject<MyActor, Create, &()> for MyObject {
@@ -686,22 +611,65 @@ mod tests {
} }
fn build_create() -> Create { fn build_create() -> Create {
let mut person = Person::new(); let mut act = Create::default();
person.set_id("https://test.ap/actor".parse().unwrap()); act.object_props
let mut note = Note::new(); .set_id_string(String::from("https://test.ap/activity"))
note.set_id("https://test.ap/note".parse().unwrap()); .unwrap();
let mut act = Create::new( let mut person = Person::default();
Base::retract(person).unwrap().into_generic().unwrap(), person
Base::retract(note).unwrap().into_generic().unwrap(), .object_props
); .set_id_string(String::from("https://test.ap/actor"))
act.set_id("https://test.ap/activity".parse().unwrap()); .unwrap();
act.create_props.set_actor_object(person).unwrap();
let mut note = Note::default();
note.object_props
.set_id_string(String::from("https://test.ap/note"))
.unwrap();
act.create_props.set_object_object(note).unwrap();
act act
} }
struct MySigner {
public_key: String,
private_key: String,
}
impl MySigner {
fn new() -> Self {
let (pub_key, priv_key) = gen_keypair();
Self {
public_key: String::from_utf8(pub_key).unwrap(),
private_key: String::from_utf8(priv_key).unwrap(),
}
}
}
impl Signer for MySigner {
fn get_key_id(&self) -> String {
"mysigner".into()
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| SignatureError())
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| SignatureError())
}
}
#[test] #[test]
fn test_inbox_basic() { fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act) let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>(None)
.done(); .done();
assert!(res.is_ok()); assert!(res.is_ok());
@@ -710,7 +678,7 @@ mod tests {
#[test] #[test]
fn test_inbox_multi_handlers() { fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act) let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
.with::<MyActor, Announce, MyObject>(None) .with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Delete, MyObject>(None) .with::<MyActor, Delete, MyObject>(None)
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>(None)
@@ -723,7 +691,7 @@ mod tests {
fn test_inbox_failure() { fn test_inbox_failure() {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox // Create is not handled by this inbox
let res: Result<(), ()> = Inbox::handle(&(), act) let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
.with::<MyActor, Announce, MyObject>(None) .with::<MyActor, Announce, MyObject>(None)
.with::<MyActor, Like, MyObject>(None) .with::<MyActor, Like, MyObject>(None)
.done(); .done();
@@ -731,16 +699,6 @@ mod tests {
} }
struct FailingActor; struct FailingActor;
impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String {
String::from("https://test.ap/failing-actor/inbox")
}
fn is_local(&self) -> bool {
false
}
}
impl FromId<()> for FailingActor { impl FromId<()> for FailingActor {
type Error = (); type Error = ();
type Object = Person; type Object = Person;
@@ -749,12 +707,17 @@ mod tests {
Err(()) Err(())
} }
fn from_activity(_: &(), _obj: Self::Object) -> Result<Self, Self::Error> { fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
Err(()) Err(())
} }
}
impl AsActor<&()> for FailingActor {
fn get_inbox_url(&self) -> String {
String::from("https://test.ap/failing-actor/inbox")
}
fn get_sender() -> &'static dyn Signer { fn is_local(&self) -> bool {
&*MY_SIGNER false
} }
} }
@@ -777,12 +740,12 @@ mod tests {
fn test_inbox_actor_failure() { fn test_inbox_actor_failure() {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
let res: Result<(), ()> = Inbox::handle(&(), act.clone()) let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
.with::<FailingActor, Create, MyObject>(None) .with::<FailingActor, Create, MyObject>(None)
.done(); .done();
assert!(res.is_err()); assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), act.clone()) let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
.with::<FailingActor, Create, MyObject>(None) .with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>(None)
.done(); .done();
+87 -558
View File
@@ -1,27 +1,13 @@
use activitystreams::{ use activitypub::{Activity, Link, Object};
actor::{ApActor, Group, Person},
base::{AnyBase, Base, Extends},
iri_string::types::IriString,
kind,
markers::{self, Activity},
object::{ApObject, Article, Object},
primitives::{AnyString, OneOrMany},
unparsed::UnparsedMutExt,
};
use activitystreams_ext::{Ext1, Ext2, UnparsedExtension};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use futures::future::join_all; use reqwest::{header::HeaderValue, r#async::ClientBuilder, Url};
use reqwest::{header::HeaderValue, ClientBuilder, RequestBuilder, Url};
use rocket::{ use rocket::{
http::Status, http::Status,
request::{FromRequest, Request}, request::{FromRequest, Request},
response::{Responder, Response}, response::{Responder, Response},
Outcome, Outcome,
}; };
use tokio::{ use tokio::prelude::*;
runtime,
time::{sleep, Duration},
};
use tracing::{debug, warn}; use tracing::{debug, warn};
use self::sign::Signable; use self::sign::Signable;
@@ -38,8 +24,8 @@ pub const AP_CONTENT_TYPE: &str =
pub fn ap_accept_header() -> Vec<&'static str> { pub fn ap_accept_header() -> Vec<&'static str> {
vec![ vec![
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "application/ld+json; profile=\"https://w3.org/ns/activitystreams\"",
"application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"", "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"",
"application/activity+json", "application/activity+json",
"application/ld+json", "application/ld+json",
] ]
@@ -77,7 +63,7 @@ impl<T> ActivityStream<T> {
} }
} }
impl<'r, O: serde::Serialize> Responder<'r> for ActivityStream<O> { impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> { fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context(); json["@context"] = context();
@@ -101,16 +87,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
.map(|header| { .map(|header| {
header header
.split(',') .split(',')
.map(|ct| { .map(|ct| match ct.trim() {
match ct.trim() {
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise // bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" "application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
| "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"" | "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
| "application/activity+json" | "application/activity+json"
| "application/ld+json" => Outcome::Success(ApRequest), | "application/ld+json" => Outcome::Success(ApRequest),
"text/html" => Outcome::Forward(true), "text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false), _ => Outcome::Forward(false),
}
}) })
.fold(Outcome::Forward(false), |out, ct| { .fold(Outcome::Forward(false), |out, ct| {
if out.clone().forwarded().unwrap_or_else(|| out.is_success()) { if out.clone().forwarded().unwrap_or_else(|| out.is_success()) {
@@ -124,11 +108,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
.unwrap_or(Outcome::Forward(())) .unwrap_or(Outcome::Forward(()))
} }
} }
pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>) pub fn broadcast<S, A, T, C>(sender: &S, act: A, to: Vec<T>, proxy: Option<reqwest::Proxy>)
where where
S: sign::Signer, S: sign::Signer,
A: Activity + serde::Serialize, A: Activity,
T: inbox::AsActor<C>, T: inbox::AsActor<C>,
{ {
let boxes = to let boxes = to
@@ -147,79 +130,59 @@ where
.sign(sender) .sign(sender)
.expect("activity_pub::broadcast: signature error"); .expect("activity_pub::broadcast: signature error");
let client = if let Some(proxy) = proxy { let mut rt = tokio::runtime::current_thread::Runtime::new()
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Error while initializing tokio runtime for federation"); .expect("Error while initializing tokio runtime for federation");
rt.block_on(async { for inbox in boxes {
// TODO: should be determined dependent on database connections because let body = signed.to_string();
// after broadcasting, target instance sends request to this instance, let mut headers = request::headers();
// and Plume accesses database at that time. let url = Url::parse(&inbox);
let capacity = 6; if url.is_err() {
let (tx, rx) = flume::bounded::<RequestBuilder>(capacity); warn!("Inbox is invalid URL: {:?}", &inbox);
let mut handles = Vec::with_capacity(capacity); continue;
for _ in 0..capacity {
let rx = rx.clone();
let handle = rt.spawn(async move {
while let Ok(request_builder) = rx.recv_async().await {
// After broadcasting, target instance sends request to this instance.
// Sleep here in order to reduce requests at once
sleep(Duration::from_millis(500)).await;
let _ = request_builder
.send()
.await
.map(move |r| {
if r.status().is_success() {
debug!("Successfully sent activity to inbox ({})", &r.url());
} else {
warn!("Error while sending to inbox ({:?})", &r)
}
debug!("Response: \"{:?}\"\n", r);
})
.map_err(|e| warn!("Error while sending to inbox ({:?})", e));
}
});
handles.push(handle);
} }
for inbox in boxes { let url = url.unwrap();
let body = signed.to_string(); if !url.has_host() {
let mut headers = request::headers(); warn!("Inbox doesn't have host: {:?}", &inbox);
let url = Url::parse(&inbox); continue;
if url.is_err() { };
warn!("Inbox is invalid URL: {:?}", &inbox); let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"));
continue; if host_header_value.is_err() {
warn!("Header value is invalid: {:?}", url.host_str());
continue;
}
headers.insert("Host", host_header_value.unwrap());
headers.insert("Digest", request::Digest::digest(&body));
rt.spawn(
if let Some(proxy) = proxy.clone() {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
} }
let url = url.unwrap(); .connect_timeout(std::time::Duration::from_secs(5))
if !url.has_host() { .build()
warn!("Inbox doesn't have host: {:?}", &inbox); .expect("Can't build client")
continue; .post(&inbox)
}; .headers(headers.clone())
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable")); .header(
if host_header_value.is_err() {
warn!("Header value is invalid: {:?}", url.host_str());
continue;
}
headers.insert("Host", host_header_value.unwrap());
headers.insert("Digest", request::Digest::digest(&body));
headers.insert(
"Signature", "Signature",
request::signature(sender, &headers, ("post", url.path(), url.query())) request::signature(sender, &headers, ("post", url.path(), url.query()))
.expect("activity_pub::broadcast: request signature error"), .expect("activity_pub::broadcast: request signature error"),
); )
let request_builder = client.post(&inbox).headers(headers.clone()).body(body); .body(body)
let _ = tx.send_async(request_builder).await; .send()
} .and_then(move |r| {
drop(tx); if r.status().is_success() {
join_all(handles).await; debug!("Successfully sent activity to inbox ({})", &inbox);
}); } else {
warn!("Error while sending to inbox ({:?})", &r)
}
r.into_body().concat2()
})
.map(move |response| debug!("Response: \"{:?}\"\n", response))
.map_err(|e| warn!("Error while sending to inbox ({:?})", e)),
);
}
rt.run().unwrap();
} }
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)] #[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
@@ -241,193 +204,46 @@ pub trait IntoId {
fn into_id(self) -> Id; fn into_id(self) -> Id;
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] impl Link for Id {}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ApSignature { pub struct ApSignature {
pub public_key: PublicKey, #[activitystreams(concrete(PublicKey), functional)]
pub public_key: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PublicKey { pub struct PublicKey {
pub id: IriString, #[activitystreams(concrete(String), functional)]
pub owner: IriString, pub id: Option<serde_json::Value>,
pub public_key_pem: String,
#[activitystreams(concrete(String), functional)]
pub owner: Option<serde_json::Value>,
#[activitystreams(concrete(String), functional)]
pub public_key_pem: Option<serde_json::Value>,
} }
impl<U> UnparsedExtension<U> for ApSignature #[derive(Clone, Debug, Default, UnitString)]
where #[activitystreams(Hashtag)]
U: UnparsedMutExt, pub struct HashtagType;
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> { #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
Ok(ApSignature {
public_key: unparsed_mut.remove("publicKey")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("publicKey", self.public_key)?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SourceProperty {
pub source: Source,
}
impl<U> UnparsedExtension<U> for SourceProperty
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(SourceProperty {
source: unparsed_mut.remove("source")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("source", self.source)?;
Ok(())
}
}
pub type CustomPerson = Ext1<ApActor<Person>, ApSignature>;
pub type CustomGroup = Ext2<ApActor<Group>, ApSignature, SourceProperty>;
kind!(HashtagType, Hashtag);
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct Hashtag { pub struct Hashtag {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")]
pub href: Option<IriString>, kind: HashtagType,
#[serde(skip_serializing_if = "Option::is_none")] #[activitystreams(concrete(String), functional)]
pub name: Option<AnyString>, pub href: Option<serde_json::Value>,
#[serde(flatten)] #[activitystreams(concrete(String), functional)]
inner: Object<HashtagType>, pub name: Option<serde_json::Value>,
} }
impl Hashtag { #[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub fn new() -> Self {
Self {
href: None,
name: None,
inner: Object::new(),
}
}
pub fn extending(mut inner: Object<HashtagType>) -> Result<Self, serde_json::Error> {
let href = inner.remove("href")?;
let name = inner.remove("name")?;
Ok(Self { href, name, inner })
}
pub fn retracting(self) -> Result<Object<HashtagType>, serde_json::Error> {
let Self {
href,
name,
mut inner,
} = self;
inner.insert("href", href)?;
inner.insert("name", name)?;
Ok(inner)
}
}
pub trait AsHashtag: markers::Object {
fn hashtag_ref(&self) -> &Hashtag;
fn hashtag_mut(&mut self) -> &mut Hashtag;
}
pub trait HashtagExt: AsHashtag {
fn href(&self) -> Option<&IriString> {
self.hashtag_ref().href.as_ref()
}
fn set_href<T>(&mut self, href: T) -> &mut Self
where
T: Into<IriString>,
{
self.hashtag_mut().href = Some(href.into());
self
}
fn take_href(&mut self) -> Option<IriString> {
self.hashtag_mut().href.take()
}
fn delete_href(&mut self) -> &mut Self {
self.hashtag_mut().href = None;
self
}
fn name(&self) -> Option<&AnyString> {
self.hashtag_ref().name.as_ref()
}
fn set_name<T>(&mut self, name: T) -> &mut Self
where
T: Into<AnyString>,
{
self.hashtag_mut().name = Some(name.into());
self
}
fn take_name(&mut self) -> Option<AnyString> {
self.hashtag_mut().name.take()
}
fn delete_name(&mut self) -> &mut Self {
self.hashtag_mut().name = None;
self
}
}
impl Default for Hashtag {
fn default() -> Self {
Self::new()
}
}
impl AsHashtag for Hashtag {
fn hashtag_ref(&self) -> &Self {
self
}
fn hashtag_mut(&mut self) -> &mut Self {
self
}
}
impl Extends<HashtagType> for Hashtag {
type Error = serde_json::Error;
fn extends(base: Base<HashtagType>) -> Result<Self, Self::Error> {
let inner = Object::extends(base)?;
Self::extending(inner)
}
fn retracts(self) -> Result<Base<HashtagType>, Self::Error> {
let inner = self.retracting()?;
inner.retracts()
}
}
impl markers::Base for Hashtag {}
impl markers::Object for Hashtag {}
impl<T> HashtagExt for T where T: AsHashtag {}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Source { pub struct Source {
pub media_type: String, pub media_type: String,
@@ -435,300 +251,13 @@ pub struct Source {
pub content: String, pub content: String,
} }
impl<U> UnparsedExtension<U> for Source impl Object for Source {}
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> { #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
Ok(Source {
content: unparsed_mut.remove("content")?,
media_type: unparsed_mut.remove("mediaType")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("content", self.content)?;
unparsed_mut.insert("mediaType", self.media_type)?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Licensed { pub struct Licensed {
pub license: Option<String>, #[activitystreams(concrete(String), functional)]
pub license: Option<serde_json::Value>,
} }
impl<U> UnparsedExtension<U> for Licensed impl Object for Licensed {}
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(Licensed {
license: unparsed_mut.remove("license")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("license", self.license)?;
Ok(())
}
}
pub type LicensedArticle = Ext1<ApObject<Article>, Licensed>;
pub trait ToAsString {
fn to_as_string(&self) -> Option<String>;
}
impl ToAsString for OneOrMany<&AnyString> {
fn to_as_string(&self) -> Option<String> {
self.as_as_str().map(|s| s.to_string())
}
}
trait AsAsStr {
fn as_as_str(&self) -> Option<&str>;
}
impl AsAsStr for OneOrMany<&AnyString> {
fn as_as_str(&self) -> Option<&str> {
self.iter().next().map(|prop| prop.as_str())
}
}
pub trait ToAsUri {
fn to_as_uri(&self) -> Option<String>;
}
impl ToAsUri for OneOrMany<AnyBase> {
fn to_as_uri(&self) -> Option<String> {
self.iter()
.next()
.and_then(|prop| prop.as_xsd_any_uri().map(|uri| uri.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use activitystreams::{
activity::{ActorAndObjectRef, Create},
object::kind::ArticleType,
};
use assert_json_diff::assert_json_eq;
use serde_json::{from_str, json, to_value};
#[test]
fn se_ap_signature() {
let ap_signature = ApSignature {
public_key: PublicKey {
id: "https://example.com/pubkey".parse().unwrap(),
owner: "https://example.com/owner".parse().unwrap(),
public_key_pem: "pubKeyPem".into(),
},
};
let expected = json!({
"publicKey": {
"id": "https://example.com/pubkey",
"owner": "https://example.com/owner",
"publicKeyPem": "pubKeyPem"
}
});
assert_json_eq!(to_value(ap_signature).unwrap(), expected);
}
#[test]
fn de_ap_signature() {
let value: ApSignature = from_str(
r#"
{
"publicKey": {
"id": "https://example.com/",
"owner": "https://example.com/",
"publicKeyPem": ""
}
}
"#,
)
.unwrap();
let expected = ApSignature {
public_key: PublicKey {
id: "https://example.com/".parse().unwrap(),
owner: "https://example.com/".parse().unwrap(),
public_key_pem: "".into(),
},
};
assert_eq!(value, expected);
}
#[test]
fn se_custom_person() {
let actor = ApActor::new("https://example.com/inbox".parse().unwrap(), Person::new());
let person = CustomPerson::new(
actor,
ApSignature {
public_key: PublicKey {
id: "https://example.com/pubkey".parse().unwrap(),
owner: "https://example.com/owner".parse().unwrap(),
public_key_pem: "pubKeyPem".into(),
},
},
);
let expected = json!({
"inbox": "https://example.com/inbox",
"type": "Person",
"publicKey": {
"id": "https://example.com/pubkey",
"owner": "https://example.com/owner",
"publicKeyPem": "pubKeyPem"
}
});
assert_eq!(to_value(person).unwrap(), expected);
}
#[test]
fn de_custom_group() {
let group = CustomGroup::new(
ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()),
ApSignature {
public_key: PublicKey {
id: "https://example.com/pubkey".parse().unwrap(),
owner: "https://example.com/owner".parse().unwrap(),
public_key_pem: "pubKeyPem".into(),
},
},
SourceProperty {
source: Source {
content: String::from("This is a *custom* group."),
media_type: String::from("text/markdown"),
},
},
);
let expected = json!({
"inbox": "https://example.com/inbox",
"type": "Group",
"publicKey": {
"id": "https://example.com/pubkey",
"owner": "https://example.com/owner",
"publicKeyPem": "pubKeyPem"
},
"source": {
"content": "This is a *custom* group.",
"mediaType": "text/markdown"
}
});
assert_eq!(to_value(group).unwrap(), expected);
}
#[test]
fn se_licensed_article() {
let object = ApObject::new(Article::new());
let licensed_article = LicensedArticle::new(
object,
Licensed {
license: Some("CC-0".into()),
},
);
let expected = json!({
"type": "Article",
"license": "CC-0",
});
assert_json_eq!(to_value(licensed_article).unwrap(), expected);
}
#[test]
fn de_licensed_article() {
let value: LicensedArticle = from_str(
r#"
{
"type": "Article",
"id": "https://plu.me/~/Blog/my-article",
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
"content": "Hello.",
"name": "My Article",
"summary": "Bye.",
"source": {
"content": "Hello.",
"mediaType": "text/markdown"
},
"published": "2014-12-12T12:12:12Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"license": "CC-0"
}
"#,
)
.unwrap();
let expected = json!({
"type": "Article",
"id": "https://plu.me/~/Blog/my-article",
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
"content": "Hello.",
"name": "My Article",
"summary": "Bye.",
"source": {
"content": "Hello.",
"mediaType": "text/markdown"
},
"published": "2014-12-12T12:12:12Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"license": "CC-0"
});
assert_eq!(to_value(value).unwrap(), expected);
}
#[test]
fn de_create_with_licensed_article() {
let create: Create = from_str(
r#"
{
"id": "https://plu.me/~/Blog/my-article",
"type": "Create",
"actor": "https://plu.me/@/Admin",
"to": "https://www.w3.org/ns/activitystreams#Public",
"object": {
"type": "Article",
"id": "https://plu.me/~/Blog/my-article",
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
"content": "Hello.",
"name": "My Article",
"summary": "Bye.",
"source": {
"content": "Hello.",
"mediaType": "text/markdown"
},
"published": "2014-12-12T12:12:12Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"license": "CC-0"
}
}
"#,
)
.unwrap();
let base = create.object_field_ref().as_single_base().unwrap();
let any_base = AnyBase::from_base(base.clone());
let value = any_base.extend::<LicensedArticle, ArticleType>().unwrap();
let expected = json!({
"type": "Article",
"id": "https://plu.me/~/Blog/my-article",
"attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
"content": "Hello.",
"name": "My Article",
"summary": "Bye.",
"source": {
"content": "Hello.",
"mediaType": "text/markdown"
},
"published": "2014-12-12T12:12:12Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"license": "CC-0"
});
assert_eq!(to_value(value).unwrap(), expected);
}
}
+8 -55
View File
@@ -1,12 +1,6 @@
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest}; use openssl::hash::{Hasher, MessageDigest};
use reqwest::{ use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
blocking::{ClientBuilder, Response},
header::{
HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT,
},
Proxy, Url,
};
use std::ops::Deref; use std::ops::Deref;
use std::time::SystemTime; use std::time::SystemTime;
use tracing::warn; use tracing::warn;
@@ -19,24 +13,6 @@ const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)] #[derive(Debug)]
pub struct Error(); pub struct Error();
impl From<url::ParseError> for Error {
fn from(_err: url::ParseError) -> Self {
Error()
}
}
impl From<InvalidHeaderValue> for Error {
fn from(_err: InvalidHeaderValue) -> Self {
Error()
}
}
impl From<reqwest::Error> for Error {
fn from(_err: reqwest::Error) -> Self {
Error()
}
}
pub struct Digest(String); pub struct Digest(String);
impl Digest { impl Digest {
@@ -188,35 +164,12 @@ pub fn signature(
)).map_err(|_| Error()) )).map_err(|_| Error())
} }
pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option<Proxy>) -> Result<Response, Error> {
let mut headers = headers();
let url = Url::parse(url_str)?;
if !url.has_host() {
return Err(Error());
}
let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?;
headers.insert(HOST, host_header_value);
if let Some(proxy) = proxy {
ClientBuilder::new().proxy(proxy)
} else {
ClientBuilder::new()
}
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url_str)
.headers(headers.clone())
.header(
"Signature",
signature(sender, &headers, ("get", url.path(), url.query()))?,
)
.send()
.map_err(|_| Error())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::signature; use super::signature;
use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer}; use crate::activity_pub::sign::{
gen_keypair, Error as SignatureError, Result as SignatureResult, Signer,
};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
@@ -240,20 +193,20 @@ mod tests {
"mysigner".into() "mysigner".into()
} }
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> { 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()) let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap(); .unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).unwrap(); signer.update(to_sign.as_bytes()).unwrap();
signer.sign_to_vec().map_err(|_| Error()) signer.sign_to_vec().map_err(|_| SignatureError())
} }
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()).unwrap()) let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap(); .unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap(); verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| Error()) verifier.verify(&signature).map_err(|_| SignatureError())
} }
} }
+1 -1
View File
@@ -187,7 +187,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
} }
let digest = all_headers.get_one("digest").unwrap_or(""); let digest = all_headers.get_one("digest").unwrap_or("");
let digest = request::Digest::from_header(digest); let digest = request::Digest::from_header(digest);
if !digest.map(|d| d.verify_header(data)).unwrap_or(false) { if !digest.map(|d| d.verify_header(&data)).unwrap_or(false) {
// signature was valid, but body content does not match its digest // signature was valid, but body content does not match its digest
return SignatureValidity::Invalid; return SignatureValidity::Invalid;
} }
Regular → Executable
+2
View File
@@ -1,5 +1,7 @@
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#[macro_use]
extern crate activitystreams_derive;
#[macro_use] #[macro_use]
extern crate shrinkwraprs; extern crate shrinkwraprs;
#[macro_use] #[macro_use]
+26 -13
View File
@@ -1,8 +1,11 @@
use heck::ToUpperCamelCase; use heck::CamelCase;
use openssl::rand::rand_bytes; use openssl::rand::rand_bytes;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag}; use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
use regex_syntax::is_word_character; use regex_syntax::is_word_character;
use rocket::http::uri::Uri; use rocket::{
http::uri::Uri,
response::{Flash, Redirect},
};
use std::collections::HashSet; use std::collections::HashSet;
use syntect::html::{ClassStyle, ClassedHTMLGenerator}; use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
@@ -18,7 +21,7 @@ pub fn random_hex() -> String {
/// Remove non alphanumeric characters and CamelCase a string /// Remove non alphanumeric characters and CamelCase a string
pub fn make_actor_id(name: &str) -> String { pub fn make_actor_id(name: &str) -> String {
name.to_upper_camel_case() name.to_camel_case()
.chars() .chars()
.filter(|c| c.is_alphanumeric()) .filter(|c| c.is_alphanumeric())
.collect() .collect()
@@ -77,6 +80,19 @@ pub fn iri_percent_encode_seg_char(c: char) -> String {
} }
} }
/**
* Redirects to the login page with a given message.
*
* Note that the message should be translated before passed to this function.
*/
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
Flash::new(
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
"callback",
url.into().to_string(),
)
}
#[derive(Debug)] #[derive(Debug)]
enum State { enum State {
Mention, Mention,
@@ -125,13 +141,13 @@ fn highlight_code<'a>(
unreachable!(); unreachable!();
}; };
let syntax_set = SyntaxSet::load_defaults_newlines(); let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_token(lang).unwrap_or_else(|| { let syntax = syntax_set.find_syntax_by_token(&lang).unwrap_or_else(|| {
syntax_set syntax_set
.find_syntax_by_name(lang) .find_syntax_by_name(&lang)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text()) .unwrap_or_else(|| syntax_set.find_syntax_plain_text())
}); });
let mut html = ClassedHTMLGenerator::new_with_class_style( let mut html = ClassedHTMLGenerator::new_with_class_style(
syntax, &syntax,
&syntax_set, &syntax_set,
ClassStyle::Spaced, ClassStyle::Spaced,
); );
@@ -271,7 +287,7 @@ pub fn md_to_html<'a>(
media_processor: Option<MediaProcessor<'a>>, media_processor: Option<MediaProcessor<'a>>,
) -> (String, HashSet<String>, HashSet<String>) { ) -> (String, HashSet<String>, HashSet<String>) {
let base_url = if let Some(base_url) = base_url { let base_url = if let Some(base_url) = base_url {
format!("https://{}/", base_url) format!("//{}/", base_url)
} else { } else {
"/".to_owned() "/".to_owned()
}; };
@@ -318,15 +334,16 @@ pub fn md_to_html<'a>(
text_acc.push(c) text_acc.push(c)
} }
let mention = text_acc; let mention = text_acc;
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
let link = Tag::Link( let link = Tag::Link(
LinkType::Inline, LinkType::Inline,
format!("{}@/{}/", base_url, &mention).into(), format!("{}@/{}/", base_url, &mention).into(),
mention.clone().into(), short_mention.to_owned().into(),
); );
mentions.push(mention.clone()); mentions.push(mention.clone());
events.push(Event::Start(link.clone())); events.push(Event::Start(link.clone()));
events.push(Event::Text(format!("@{}", &mention).into())); events.push(Event::Text(format!("@{}", &short_mention).into()));
events.push(Event::End(link)); events.push(Event::End(link));
( (
@@ -450,10 +467,6 @@ pub fn md_to_html<'a>(
(buf, mentions.collect(), hashtags.collect()) (buf, mentions.collect(), hashtags.collect())
} }
pub fn escape(string: &str) -> askama_escape::Escaped<askama_escape::Html> {
askama_escape::escape(string, askama_escape::Html)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+7 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-front" name = "plume-front"
version = "0.7.2" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
@@ -8,19 +8,19 @@ edition = "2018"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
gettext = "0.4.0" gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
gettext-macros = "0.6.1" gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
gettext-utils = "0.1.0" gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
lazy_static = "1.3" lazy_static = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
wasm-bindgen = "0.2.80" wasm-bindgen = "0.2.70"
js-sys = "0.3.57" js-sys = "0.3.47"
serde_derive = "1.0.123" serde_derive = "1.0.123"
console_error_panic_hook = "0.1.6" console_error_panic_hook = "0.1.6"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.57" version = "0.3.47"
features = [ features = [
'console', 'console',
'ClipboardEvent', 'ClipboardEvent',
+15 -23
View File
@@ -54,6 +54,11 @@ pub enum EditorError {
DOMError, DOMError,
} }
impl From<std::option::NoneError> for EditorError {
fn from(_: std::option::NoneError) -> Self {
EditorError::NoneError
}
}
const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000; const AUTOSAVE_DEBOUNCE_TIME: i32 = 5000;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct AutosaveInformation { struct AutosaveInformation {
@@ -193,7 +198,7 @@ fn clear_autosave() {
.unwrap() .unwrap()
.remove_item(&get_autosave_id()) .remove_item(&get_autosave_id())
.unwrap(); .unwrap();
console::log_1(&format!("Saved to {}", &get_autosave_id()).into()); console::log_1(&&format!("Saved to {}", &get_autosave_id()).into());
} }
type TimeoutHandle = i32; type TimeoutHandle = i32;
lazy_static! { lazy_static! {
@@ -361,9 +366,7 @@ fn init_editor() -> Result<(), EditorError> {
return Ok(()); return Ok(());
} }
let old_ed = old_ed.unwrap(); let old_ed = old_ed.unwrap();
let old_title = document() let old_title = document().get_element_by_id("plume-editor-title")?;
.get_element_by_id("plume-editor-title")
.ok_or(EditorError::NoneError)?;
old_ed old_ed
.dyn_ref::<HtmlElement>() .dyn_ref::<HtmlElement>()
.unwrap() .unwrap()
@@ -397,9 +400,7 @@ fn init_editor() -> Result<(), EditorError> {
content_val.clone(), content_val.clone(),
false, false,
)?; )?;
if !content_val.is_empty() { content.set_inner_html(&content_val);
content.set_inner_html(&content_val);
}
// character counter // character counter
let character_counter = Closure::wrap(Box::new(mv!(content => move |_| { let character_counter = Closure::wrap(Box::new(mv!(content => move |_| {
@@ -433,8 +434,7 @@ fn init_editor() -> Result<(), EditorError> {
bg.class_list().add_1("show").unwrap(); bg.class_list().add_1("show").unwrap();
})) as Box<dyn FnMut(MouseEvent)>); })) as Box<dyn FnMut(MouseEvent)>);
document() document()
.get_element_by_id("publish") .get_element_by_id("publish")?
.ok_or(EditorError::NoneError)?
.add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref()) .add_event_listener_with_callback("click", show_popup.as_ref().unchecked_ref())
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
show_popup.forget(); show_popup.forget();
@@ -528,14 +528,8 @@ fn init_popup(
cover_label cover_label
.set_attribute("for", "cover") .set_attribute("for", "cover")
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
let cover = document let cover = document.get_element_by_id("cover")?;
.get_element_by_id("cover") cover.parent_element()?.remove_child(&cover).ok();
.ok_or(EditorError::NoneError)?;
cover
.parent_element()
.ok_or(EditorError::NoneError)?
.remove_child(&cover)
.ok();
popup popup
.append_child(&cover_label) .append_child(&cover_label)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
@@ -560,7 +554,7 @@ fn init_popup(
draft.set_checked(draft_checkbox.checked()); draft.set_checked(draft_checkbox.checked());
draft_label draft_label
.append_child(draft) .append_child(&draft)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
draft_label draft_label
.append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft"))) .append_child(&document.create_text_node(&i18n!(CATALOG, "This is a draft")))
@@ -626,12 +620,11 @@ fn init_popup(
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
callback.forget(); callback.forget();
popup popup
.append_child(button) .append_child(&button)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
document document
.body() .body()?
.ok_or(EditorError::NoneError)?
.append_child(&popup) .append_child(&popup)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
Ok(popup) Ok(popup)
@@ -648,8 +641,7 @@ fn init_popup_bg() -> Result<Element, EditorError> {
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
document() document()
.body() .body()?
.ok_or(EditorError::NoneError)?
.append_child(&bg) .append_child(&bg)
.map_err(|_| EditorError::DOMError)?; .map_err(|_| EditorError::DOMError)?;
let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>); let callback = Closure::wrap(Box::new(|_| close_popup()) as Box<dyn FnMut(MouseEvent)>);
+3 -4
View File
@@ -1,5 +1,5 @@
#![recursion_limit = "128"] #![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene)] #![feature(decl_macro, proc_macro_hygiene, try_trait)]
#[macro_use] #[macro_use]
extern crate gettext_macros; extern crate gettext_macros;
@@ -23,7 +23,6 @@ init_i18n!(
en, en,
eo, eo,
es, es,
eu,
fa, fa,
fi, fi,
fr, fr,
@@ -62,7 +61,7 @@ lazy_static! {
static ref CATALOG: gettext::Catalog = { static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!(); let catalogs = include_i18n!();
let lang = window().unwrap().navigator().language().unwrap(); let lang = window().unwrap().navigator().language().unwrap();
let lang = lang.split_once('-').map_or("en", |x| x.0); let lang = lang.splitn(2, '-').next().unwrap_or("en");
let english_position = catalogs let english_position = catalogs
.iter() .iter()
@@ -86,7 +85,7 @@ pub fn main() -> Result<(), JsValue> {
menu(); menu();
search(); search();
editor::init() editor::init()
.map_err(|e| console::error_1(&format!("Editor error: {:?}", e).into())) .map_err(|e| console::error_1(&&format!("Editor error: {:?}", e).into()))
.ok(); .ok();
Ok(()) Ok(())
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "plume-macro" name = "plume-macro"
version = "0.7.2" version = "0.6.1-dev"
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"] authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
edition = "2018" edition = "2018"
description = "Plume procedural macros" description = "Plume procedural macros"
+2 -2
View File
@@ -58,7 +58,7 @@ pub fn import_migrations(input: TokenStream) -> TokenStream {
(name, up_sql, down_sql) (name, up_sql, down_sql)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let migrations_name = migrations.iter().map(|m| &m.0); let migrations_name = migrations.iter().map(|m| &m.0).collect::<Vec<_>>();
let migrations_up = migrations let migrations_up = migrations
.iter() .iter()
.map(|m| m.1.as_str()) .map(|m| m.1.as_str())
@@ -103,7 +103,7 @@ fn file_to_migration(file: &str) -> TokenStream2 {
acc.push('\n'); acc.push('\n');
} }
} else if let Some(acc_str) = line.strip_prefix("--#!") { } else if let Some(acc_str) = line.strip_prefix("--#!") {
acc.push_str(acc_str); acc.push_str(&acc_str);
acc.push('\n'); acc.push('\n');
} else if line.starts_with("--") { } else if line.starts_with("--") {
continue; continue;
+17 -19
View File
@@ -1,40 +1,39 @@
[package] [package]
name = "plume-models" name = "plume-models"
version = "0.7.2" version = "0.6.1-dev"
authors = ["Plume contributors"] authors = ["Plume contributors"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
ammonia = "3.2.0" activitypub = "0.1.1"
bcrypt = "0.12.1" ammonia = "2.1.1"
guid-create = "0.2" askama_escape = "0.1"
itertools = "0.10.3" bcrypt = "0.10.1"
guid-create = "0.1"
itertools = "0.8.0"
lazy_static = "1.0" lazy_static = "1.0"
ldap3 = "0.10.4" ldap3 = "0.7.1"
migrations_internals= "1.4.0" migrations_internals= "1.4.0"
openssl = "0.10.40" openssl = "0.10.22"
rocket = "0.4.6" rocket = "=0.4.6"
rocket_i18n = "0.4.1" rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.11.10" reqwest = "0.9"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0.81" serde_json = "1.0"
tantivy = "0.13.3" tantivy = "0.13.3"
url = "2.1" url = "2.1"
walkdir = "2.2" walkdir = "2.2"
webfinger = "0.4.1" webfinger = "0.4.1"
whatlang = "0.16.0" whatlang = "0.11.1"
shrinkwraprs = "0.3.0" shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2" diesel-derive-newtype = "0.1.2"
glob = "0.3.0" glob = "0.3.0"
lindera-tantivy = { version = "0.7.1", optional = true } lindera-tantivy = { version = "0.7.1", optional = true }
tracing = "0.1.34" tracing = "0.1.22"
riker = "0.4.2" riker = "0.4.2"
once_cell = "1.10.0" once_cell = "1.5.2"
lettre = "0.9.6"
native-tls = "0.2.10"
activitystreams = "0.7.0-alpha.18"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@@ -54,7 +53,6 @@ path = "../plume-common"
path = "../plume-macro" path = "../plume-macro"
[dev-dependencies] [dev-dependencies]
assert-json-diff = "2.0.1"
diesel_migrations = "1.3.0" diesel_migrations = "1.3.0"
[features] [features]
+8 -12
View File
@@ -86,18 +86,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
} }
let mut parsed_header = headers[0].split(' '); let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header let auth_type = parsed_header.next().map_or_else(
.next() || Outcome::Failure((Status::BadRequest, TokenError::NoType)),
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>( Outcome::Success,
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), )?;
Outcome::Success, let val = parsed_header.next().map_or_else(
)?; || Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
let val = parsed_header Outcome::Success,
.next() )?;
.map_or_else::<rocket::Outcome<&str, _, ()>, _, _>(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
if auth_type == "Bearer" { if auth_type == "Bearer" {
let conn = request let conn = request
+1 -1
View File
@@ -28,7 +28,7 @@ impl BlocklistedEmail {
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> { pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
use diesel::delete; use diesel::delete;
for i in ids { for i in ids {
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(conn, i)?; let be: BlocklistedEmail = BlocklistedEmail::find_by_id(&conn, i)?;
delete(&be).execute(conn)?; delete(&be).execute(conn)?;
} }
Ok(true) Ok(true)
+189 -331
View File
@@ -1,14 +1,12 @@
use crate::{ use crate::{
db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString, ap_url, db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE, schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
}; };
use activitystreams::{ use activitypub::{
actor::{ApActor, ApActorExt, AsApActor, Group}, actor::Group,
base::AnyBase,
collection::{OrderedCollection, OrderedCollectionPage}, collection::{OrderedCollection, OrderedCollectionPage},
iri_string::types::IriString, object::Image,
object::{kind::ImageType, ApObject, Image, ObjectExt}, CustomObject,
prelude::*,
}; };
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
@@ -20,12 +18,15 @@ use openssl::{
}; };
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, FromId}, inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source, SourceProperty, sign::{self, Error as SignatureError, Result as SignatureResult},
ToAsString, ToAsUri, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
}; };
use url::Url;
use webfinger::*; use webfinger::*;
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)] pub type CustomGroup = CustomObject<ApSignature, Group>;
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")] #[changeset_options(treat_none_as_null = "true")]
pub struct Blog { pub struct Blog {
pub id: i32, pub id: i32,
@@ -152,7 +153,8 @@ impl Blog {
.and_then(|l| { .and_then(|l| {
Blog::from_id( Blog::from_id(
conn, conn,
&l.href.ok_or(Error::MissingApProperty)?, &Instance::get_local().expect("Failed to get local instance"),
&l.href?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@@ -161,120 +163,104 @@ impl Blog {
} }
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
let mut blog = ApActor::new(self.inbox_url.parse()?, Group::new()); let mut blog = Group::default();
blog.set_preferred_username(self.actor_id.clone()); blog.ap_actor_props
blog.set_name(self.title.clone()); .set_preferred_username_string(self.actor_id.clone())?;
blog.set_outbox(self.outbox_url.parse()?); blog.object_props.set_name_string(self.title.clone())?;
blog.set_summary(self.summary_html.to_string()); blog.ap_actor_props
let source = SourceProperty { .set_outbox_string(self.outbox_url.clone())?;
source: Source { blog.ap_actor_props
content: self.summary.clone(), .set_inbox_string(self.inbox_url.clone())?;
media_type: String::from("text/markdown"), blog.object_props
}, .set_summary_string(self.summary_html.to_string())?;
}; blog.ap_object_props.set_source_object(Source {
content: self.summary.clone(),
media_type: String::from("text/markdown"),
})?;
let mut icon = Image::new(); let mut icon = Image::default();
let _ = self.icon_id.map(|id| { icon.object_props.set_url_string(
Media::get(conn, id).and_then(|m| { self.icon_id
let _ = m .and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
.url() .unwrap_or_default(),
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url)) )?;
.map(|url| icon.set_url(url)); icon.object_props.set_attributed_to_link(
icon.set_attributed_to( self.icon_id
User::get(conn, m.owner_id)? .and_then(|id| {
.into_id() Media::get(conn, id)
.parse::<IriString>()?, .and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
); .ok()
Ok(()) })
}) .unwrap_or_else(|| Id::new(String::new())),
}); )?;
blog.set_icon(icon.into_any_base()?); blog.object_props.set_icon_object(icon)?;
let mut banner = Image::new(); let mut banner = Image::default();
let _ = self.banner_id.map(|id| { banner.object_props.set_url_string(
Media::get(conn, id).and_then(|m| { self.banner_id
let _ = m .and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
.url() .unwrap_or_default(),
.and_then(|url| url.parse::<IriString>().map_err(|_| Error::Url)) )?;
.map(|url| banner.set_url(url)); banner.object_props.set_attributed_to_link(
banner.set_attributed_to( self.banner_id
User::get(conn, m.owner_id)? .and_then(|id| {
.into_id() Media::get(conn, id)
.parse::<IriString>()?, .and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
); .ok()
Ok(()) })
}) .unwrap_or_else(|| Id::new(String::new())),
}); )?;
blog.set_image(banner.into_any_base()?); blog.object_props.set_image_object(banner)?;
blog.set_id(self.ap_url.parse()?); blog.object_props.set_id_string(self.ap_url.clone())?;
let pub_key = PublicKey { let mut public_key = PublicKey::default();
id: format!("{}#main-key", self.ap_url).parse()?, public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
owner: self.ap_url.parse()?, public_key.set_owner_string(self.ap_url.clone())?;
public_key_pem: self.public_key.clone(), public_key.set_public_key_pem_string(self.public_key.clone())?;
}; let mut ap_signature = ApSignature::default();
let ap_signature = ApSignature { ap_signature.set_public_key_publickey(public_key)?;
public_key: pub_key,
};
Ok(CustomGroup::new(blog, ap_signature, source)) Ok(CustomGroup::new(blog, ap_signature))
} }
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> { pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
self.outbox_collection(conn).map(ActivityStream::new) let mut coll = OrderedCollection::default();
} coll.collection_props.items = serde_json::to_value(self.get_activities(conn))?;
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> { coll.collection_props
let acts = self.get_activities(conn); .set_total_items_u64(self.get_activities(conn).len() as u64)?;
let acts = acts coll.collection_props
.iter() .set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok()) coll.collection_props
.collect::<Vec<AnyBase>>(); .set_last_link(Id::new(ap_url(&format!(
let n_acts = acts.len();
let mut coll = OrderedCollection::new();
coll.set_many_items(acts);
coll.set_total_items(n_acts as u64);
coll.set_first(format!("{}?page=1", &self.outbox_url).parse::<IriString>()?);
coll.set_last(
format!(
"{}?page={}", "{}?page={}",
&self.outbox_url, &self.outbox_url,
(n_acts as u64 + ITEMS_PER_PAGE as u64 - 1) as u64 / ITEMS_PER_PAGE as u64 (self.get_activities(conn).len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
) / ITEMS_PER_PAGE as u64
.parse::<IriString>()?, ))))?;
); Ok(ActivityStream::new(coll))
Ok(coll)
} }
pub fn outbox_page( pub fn outbox_page(
&self, &self,
conn: &Connection, conn: &Connection,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> { ) -> Result<ActivityStream<OrderedCollectionPage>> {
self.outbox_collection_page(conn, (min, max)) let mut coll = OrderedCollectionPage::default();
.map(ActivityStream::new) let acts = self.get_activity_page(&conn, (min, max));
}
pub fn outbox_collection_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<OrderedCollectionPage> {
let mut coll = OrderedCollectionPage::new();
let acts = self.get_activity_page(conn, (min, max));
//This still doesn't do anything because the outbox //This still doesn't do anything because the outbox
//doesn't do anything yet //doesn't do anything yet
coll.set_next( coll.collection_page_props.set_next_link(Id::new(&format!(
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 1) "{}?page={}",
.parse::<IriString>()?, &self.outbox_url,
); min / ITEMS_PER_PAGE + 1
coll.set_prev( )))?;
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE - 1) coll.collection_page_props.set_prev_link(Id::new(&format!(
.parse::<IriString>()?, "{}?page={}",
); &self.outbox_url,
coll.set_many_items( min / ITEMS_PER_PAGE - 1
acts.iter() )))?;
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok()), coll.collection_props.items = serde_json::to_value(acts)?;
); Ok(ActivityStream::new(coll))
Ok(coll)
} }
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> { fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> {
vec![] vec![]
@@ -289,10 +275,7 @@ impl Blog {
pub fn get_keypair(&self) -> Result<PKey<Private>> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem( PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key self.private_key.clone()?.as_ref(),
.clone()
.ok_or(Error::MissingApProperty)?
.as_ref(),
)?) )?)
.map_err(Error::from) .map_err(Error::from)
} }
@@ -345,7 +328,7 @@ impl Blog {
} }
pub fn delete(&self, conn: &Connection) -> Result<()> { pub fn delete(&self, conn: &Connection) -> Result<()> {
for post in Post::get_for_blog(conn, self)? { for post in Post::get_for_blog(conn, &self)? {
post.delete(conn)?; post.delete(conn)?;
} }
diesel::delete(self) diesel::delete(self)
@@ -366,94 +349,13 @@ impl FromId<DbConn> for Blog {
type Object = CustomGroup; type Object = CustomGroup;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Self::find_by_ap_url(conn, id) Self::find_by_ap_url(&conn, id)
} }
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> { fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
let (name, outbox_url, inbox_url) = { let url = Url::parse(&acct.object.object_props.id_string()?)?;
let actor = acct.ap_actor_ref(); let inst = url.host_str()?;
let name = actor let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
.preferred_username()
.ok_or(Error::MissingApProperty)?
.to_string();
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
(
name,
actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(),
actor.inbox()?.to_string(),
)
};
let mut new_blog = NewBlog {
actor_id: name.to_string(),
outbox_url,
inbox_url,
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
private_key: None,
theme: None,
..NewBlog::default()
};
let object = ApObject::new(acct.inner);
new_blog.title = object
.name()
.and_then(|name| name.to_as_string())
.unwrap_or(name);
new_blog.summary_html = SafeString::new(
&object
.summary()
.and_then(|summary| summary.to_as_string())
.unwrap_or_default(),
);
let icon_id = object
.icon()
.and_then(|icons| {
icons.iter().next().and_then(|icon| {
let icon = icon.to_owned().extend::<Image, ImageType>().ok()??;
let owner = icon.attributed_to()?.to_as_uri()?;
Media::save_remote(
conn,
icon.url()?.to_as_uri()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
})
.map(|m| m.id);
new_blog.icon_id = icon_id;
let banner_id = object
.image()
.and_then(|banners| {
banners.iter().next().and_then(|banner| {
let banner = banner.to_owned().extend::<Image, ImageType>().ok()??;
let owner = banner.attributed_to()?.to_as_uri()?;
Media::save_remote(
conn,
banner.url()?.to_as_uri()?,
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
)
.ok()
})
})
.map(|m| m.id);
new_blog.banner_id = banner_id;
new_blog.summary = acct.ext_two.source.content;
let any_base = AnyBase::from_extended(object)?;
let id = any_base.id().ok_or(Error::MissingApProperty)?;
new_blog.ap_url = id.to_string();
let inst = id
.authority_components()
.ok_or(Error::Url)?
.host()
.to_string();
let instance = Instance::find_by_domain(conn, &inst).or_else(|_| {
Instance::insert( Instance::insert(
conn, conn,
NewInstance { NewInstance {
@@ -467,16 +369,94 @@ impl FromId<DbConn> for Blog {
open_registrations: true, open_registrations: true,
short_description_html: String::new(), short_description_html: String::new(),
long_description_html: String::new(), long_description_html: String::new(),
private_key: None,
public_key: None,
}, },
) )
})?; })?;
new_blog.instance_id = instance.id; let icon_id = acct
.object
.object_props
.icon_image()
.ok()
.and_then(|icon| {
let owner = icon.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
conn,
icon.object_props.url_string().ok()?,
&User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&owner,
None,
CONFIG.proxy(),
)
.ok()?,
)
.ok()
})
.map(|m| m.id);
Blog::insert(conn, new_blog) let banner_id = acct
} .object
.object_props
.image_image()
.ok()
.and_then(|banner| {
let owner = banner.object_props.attributed_to_link::<Id>().ok()?;
Media::save_remote(
conn,
banner.object_props.url_string().ok()?,
&User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&owner,
None,
CONFIG.proxy(),
)
.ok()?,
)
.ok()
})
.map(|m| m.id);
fn get_sender() -> &'static dyn sign::Signer { let name = acct.object.ap_actor_props.preferred_username_string()?;
Instance::get_local_instance_user().expect("Failed to local instance user") if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
Blog::insert(
conn,
NewBlog {
actor_id: name.clone(),
title: acct.object.object_props.name_string().unwrap_or(name),
outbox_url: acct.object.ap_actor_props.outbox_string()?,
inbox_url: acct.object.ap_actor_props.inbox_string()?,
summary: acct
.object
.ap_object_props
.source_object::<Source>()
.map(|s| s.content)
.unwrap_or_default(),
instance_id: instance.id,
ap_url: acct.object.object_props.id_string()?,
public_key: acct
.custom_props
.public_key_publickey()?
.public_key_pem_string()?,
private_key: None,
banner_id,
icon_id,
summary_html: SafeString::new(
&acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
),
theme: None,
},
)
} }
} }
@@ -501,18 +481,18 @@ impl sign::Signer for Blog {
format!("{}#main-key", self.ap_url) format!("{}#main-key", self.ap_url)
} }
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> { fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
let key = self.get_keypair().map_err(|_| sign::Error())?; let key = self.get_keypair()?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?; let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?; signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(sign::Error::from) signer.sign_to_vec().map_err(SignatureError::from)
} }
fn verify(&self, data: &str, signature: &[u8]) -> sign::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 key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?; verifier.update(data.as_bytes())?;
verifier.verify(signature).map_err(sign::Error::from) verifier.verify(&signature).map_err(SignatureError::from)
} }
} }
@@ -543,14 +523,12 @@ pub(crate) mod tests {
blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db, blog_authors::*, instance::tests as instance_tests, medias::NewMedia, tests::db,
users::tests as usersTests, Connection as Conn, users::tests as usersTests, Connection as Conn,
}; };
use assert_json_diff::assert_json_eq;
use diesel::Connection; use diesel::Connection;
use serde_json::to_value;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) { pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
instance_tests::fill_database(conn); instance_tests::fill_database(conn);
let users = usersTests::fill_database(conn); let users = usersTests::fill_database(conn);
let mut blog1 = Blog::insert( let blog1 = Blog::insert(
conn, conn,
NewBlog::new_local( NewBlog::new_local(
"BlogName".to_owned(), "BlogName".to_owned(),
@@ -623,41 +601,6 @@ pub(crate) mod tests {
}, },
) )
.unwrap(); .unwrap();
blog1.icon_id = Some(
Media::insert(
conn,
NewMedia {
file_path: "aaa.png".into(),
alt_text: String::new(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: users[0].id,
},
)
.unwrap()
.id,
);
blog1.banner_id = Some(
Media::insert(
conn,
NewMedia {
file_path: "bbb.png".into(),
alt_text: String::new(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: users[0].id,
},
)
.unwrap()
.id,
);
let _: Blog = blog1.save_changes(&*conn).unwrap();
(users, vec![blog1, blog2, blog3]) (users, vec![blog1, blog2, blog3])
} }
@@ -954,6 +897,7 @@ pub(crate) mod tests {
.id, .id,
); );
let _: Blog = blogs[0].save_changes(&**conn).unwrap(); let _: Blog = blogs[0].save_changes(&**conn).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap(); let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap(); blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap(); let blog = Blog::from_activity(&conn, ap_repr).unwrap();
@@ -974,90 +918,4 @@ pub(crate) mod tests {
Ok(()) Ok(())
}) })
} }
#[test]
fn to_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(&conn);
let blog = &blogs[0];
let act = blog.to_activity(conn)?;
let expected = json!({
"icon": {
"attributedTo": "https://plu.me/@/admin/",
"type": "Image",
"url": "https://plu.me/aaa.png"
},
"id": "https://plu.me/~/BlogName/",
"image": {
"attributedTo": "https://plu.me/@/admin/",
"type": "Image",
"url": "https://plu.me/bbb.png"
},
"inbox": "https://plu.me/~/BlogName/inbox",
"name": "Blog name",
"outbox": "https://plu.me/~/BlogName/outbox",
"preferredUsername": "BlogName",
"publicKey": {
"id": "https://plu.me/~/BlogName/#main-key",
"owner": "https://plu.me/~/BlogName/",
"publicKeyPem": blog.public_key
},
"source": {
"content": "This is a small blog",
"mediaType": "text/markdown"
},
"summary": "",
"type": "Group"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(conn);
let blog = &blogs[0];
let act = blog.outbox_collection(conn)?;
let expected = json!({
"items": [],
"totalItems": 0,
"first": "https://plu.me/~/BlogName/outbox?page=1",
"last": "https://plu.me/~/BlogName/outbox?page=0",
"type": "OrderedCollection"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection_page() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(conn);
let blog = &blogs[0];
let act = blog.outbox_collection_page(conn, (33, 36))?;
let expected = json!({
"next": "https://plu.me/~/BlogName/outbox?page=3",
"prev": "https://plu.me/~/BlogName/outbox?page=1",
"items": [],
"type": "OrderedCollectionPage"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
} }
+122 -276
View File
@@ -11,23 +11,17 @@ use crate::{
users::User, users::User,
Connection, Error, Result, CONFIG, Connection, Error, Result, CONFIG,
}; };
use activitystreams::{ use activitypub::{
activity::{Create, Delete}, activity::{Create, Delete},
base::{AnyBase, Base}, link,
iri_string::types::IriString,
link::{self, kind::MentionType},
object::{Note, Tombstone}, object::{Note, Tombstone},
prelude::*,
primitives::OneOrMany,
time::OffsetDateTime,
}; };
use chrono::{self, NaiveDateTime}; use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, Id, IntoId, PUBLIC_VISIBILITY,
IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY,
}, },
utils, utils,
}; };
@@ -64,7 +58,7 @@ impl Comment {
insert!(comments, NewComment, |inserted, conn| { insert!(comments, NewComment, |inserted, conn| {
if inserted.ap_url.is_none() { if inserted.ap_url.is_none() {
inserted.ap_url = Some(format!( inserted.ap_url = Some(format!(
"{}/comment/{}", "{}comment/{}",
inserted.get_post(conn)?.ap_url, inserted.get_post(conn)?.ap_url,
inserted.id inserted.id
)); ));
@@ -120,59 +114,45 @@ impl Comment {
Some(Media::get_media_processor(conn, vec![&author])), Some(Media::get_media_processor(conn, vec![&author])),
); );
let mut note = Note::new(); let mut note = Note::default();
let to = vec![PUBLIC_VISIBILITY.parse::<IriString>()?]; let to = vec![Id::new(PUBLIC_VISIBILITY.to_string())];
note.set_id( note.object_props
self.ap_url .set_id_string(self.ap_url.clone().unwrap_or_default())?;
.clone() note.object_props
.unwrap_or_default() .set_summary_string(self.spoiler_text.clone())?;
.parse::<IriString>()?, note.object_props.set_content_string(html)?;
); note.object_props
note.set_summary(self.spoiler_text.clone()); .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
note.set_content(html); || Ok(Post::get(conn, self.post_id)?.ap_url),
note.set_in_reply_to(self.in_response_to_id.map_or_else( |id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result<String>,
|| Post::get(conn, self.post_id).map(|post| post.ap_url), )?))?;
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()), note.object_props
)?); .set_published_string(chrono::Utc::now().to_rfc3339())?;
note.set_published( note.object_props.set_attributed_to_link(author.into_id())?;
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into()) note.object_props.set_to_link_vec(to)?;
.expect("OffsetDateTime"), note.object_props.set_tag_link_vec(
); mentions
note.set_attributed_to(author.into_id().parse::<IriString>()?); .into_iter()
note.set_many_tos(to); .filter_map(|m| Mention::build_activity(conn, &m).ok())
note.set_many_tags(mentions.into_iter().filter_map(|m| { .collect::<Vec<link::Mention>>(),
Mention::build_activity(conn, &m) )?;
.map(|mention| mention.into_any_base().expect("Can convert"))
.ok()
}));
Ok(note) Ok(note)
} }
pub fn create_activity(&self, conn: &DbConn) -> Result<Create> { pub fn create_activity(&self, conn: &DbConn) -> Result<Create> {
let author = User::get(conn, self.author_id)?; let author = User::get(&conn, self.author_id)?;
let note = self.to_activity(conn)?; let note = self.to_activity(conn)?;
let note_clone = note.clone(); let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?;
let mut act = Create::new( act.create_props.set_object_object(note.clone())?;
author.into_id().parse::<IriString>()?, act.object_props
Base::retract(note)?.into_generic()?, .set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
); act.object_props
act.set_id( .set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
format!( act.object_props
"{}/activity", .set_cc_link_vec(vec![Id::new(self.get_author(&conn)?.followers_endpoint)])?;
self.ap_url.clone().ok_or(Error::MissingApProperty)?,
)
.parse::<IriString>()?,
);
act.set_many_tos(
note_clone
.to()
.iter()
.flat_map(|tos| tos.iter().map(|to| to.to_owned())),
);
act.set_many_ccs(vec![self.get_author(conn)?.followers_endpoint]);
Ok(act) Ok(act)
} }
@@ -197,21 +177,18 @@ impl Comment {
} }
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> { pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
let mut tombstone = Tombstone::new(); let mut act = Delete::default();
tombstone.set_id( act.delete_props
self.ap_url .set_actor_link(self.get_author(conn)?.into_id())?;
.as_ref()
.ok_or(Error::MissingApProperty)?
.parse::<IriString>()?,
);
let mut act = Delete::new( let mut tombstone = Tombstone::default();
self.get_author(conn)?.into_id().parse::<IriString>()?, tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
Base::retract(tombstone)?.into_generic()?, act.delete_props.set_object_object(tombstone)?;
);
act.set_id(format!("{}#delete", self.ap_url.clone().unwrap()).parse::<IriString>()?); act.object_props
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); .set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
Ok(act) Ok(act)
} }
@@ -227,110 +204,110 @@ impl FromId<DbConn> for Comment {
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> { fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = { let comm = {
let previous_url = note let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
.in_reply_to() let previous_comment = Comment::find_by_ap_url(conn, previous_url);
.ok_or(Error::MissingApProperty)?
.iter()
.next()
.ok_or(Error::MissingApProperty)?
.id()
.ok_or(Error::MissingApProperty)?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url.as_str());
let is_public = |v: &Option<&OneOrMany<AnyBase>>| match v { let is_public = |v: &Option<serde_json::Value>| match v
Some(one_or_many) => one_or_many.iter().any(|any_base| { .as_ref()
let id = any_base.id(); .unwrap_or(&serde_json::Value::Null)
id.is_some() && id.unwrap() == PUBLIC_VISIBILITY {
}), serde_json::Value::Array(v) => v
None => false, .iter()
.filter_map(serde_json::Value::as_str)
.any(|s| s == PUBLIC_VISIBILITY),
serde_json::Value::String(s) => s == PUBLIC_VISIBILITY,
_ => false,
}; };
let public_visibility = is_public(&note.to()) let public_visibility = is_public(&note.object_props.to)
|| is_public(&note.bto()) || is_public(&note.object_props.bto)
|| is_public(&note.cc()) || is_public(&note.object_props.cc)
|| is_public(&note.bcc()); || is_public(&note.object_props.bcc);
let summary = note.summary().and_then(|summary| summary.to_as_string());
let sensitive = summary.is_some();
let comm = Comment::insert( let comm = Comment::insert(
conn, conn,
NewComment { NewComment {
content: SafeString::new( content: SafeString::new(&note.object_props.content_string()?),
&note spoiler_text: note.object_props.summary_string().unwrap_or_default(),
.content() ap_url: note.object_props.id_string().ok(),
.ok_or(Error::MissingApProperty)?
.to_as_string()
.ok_or(Error::InvalidValue)?,
),
spoiler_text: summary.unwrap_or_default(),
ap_url: Some(
note.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
),
in_response_to_id: previous_comment.iter().map(|c| c.id).next(), in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
post_id: previous_comment.map(|c| c.post_id).or_else(|_| { post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
Ok(Post::find_by_ap_url(conn, previous_url.as_str())?.id) as Result<i32> Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
})?, })?,
author_id: User::from_id( author_id: User::from_id(
conn, conn,
&note &Instance::get_local().expect("Failed to get local instance"),
.attributed_to() &note.object_props.attributed_to_link::<Id>()?,
.ok_or(Error::MissingApProperty)?
.to_as_uri()
.ok_or(Error::MissingApProperty)?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
sensitive, sensitive: note.object_props.summary_string().is_ok(),
public_visibility, public_visibility,
}, },
)?; )?;
// save mentions // save mentions
if let Some(tags) = note.tag() { if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
let author_url = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0].ap_url; for tag in tags {
for tag in tags.iter() { serde_json::from_value::<link::Mention>(tag)
let m = tag.clone().extend::<link::Mention, MentionType>()?; // FIXME: Don't clone .map_err(Error::from)
if m.is_none() { .and_then(|m| {
continue; let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
} let not_author = m.link_props.href_string()? != author.ap_url.clone();
let m = m.unwrap(); Mention::from_activity(conn, &m, comm.id, false, not_author)
let not_author = m.href().ok_or(Error::MissingApProperty)? != author_url; })
let _ = Mention::from_activity(conn, &m, comm.id, false, not_author); .ok();
} }
} }
comm comm
}; };
if !comm.public_visibility { if !comm.public_visibility {
let mut receiver_ids = HashSet::new(); let receivers_ap_url = |v: Option<serde_json::Value>| {
let mut receivers_id = |v: Option<&'_ OneOrMany<AnyBase>>| { let filter = |e: serde_json::Value| {
if let Some(one_or_many) = v { if let serde_json::Value::String(s) = e {
for any_base in one_or_many.iter() { Some(s)
if let Some(id) = any_base.id() { } else {
receiver_ids.insert(id.to_string()); None
}
} }
};
match v.unwrap_or(serde_json::Value::Null) {
serde_json::Value::Array(v) => v,
v => vec![v],
} }
.into_iter()
.filter_map(filter)
}; };
receivers_id(note.to()); let mut note = note;
receivers_id(note.cc());
receivers_id(note.bto());
receivers_id(note.bcc());
let receivers_ap_url = receiver_ids let to = receivers_ap_url(note.object_props.to.take());
let cc = receivers_ap_url(note.object_props.cc.take());
let bto = receivers_ap_url(note.object_props.bto.take());
let bcc = receivers_ap_url(note.object_props.bcc.take());
let receivers_ap_url = to
.chain(cc)
.chain(bto)
.chain(bcc)
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
.into_iter() .into_iter()
.flat_map(|v| { .map(|v| {
if let Ok(user) = User::from_id(conn, v.as_ref(), 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] vec![user]
} else { } else {
vec![] // TODO try to fetch collection vec![] // TODO try to fetch collection
} }
}) })
.flatten()
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false)) .filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
.collect::<HashSet<User>>(); //remove duplicates (prevent db error) .collect::<HashSet<User>>(); //remove duplicates (prevent db error)
@@ -348,10 +325,6 @@ impl FromId<DbConn> for Comment {
comm.notify(conn)?; comm.notify(conn)?;
Ok(comm) Ok(comm)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Create, &DbConn> for Comment { impl AsObject<User, Create, &DbConn> for Comment {
@@ -380,7 +353,7 @@ impl AsObject<User, Delete, &DbConn> for Comment {
m.delete(conn)?; m.delete(conn)?;
} }
for n in Notification::find_for_comment(conn, &self)? { for n in Notification::find_for_comment(&conn, &self)? {
n.delete(&**conn)?; n.delete(&**conn)?;
} }
@@ -422,34 +395,10 @@ impl CommentTree {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::blogs::Blog;
use crate::inbox::{inbox, tests::fill_database, InboxResult}; use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString; use crate::safe_string::SafeString;
use crate::tests::{db, format_datetime}; use crate::tests::db;
use assert_json_diff::assert_json_eq;
use diesel::Connection; use diesel::Connection;
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Comment, Vec<Post>, Vec<User>, Vec<Blog>) {
let (posts, users, blogs) = fill_database(&conn);
let comment = Comment::insert(
conn,
NewComment {
content: SafeString::new("My comment, mentioning to @user"),
in_response_to_id: None,
post_id: posts[0].id,
author_id: users[0].id,
ap_url: None,
sensitive: true,
spoiler_text: "My CW".into(),
public_visibility: true,
},
)
.unwrap();
(comment, posts, users, blogs)
}
// creates a post, get it's Create activity, delete the post, // creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works // "send" the Create to the inbox, and check it works
@@ -457,77 +406,30 @@ mod tests {
fn self_federation() { fn self_federation() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (original_comm, posts, users, _blogs) = prepare_activity(&conn); let (posts, users, _) = fill_database(&conn);
let act = original_comm.create_activity(&conn).unwrap();
assert_json_eq!(to_value(&act).unwrap(), json!({ let original_comm = Comment::insert(
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id),
"object": {
"attributedTo": "https://plu.me/@/admin/",
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
"###,
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
"inReplyTo": "https://plu.me/~/BlogName/testing",
"published": format_datetime(&original_comm.creation_date),
"summary": "My CW",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create",
}));
let reply = Comment::insert(
conn, conn,
NewComment { NewComment {
content: SafeString::new(""), content: SafeString::new("My comment"),
in_response_to_id: Some(original_comm.id), in_response_to_id: None,
post_id: posts[0].id, post_id: posts[0].id,
author_id: users[1].id, author_id: users[0].id,
ap_url: None, ap_url: None,
sensitive: false, sensitive: true,
spoiler_text: "".into(), spoiler_text: "My CW".into(),
public_visibility: true, public_visibility: true,
}, },
) )
.unwrap(); .unwrap();
let reply_act = reply.create_activity(&conn).unwrap(); let act = original_comm.create_activity(&conn).unwrap();
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
"actor": "https://plu.me/@/user/",
"cc": ["https://plu.me/@/user/followers"],
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id),
"object": {
"attributedTo": "https://plu.me/@/user/",
"content": "",
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id),
"inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
"published": format_datetime(&reply.creation_date),
"summary": "",
"tag": [],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create"
}));
inbox( inbox(
&conn, &conn,
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(), serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(),
) )
.unwrap(); .unwrap();
match inbox(&conn, to_value(act).unwrap()).unwrap() { match inbox(&conn, serde_json::to_value(act).unwrap()).unwrap() {
InboxResult::Commented(c) => { InboxResult::Commented(c) => {
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content); // TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content);
assert_eq!(c.in_response_to_id, original_comm.in_response_to_id); assert_eq!(c.in_response_to_id, original_comm.in_response_to_id);
@@ -542,60 +444,4 @@ mod tests {
Ok(()) Ok(())
}) })
} }
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
let act = comment.to_activity(&conn)?;
let expected = json!({
"attributedTo": "https://plu.me/@/admin/",
"content": r###"<p dir="auto">My comment, mentioning to <a href="https://plu.me/@/user/" title="user">@user</a></p>
"###,
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
"inReplyTo": "https://plu.me/~/BlogName/testing",
"published": format_datetime(&comment.creation_date),
"summary": "My CW",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_delete() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (comment, _posts, _users, _blogs) = prepare_activity(&conn);
let act = comment.build_delete(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id),
"object": {
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
"type": "Tombstone"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Delete"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
} }
+11 -42
View File
@@ -1,6 +1,4 @@
use crate::search::TokenizerKind as SearchTokenizer; use crate::search::TokenizerKind as SearchTokenizer;
use crate::signups::Strategy as SignupStrategy;
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
use rocket::config::Limits; use rocket::config::Limits;
use rocket::Config as RocketConfig; use rocket::Config as RocketConfig;
use std::collections::HashSet; use std::collections::HashSet;
@@ -17,14 +15,12 @@ pub struct Config {
pub db_name: &'static str, pub db_name: &'static str,
pub db_max_size: Option<u32>, pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>, pub db_min_idle: Option<u32>,
pub signup: SignupStrategy,
pub search_index: String, pub search_index: String,
pub search_tokenizers: SearchTokenizerConfig, pub search_tokenizers: SearchTokenizerConfig,
pub rocket: Result<RocketConfig, InvalidRocketConfig>, pub rocket: Result<RocketConfig, RocketError>,
pub logo: LogoConfig, pub logo: LogoConfig,
pub default_theme: String, pub default_theme: String,
pub media_directory: String, pub media_directory: String,
pub mail: Option<MailConfig>,
pub ldap: Option<LdapConfig>, pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>, pub proxy: Option<ProxyConfig>,
} }
@@ -35,21 +31,21 @@ impl Config {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum InvalidRocketConfig { pub enum RocketError {
Env, InvalidEnv,
Address, InvalidAddress,
SecretKey, InvalidSecretKey,
} }
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> { fn get_rocket_config() -> Result<RocketConfig, RocketError> {
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?; let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned()); let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
let port = var("ROCKET_PORT") let port = var("ROCKET_PORT")
.ok() .ok()
.map(|s| s.parse::<u16>().unwrap()) .map(|s| s.parse::<u16>().unwrap())
.unwrap_or(7878); .unwrap_or(7878);
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?; let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| RocketError::InvalidSecretKey)?;
let form_size = var("FORM_SIZE") let form_size = var("FORM_SIZE")
.unwrap_or_else(|_| "128".to_owned()) .unwrap_or_else(|_| "128".to_owned())
.parse::<u64>() .parse::<u64>()
@@ -60,10 +56,10 @@ fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
.unwrap(); .unwrap();
c.set_address(address) c.set_address(address)
.map_err(|_| InvalidRocketConfig::Address)?; .map_err(|_| RocketError::InvalidAddress)?;
c.set_port(port); c.set_port(port);
c.set_secret_key(secret_key) c.set_secret_key(secret_key)
.map_err(|_| InvalidRocketConfig::SecretKey)?; .map_err(|_| RocketError::InvalidSecretKey)?;
c.set_limits( c.set_limits(
Limits::new() Limits::new()
@@ -159,7 +155,7 @@ impl Default for LogoConfig {
.ok() .ok()
.or_else(|| custom_main.clone()); .or_else(|| custom_main.clone());
let other = if let Some(main) = custom_main.clone() { let other = if let Some(main) = custom_main.clone() {
let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) { let ext = |path: &str| match path.rsplitn(2, '.').next() {
Some("png") => Some("image/png".to_owned()), Some("png") => Some("image/png".to_owned()),
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()), Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
Some("svg") => Some("image/svg+xml".to_owned()), Some("svg") => Some("image/svg+xml".to_owned()),
@@ -249,31 +245,6 @@ impl SearchTokenizerConfig {
} }
} }
pub struct MailConfig {
pub server: String,
pub port: u16,
pub helo_name: String,
pub username: String,
pub password: String,
}
fn get_mail_config() -> Option<MailConfig> {
Some(MailConfig {
server: env::var("MAIL_SERVER").ok()?,
port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() {
"smtp" => SMTP_PORT,
"submissions" => SUBMISSIONS_PORT,
"submission" => SUBMISSION_PORT,
number => number
.parse()
.expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#),
}),
helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()),
username: env::var("MAIL_USER").ok()?,
password: env::var("MAIL_PASSWORD").ok()?,
})
}
pub struct LdapConfig { pub struct LdapConfig {
pub addr: String, pub addr: String,
pub base_dn: String, pub base_dn: String,
@@ -364,7 +335,6 @@ lazy_static! {
s.parse::<u32>() s.parse::<u32>()
.expect("Couldn't parse DB_MIN_IDLE into u32") .expect("Couldn't parse DB_MIN_IDLE into u32")
)), )),
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
database_url: var("DATABASE_URL") database_url: var("DATABASE_URL")
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)), .unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
@@ -377,7 +347,6 @@ lazy_static! {
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()), default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
media_directory: var("MEDIA_UPLOAD_DIRECTORY") media_directory: var("MEDIA_UPLOAD_DIRECTORY")
.unwrap_or_else(|_| "static/media".to_owned()), .unwrap_or_else(|_| "static/media".to_owned()),
mail: get_mail_config(),
ldap: get_ldap_config(), ldap: get_ldap_config(),
proxy: get_proxy_config(), proxy: get_proxy_config(),
}; };
-143
View File
@@ -1,143 +0,0 @@
use crate::{
db_conn::DbConn,
schema::email_signups,
users::{NewUser, Role, User},
Error, Result,
};
use chrono::{offset::Utc, Duration, NaiveDateTime};
use diesel::{
Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
};
use plume_common::utils::random_hex;
use std::ops::Deref;
const TOKEN_VALIDITY_HOURS: i64 = 2;
#[repr(transparent)]
pub struct Token(String);
impl From<String> for Token {
fn from(string: String) -> Self {
Token(string)
}
}
impl From<Token> for String {
fn from(token: Token) -> Self {
token.0
}
}
impl Deref for Token {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Token {
fn generate() -> Self {
Self(random_hex())
}
}
#[derive(Identifiable, Queryable)]
pub struct EmailSignup {
pub id: i32,
pub email: String,
pub token: String,
pub expiration_date: NaiveDateTime,
}
#[derive(Insertable)]
#[table_name = "email_signups"]
pub struct NewEmailSignup<'a> {
pub email: &'a str,
pub token: &'a str,
pub expiration_date: NaiveDateTime,
}
impl EmailSignup {
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, email)?;
let _rows = Self::delete_existings_by_email(conn, email)?;
let token = Token::generate();
let expiration_date = Utc::now()
.naive_utc()
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
.expect("could not calculate expiration date");
let new_signup = NewEmailSignup {
email,
token: &token,
expiration_date,
};
let _rows = diesel::insert_into(email_signups::table)
.values(new_signup)
.execute(&**conn)?;
Ok(token)
})
}
pub fn find_by_token(conn: &DbConn, token: Token) -> Result<Self> {
let signup = email_signups::table
.filter(email_signups::token.eq(token.as_str()))
.first::<Self>(&**conn)
.map_err(Error::from)?;
Ok(signup)
}
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
if self.expired() {
Self::delete_existings_by_email(conn, &self.email)?;
return Err(Error::Expired);
}
Ok(())
})
}
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
let user = NewUser::new_local(
conn,
username,
"".to_string(),
Role::Normal,
"",
self.email.clone(),
Some(User::hash_pass(&password)?),
)?;
self.delete(conn)?;
Ok(user)
})
}
fn delete(&self, conn: &DbConn) -> Result<()> {
let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?;
Ok(())
}
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
if User::email_used(conn, email)? {
let _rows = Self::delete_existings_by_email(conn, email)?;
return Err(Error::UserAlreadyExists);
}
Ok(())
}
fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result<usize> {
let existing_signups = email_signups::table.filter(email_signups::email.eq(email));
diesel::delete(existing_signups)
.execute(&**conn)
.map_err(Error::from)
}
fn expired(&self) -> bool {
self.expiration_date < Utc::now().naive_utc()
}
}
+49 -163
View File
@@ -2,12 +2,7 @@ use crate::{
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User, ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG, Connection, Error, Result, CONFIG,
}; };
use activitystreams::{ use activitypub::activity::{Accept, Follow as FollowAct, Undo};
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
base::AnyBase,
iri_string::types::IriString,
prelude::*,
};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
broadcast, broadcast,
@@ -58,13 +53,15 @@ impl Follow {
pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> { pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
let user = User::get(conn, self.follower_id)?; let user = User::get(conn, self.follower_id)?;
let target = User::get(conn, self.following_id)?; let target = User::get(conn, self.following_id)?;
let target_id = target.ap_url.parse::<IriString>()?;
let mut act = FollowAct::new(user.ap_url.parse::<IriString>()?, target_id.clone());
act.set_id(self.ap_url.parse::<IriString>()?);
act.set_many_tos(vec![target_id]);
act.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
let mut act = FollowAct::default();
act.follow_props.set_actor_link::<Id>(user.into_id())?;
act.follow_props
.set_object_link::<Id>(target.clone().into_id())?;
act.object_props.set_id_string(self.ap_url.clone())?;
act.object_props.set_to_link_vec(vec![target.into_id()])?;
act.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
Ok(act) Ok(act)
} }
@@ -97,16 +94,28 @@ impl Follow {
NewFollow { NewFollow {
follower_id: from_id, follower_id: from_id,
following_id: target_id, following_id: target_id,
ap_url: follow ap_url: follow.object_props.id_string()?,
.object_field_ref()
.as_single_id()
.ok_or(Error::MissingApProperty)?
.to_string(),
}, },
)?; )?;
res.notify(conn)?; res.notify(conn)?;
let accept = res.build_accept(from, target, follow)?; let mut accept = Accept::default();
let accept_id = ap_url(&format!(
"{}/follow/{}/accept",
CONFIG.base_url.as_str(),
&res.id
));
accept.object_props.set_id_string(accept_id)?;
accept
.object_props
.set_to_link_vec(vec![from.clone().into_id()])?;
accept
.object_props
.set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
accept
.accept_props
.set_actor_link::<Id>(target.clone().into_id())?;
accept.accept_props.set_object_object(follow)?;
broadcast( broadcast(
&*target, &*target,
accept, accept,
@@ -116,41 +125,18 @@ impl Follow {
Ok(res) Ok(res)
} }
pub fn build_accept<A: Signer + IntoId + Clone, B: Clone + AsActor<T> + IntoId, T>(
&self,
from: &B,
target: &A,
follow: FollowAct,
) -> Result<Accept> {
let mut accept = Accept::new(
target.clone().into_id().parse::<IriString>()?,
AnyBase::from_extended(follow)?,
);
let accept_id = ap_url(&format!(
"{}/follows/{}/accept",
CONFIG.base_url.as_str(),
self.id
));
accept.set_id(accept_id.parse::<IriString>()?);
accept.set_many_tos(vec![from.clone().into_id().parse::<IriString>()?]);
accept.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]);
Ok(accept)
}
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut undo = Undo::new( let mut undo = Undo::default();
User::get(conn, self.follower_id)? undo.undo_props
.ap_url .set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
.parse::<IriString>()?, undo.object_props
self.ap_url.parse::<IriString>()?, .set_id_string(format!("{}/undo", self.ap_url))?;
); undo.undo_props
undo.set_id(format!("{}/undo", self.ap_url).parse::<IriString>()?); .set_object_link::<Id>(self.clone().into_id())?;
undo.set_many_tos(vec![User::get(conn, self.following_id)? undo.object_props
.ap_url .set_to_link_vec(vec![User::get(conn, self.following_id)?.into_id()])?;
.parse::<IriString>()?]); undo.object_props
undo.set_many_ccs(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); .set_cc_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
Ok(undo) Ok(undo)
} }
} }
@@ -162,7 +148,11 @@ impl AsObject<User, FollowAct, &DbConn> for User {
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> { fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it, // Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here // so we rebuilt it here
let follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?); let mut follow = FollowAct::default();
follow.object_props.set_id_string(id.to_string())?;
follow
.follow_props
.set_actor_link::<Id>(actor.clone().into_id())?;
Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id) Follow::accept_follow(conn, &actor, &self, follow, actor.id, self.id)
} }
} }
@@ -178,11 +168,8 @@ impl FromId<DbConn> for Follow {
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> { fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id( let actor = User::from_id(
conn, conn,
follow &Instance::get_local().expect("Failed to get local instance"),
.actor_field_ref() &follow.follow_props.actor_link::<Id>()?,
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@@ -190,21 +177,14 @@ impl FromId<DbConn> for Follow {
let target = User::from_id( let target = User::from_id(
conn, conn,
follow &Instance::get_local().expect("Failed to get local instance"),
.object_field_ref() &follow.follow_props.object_link::<Id>()?,
.as_single_id()
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
.map_err(|(_, e)| e)?; .map_err(|(_, e)| e)?;
Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id) Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Undo, &DbConn> for Follow { impl AsObject<User, Undo, &DbConn> for Follow {
@@ -217,7 +197,7 @@ impl AsObject<User, Undo, &DbConn> for Follow {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { if let Ok(notif) = Notification::find(&conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(&**conn)?;
} }
@@ -237,29 +217,8 @@ impl IntoId for Follow {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{tests::db, users::tests as user_tests, users::tests::fill_database}; use crate::{tests::db, users::tests as user_tests};
use assert_json_diff::assert_json_eq;
use diesel::Connection; use diesel::Connection;
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Follow, User, User, Vec<User>) {
let users = fill_database(conn);
let following = &users[1];
let follower = &users[2];
let mut follow = Follow::insert(
conn,
NewFollow {
follower_id: follower.id,
following_id: following.id,
ap_url: "".into(),
},
)
.unwrap();
// following.ap_url = format!("https://plu.me/follows/{}", follow.id);
follow.ap_url = format!("https://plu.me/follows/{}", follow.id);
(follow, following.to_owned(), follower.to_owned(), users)
}
#[test] #[test]
fn test_id() { fn test_id() {
@@ -294,77 +253,4 @@ mod tests {
Ok(()) Ok(())
}) })
} }
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (follow, _following, _follower, _users) = prepare_activity(&conn);
let act = follow.to_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/other/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://plu.me/follows/{}", follow.id),
"object": "https://plu.me/@/user/",
"to": ["https://plu.me/@/user/"],
"type": "Follow"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_accept() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (follow, following, follower, _users) = prepare_activity(&conn);
let act = follow.build_accept(&follower, &following, follow.to_activity(&conn)?)?;
let expected = json!({
"actor": "https://plu.me/@/user/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://127.0.0.1:7878/follows/{}/accept", follow.id),
"object": {
"actor": "https://plu.me/@/other/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://plu.me/follows/{}", follow.id),
"object": "https://plu.me/@/user/",
"to": ["https://plu.me/@/user/"],
"type": "Follow"
},
"to": ["https://plu.me/@/other/"],
"type": "Accept"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_undo() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (follow, _following, _follower, _users) = prepare_activity(&conn);
let act = follow.build_undo(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/other/",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"id": format!("https://plu.me/follows/{}/undo", follow.id),
"object": format!("https://plu.me/follows/{}", follow.id),
"to": ["https://plu.me/@/user/"],
"type": "Undo"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
} }
+29 -23
View File
@@ -1,9 +1,11 @@
use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Update}; use activitypub::activity::*;
use crate::{ use crate::{
comments::Comment, comments::Comment,
db_conn::DbConn, db_conn::DbConn,
follows, likes, follows,
instance::Instance,
likes,
posts::{Post, PostUpdate}, posts::{Post, PostUpdate},
reshares::Reshare, reshares::Reshare,
users::User, users::User,
@@ -47,20 +49,24 @@ impl_into_inbox_result! {
} }
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> { pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(conn, act) Inbox::handle(
.with::<User, Announce, Post>(CONFIG.proxy()) conn,
.with::<User, Create, Comment>(CONFIG.proxy()) &Instance::get_local().expect("Failed to get local instance"),
.with::<User, Create, Post>(CONFIG.proxy()) act,
.with::<User, Delete, Comment>(CONFIG.proxy()) )
.with::<User, Delete, Post>(CONFIG.proxy()) .with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Delete, User>(CONFIG.proxy()) .with::<User, Create, Comment>(CONFIG.proxy())
.with::<User, Follow, User>(CONFIG.proxy()) .with::<User, Create, Post>(CONFIG.proxy())
.with::<User, Like, Post>(CONFIG.proxy()) .with::<User, Delete, Comment>(CONFIG.proxy())
.with::<User, Undo, Reshare>(CONFIG.proxy()) .with::<User, Delete, Post>(CONFIG.proxy())
.with::<User, Undo, follows::Follow>(CONFIG.proxy()) .with::<User, Delete, User>(CONFIG.proxy())
.with::<User, Undo, likes::Like>(CONFIG.proxy()) .with::<User, Follow, User>(CONFIG.proxy())
.with::<User, Update, PostUpdate>(CONFIG.proxy()) .with::<User, Like, Post>(CONFIG.proxy())
.done() .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)] #[cfg(test)]
@@ -94,8 +100,8 @@ pub(crate) mod tests {
license: "WTFPL".to_owned(), license: "WTFPL".to_owned(),
creation_date: None, creation_date: None,
ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id), ap_url: format!("https://plu.me/~/{}/testing", blogs[0].actor_id),
subtitle: "Bye".to_string(), subtitle: String::new(),
source: "Hello".to_string(), source: String::new(),
cover_id: None, cover_id: None,
}, },
) )
@@ -268,7 +274,7 @@ pub(crate) mod tests {
"actor": users[0].ap_url, "actor": users[0].ap_url,
"object": { "object": {
"type": "Article", "type": "Article",
"id": "https://plu.me/~/BlogName/testing", "id": "https://plu.me/~/Blog/my-article",
"attributedTo": [users[0].ap_url, blogs[0].ap_url], "attributedTo": [users[0].ap_url, blogs[0].ap_url],
"content": "Hello.", "content": "Hello.",
"name": "My Article", "name": "My Article",
@@ -286,11 +292,11 @@ pub(crate) mod tests {
match super::inbox(&conn, act).unwrap() { match super::inbox(&conn, act).unwrap() {
super::InboxResult::Post(p) => { super::InboxResult::Post(p) => {
assert!(p.is_author(&conn, users[0].id).unwrap()); assert!(p.is_author(&conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello".to_owned()); assert_eq!(p.source, "Hello.".to_owned());
assert_eq!(p.blog_id, blogs[0].id); assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello")); assert_eq!(p.content, SafeString::new("Hello."));
assert_eq!(p.subtitle, "Bye".to_owned()); assert_eq!(p.subtitle, "Bye.".to_owned());
assert_eq!(p.title, "Testing".to_owned()); assert_eq!(p.title, "My Article".to_owned());
} }
_ => panic!("Unexpected result"), _ => panic!("Unexpected result"),
}; };
+144 -44
View File
@@ -3,14 +3,29 @@ use crate::{
medias::Media, medias::Media,
safe_string::SafeString, safe_string::SafeString,
schema::{instances, users}, schema::{instances, users},
users::{NewUser, Role, User}, users::{Role, User},
Connection, Error, Result, Connection, Error, Result,
}; };
use activitypub::{actor::Service, CustomObject};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use once_cell::sync::OnceCell; use openssl::{
use plume_common::utils::md_to_html; hash::MessageDigest,
pkey::{PKey, Private},
rsa::Rsa,
sign,
};
use plume_common::{
activity_pub::{
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
ApSignature, PublicKey,
},
utils::md_to_html,
};
use std::sync::RwLock; use std::sync::RwLock;
use tracing::warn;
pub type CustomService = CustomObject<ApSignature, Service>;
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Instance { pub struct Instance {
@@ -26,6 +41,8 @@ pub struct Instance {
pub default_license: String, pub default_license: String,
pub long_description_html: SafeString, pub long_description_html: SafeString,
pub short_description_html: SafeString, pub short_description_html: SafeString,
pub private_key: Option<String>,
pub public_key: Option<String>,
} }
#[derive(Clone, Insertable)] #[derive(Clone, Insertable)]
@@ -40,15 +57,14 @@ pub struct NewInstance {
pub default_license: String, pub default_license: String,
pub long_description_html: String, pub long_description_html: String,
pub short_description_html: String, pub short_description_html: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
} }
lazy_static! { lazy_static! {
static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None); static ref LOCAL_INSTANCE: RwLock<Option<Instance>> = RwLock::new(None);
} }
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
static LOCAL_INSTANCE_USER: OnceCell<User> = OnceCell::new();
impl Instance { impl Instance {
pub fn set_local(self) { pub fn set_local(self) {
LOCAL_INSTANCE.write().unwrap().replace(self); LOCAL_INSTANCE.write().unwrap().replace(self);
@@ -73,6 +89,13 @@ impl Instance {
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok(); *LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok();
} }
pub fn get_locals(conn: &Connection) -> Result<Vec<Instance>> {
instances::table
.filter(instances::local.eq(true))
.load::<Instance>(conn)
.map_err(Error::from)
}
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> { pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
instances::table instances::table
.filter(instances::local.eq(false)) .filter(instances::local.eq(false))
@@ -80,42 +103,6 @@ impl Instance {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn create_local_instance_user(conn: &Connection) -> Result<User> {
let instance = Instance::get_local()?;
let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain);
NewUser::new_local(
conn,
LOCAL_INSTANCE_USERNAME.into(),
instance.public_domain,
Role::Instance,
"Local instance",
email,
None,
)
}
pub fn get_local_instance_user() -> Option<&'static User> {
LOCAL_INSTANCE_USER.get()
}
pub fn get_local_instance_user_uncached(conn: &Connection) -> Result<User> {
users::table
.filter(users::role.eq(3))
.first(conn)
.or_else(|err| match err {
NotFound => Self::create_local_instance_user(conn),
_ => Err(Error::Db(err)),
})
}
pub fn cache_local_instance_user(conn: &Connection) {
let _ = LOCAL_INSTANCE_USER.get_or_init(|| {
Self::get_local_instance_user_uncached(conn)
.or_else(|_| Self::create_local_instance_user(conn))
.expect("Failed to cache local instance user")
});
}
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> { pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
instances::table instances::table
.order(instances::public_domain.asc()) .order(instances::public_domain.asc())
@@ -278,6 +265,112 @@ impl Instance {
}) })
.map_err(Error::from) .map_err(Error::from)
} }
pub fn set_keypair(&self, conn: &Connection) -> Result<()> {
let (pub_key, priv_key) = gen_keypair();
let private_key = String::from_utf8(priv_key).or(Err(Error::Signature))?;
let public_key = String::from_utf8(pub_key).or(Err(Error::Signature))?;
diesel::update(self)
.set((
instances::private_key.eq(Some(private_key)),
instances::public_key.eq(Some(public_key)),
))
.execute(conn)
.and(Ok(()))
.map_err(Error::from)
}
pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone()?.as_ref(),
)?)
.map_err(Error::from)
}
/// This is experimental and might change in the future.
/// Currently "!" sign is used but it's not decided.
pub fn ap_url(&self) -> String {
ap_url(&format!(
"{}/!/{}",
Self::get_local().unwrap().public_domain,
self.public_domain
))
}
pub fn to_activity(&self) -> Result<CustomService> {
let mut actor = Service::default();
let id = self.ap_url();
actor.object_props.set_id_string(id.clone())?;
actor.object_props.set_name_string(self.name.clone())?;
let mut ap_signature = ApSignature::default();
if self.local {
if let Some(pub_key) = self.public_key.clone() {
let mut public_key = PublicKey::default();
public_key.set_id_string(format!("{}#main-key", id))?;
public_key.set_owner_string(id)?;
public_key.set_public_key_pem_string(pub_key)?;
ap_signature.set_public_key_publickey(public_key)?;
}
};
Ok(CustomService::new(actor, ap_signature))
}
}
impl NewInstance {
pub fn new_local(
conn: &Connection,
public_domain: String,
name: String,
open_registrations: bool,
default_license: String,
) -> Result<Instance> {
let (pub_key, priv_key) = gen_keypair();
Instance::insert(
conn,
NewInstance {
public_domain,
name,
local: true,
open_registrations,
short_description: SafeString::new(""),
long_description: SafeString::new(""),
default_license,
long_description_html: String::new(),
short_description_html: String::new(),
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
public_key: Some(String::from_utf8(pub_key).or(Err(Error::Signature))?),
},
)
}
}
impl Signer for Instance {
fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url())
}
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(SignatureError::from)
}
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
if self.public_key.is_none() {
warn!("missing public key for {}", self.public_domain);
return Err(SignatureError());
}
let key = PKey::from_rsa(Rsa::public_key_from_pem(
self.public_key.clone().unwrap().as_ref(),
)?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(SignatureError::from)
}
} }
#[cfg(test)] #[cfg(test)]
@@ -299,6 +392,8 @@ pub(crate) mod tests {
name: "My instance".to_string(), name: "My instance".to_string(),
open_registrations: true, open_registrations: true,
public_domain: "plu.me".to_string(), public_domain: "plu.me".to_string(),
private_key: None,
public_key: None,
}, },
NewInstance { NewInstance {
default_license: "WTFPL".to_string(), default_license: "WTFPL".to_string(),
@@ -310,6 +405,8 @@ pub(crate) mod tests {
name: "An instance".to_string(), name: "An instance".to_string(),
open_registrations: true, open_registrations: true,
public_domain: "1plu.me".to_string(), public_domain: "1plu.me".to_string(),
private_key: None,
public_key: None,
}, },
NewInstance { NewInstance {
default_license: "CC-0".to_string(), default_license: "CC-0".to_string(),
@@ -321,6 +418,8 @@ pub(crate) mod tests {
name: "Someone instance".to_string(), name: "Someone instance".to_string(),
open_registrations: false, open_registrations: false,
public_domain: "2plu.me".to_string(), public_domain: "2plu.me".to_string(),
private_key: None,
public_key: None,
}, },
NewInstance { NewInstance {
default_license: "CC-0-BY-SA".to_string(), default_license: "CC-0-BY-SA".to_string(),
@@ -332,6 +431,8 @@ pub(crate) mod tests {
name: "Nice day".to_string(), name: "Nice day".to_string(),
open_registrations: true, open_registrations: true,
public_domain: "3plu.me".to_string(), public_domain: "3plu.me".to_string(),
private_key: None,
public_key: None,
}, },
] ]
.into_iter() .into_iter()
@@ -344,7 +445,6 @@ pub(crate) mod tests {
}) })
.collect(); .collect();
Instance::cache_local(conn); Instance::cache_local(conn);
Instance::cache_local_instance_user(conn);
res res
} }
Regular → Executable
+26 -70
View File
@@ -1,3 +1,4 @@
#![feature(try_trait)]
#![feature(never_type)] #![feature(never_type)]
#![feature(proc_macro_hygiene)] #![feature(proc_macro_hygiene)]
#![feature(box_patterns)] #![feature(box_patterns)]
@@ -16,11 +17,10 @@ extern crate serde_json;
#[macro_use] #[macro_use]
extern crate tantivy; extern crate tantivy;
use activitystreams::iri_string; use db_conn::DbPool;
pub use lettre; use instance::Instance;
pub use lettre::smtp;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use plume_common::activity_pub::{inbox::InboxError, request, sign}; use plume_common::activity_pub::{inbox::InboxError, sign};
use posts::PostEvent; use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder}; use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent; use users::UserEvent;
@@ -68,7 +68,6 @@ pub enum Error {
Url, Url,
Webfinger, Webfinger,
Expired, Expired,
UserAlreadyExists,
} }
impl From<bcrypt::BcryptError> for Error { impl From<bcrypt::BcryptError> for Error {
@@ -95,14 +94,14 @@ impl From<diesel::result::Error> for Error {
} }
} }
impl From<url::ParseError> for Error { impl From<std::option::NoneError> for Error {
fn from(_: url::ParseError) -> Self { fn from(_: std::option::NoneError) -> Self {
Error::Url Error::NotFound
} }
} }
impl From<iri_string::validate::Error> for Error { impl From<url::ParseError> for Error {
fn from(_: iri_string::validate::Error) -> Self { fn from(_: url::ParseError) -> Self {
Error::Url Error::Url
} }
} }
@@ -125,9 +124,12 @@ impl From<reqwest::header::InvalidHeaderValue> for Error {
} }
} }
impl From<activitystreams::checked::CheckError> for Error { impl From<activitypub::Error> for Error {
fn from(_: activitystreams::checked::CheckError) -> Error { fn from(err: activitypub::Error) -> Self {
Error::MissingApProperty match err {
activitypub::Error::NotFound => Error::MissingApProperty,
_ => Error::SerDe,
}
} }
} }
@@ -164,14 +166,14 @@ impl From<InboxError<Error>> for Error {
} }
} }
impl From<request::Error> for Error { pub type Result<T> = std::result::Result<T, Error>;
fn from(_err: request::Error) -> Error {
Error::Request impl From<Error> for sign::Error {
fn from(_: Error) -> Self {
Self()
} }
} }
pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first /// Adds a function to a model, that returns the first
/// matching row for a given list of fields. /// matching row for a given list of fields.
/// ///
@@ -307,38 +309,21 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url) format!("https://{}", url)
} }
pub trait SmtpNewWithAddr { pub fn migrate_data(dbpool: &DbPool) -> Result<()> {
fn new_with_addr( ensure_local_instance_keys(&dbpool.get().unwrap())
addr: (&str, u16),
) -> std::result::Result<smtp::SmtpClient, smtp::error::Error>;
} }
impl SmtpNewWithAddr for smtp::SmtpClient { fn ensure_local_instance_keys(conn: &Connection) -> Result<()> {
// Stolen from lettre::smtp::SmtpClient::new_simple() for instance in Instance::get_locals(conn)? {
fn new_with_addr(addr: (&str, u16)) -> std::result::Result<Self, smtp::error::Error> { instance.set_keypair(conn)?;
use native_tls::TlsConnector;
use smtp::{
client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS},
ClientSecurity, SmtpClient,
};
let (domain, port) = addr;
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new((domain, port), ClientSecurity::Wrapper(tls_parameters))
} }
Ok(())
} }
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
mod tests { mod tests {
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG}; use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG};
use chrono::{naive::NaiveDateTime, Datelike, Timelike};
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use std::env::temp_dir; use std::env::temp_dir;
@@ -371,33 +356,6 @@ mod tests {
pool pool
}; };
} }
#[cfg(feature = "postgres")]
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
dt.timestamp_subsec_micros()
)
}
#[cfg(feature = "sqlite")]
pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String {
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second()
)
}
} }
pub mod admin; pub mod admin;
@@ -409,7 +367,6 @@ pub mod blogs;
pub mod comment_seers; pub mod comment_seers;
pub mod comments; pub mod comments;
pub mod db_conn; pub mod db_conn;
pub mod email_signups;
pub mod follows; pub mod follows;
pub mod headers; pub mod headers;
pub mod inbox; pub mod inbox;
@@ -430,7 +387,6 @@ pub mod safe_string;
#[allow(unused_imports)] #[allow(unused_imports)]
pub mod schema; pub mod schema;
pub mod search; pub mod search;
pub mod signups;
pub mod tags; pub mod tags;
pub mod timeline; pub mod timeline;
pub mod users; pub mod users;
+38 -114
View File
@@ -2,18 +2,12 @@ use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG, users::User, Connection, Error, Result, CONFIG,
}; };
use activitystreams::{ use activitypub::activity;
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
base::AnyBase,
iri_string::types::IriString,
prelude::*,
};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, Id, IntoId, PUBLIC_VISIBILITY,
PUBLIC_VISIBILITY,
}; };
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
@@ -39,16 +33,18 @@ impl Like {
find_by!(likes, find_by_ap_url, ap_url as &str); find_by!(likes, find_by_ap_url, ap_url as &str);
find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32); find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32);
pub fn to_activity(&self, conn: &Connection) -> Result<LikeAct> { pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
let mut act = LikeAct::new( let mut act = activity::Like::default();
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, act.like_props
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?, .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
); act.like_props
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); .set_object_link(Post::get(conn, self.post_id)?.into_id())?;
act.set_many_ccs(vec![User::get(conn, self.user_id)? act.object_props
.followers_endpoint .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
.parse::<IriString>()?]); act.object_props.set_cc_link_vec(vec![Id::new(
act.set_id(self.ap_url.parse::<IriString>()?); User::get(conn, self.user_id)?.followers_endpoint,
)])?;
act.object_props.set_id_string(self.ap_url.clone())?;
Ok(act) Ok(act)
} }
@@ -70,22 +66,24 @@ impl Like {
Ok(()) Ok(())
} }
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { pub fn build_undo(&self, conn: &Connection) -> Result<activity::Undo> {
let mut act = Undo::new( let mut act = activity::Undo::default();
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, act.undo_props
AnyBase::from_extended(self.to_activity(conn)?)?, .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
); act.undo_props.set_object_object(self.to_activity(conn)?)?;
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?); act.object_props
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); .set_id_string(format!("{}#delete", self.ap_url))?;
act.set_many_ccs(vec![User::get(conn, self.user_id)? act.object_props
.followers_endpoint .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
.parse::<IriString>()?]); act.object_props.set_cc_link_vec(vec![Id::new(
User::get(conn, self.user_id)?.followers_endpoint,
)])?;
Ok(act) Ok(act)
} }
} }
impl AsObject<User, LikeAct, &DbConn> for Post { impl AsObject<User, activity::Like, &DbConn> for Post {
type Error = Error; type Error = Error;
type Output = Like; type Output = Like;
@@ -107,22 +105,20 @@ impl AsObject<User, LikeAct, &DbConn> for Post {
impl FromId<DbConn> for Like { impl FromId<DbConn> for Like {
type Error = Error; type Error = Error;
type Object = LikeAct; type Object = activity::Like;
fn from_db(conn: &DbConn, id: &str) -> Result<Self> { fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
Like::find_by_ap_url(conn, id) Like::find_by_ap_url(conn, id)
} }
fn from_activity(conn: &DbConn, act: LikeAct) -> Result<Self> { fn from_activity(conn: &DbConn, act: activity::Like) -> Result<Self> {
let res = Like::insert( let res = Like::insert(
conn, conn,
NewLike { NewLike {
post_id: Post::from_id( post_id: Post::from_id(
conn, conn,
act.object_field_ref() &Instance::get_local().expect("Failed to get local instance"),
.as_single_id() &act.like_props.object_link::<Id>()?,
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@@ -130,31 +126,22 @@ impl FromId<DbConn> for Like {
.id, .id,
user_id: User::from_id( user_id: User::from_id(
conn, conn,
act.actor_field_ref() &Instance::get_local().expect("Failed to get local instance"),
.as_single_id() &act.like_props.actor_link::<Id>()?,
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
ap_url: act ap_url: act.object_props.id_string()?,
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
}, },
)?; )?;
res.notify(conn)?; res.notify(conn)?;
Ok(res) Ok(res)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Undo, &DbConn> for Like { impl AsObject<User, activity::Undo, &DbConn> for Like {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
@@ -163,7 +150,7 @@ impl AsObject<User, Undo, &DbConn> for Like {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { if let Ok(notif) = Notification::find(&conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(&**conn)?;
} }
Ok(()) Ok(())
@@ -175,7 +162,8 @@ impl AsObject<User, Undo, &DbConn> for Like {
impl NewLike { impl NewLike {
pub fn new(p: &Post, u: &User) -> Self { pub fn new(p: &Post, u: &User) -> Self {
let ap_url = format!("{}like/{}", u.ap_url, p.ap_url); // TODO: this URL is not valid
let ap_url = format!("{}/like/{}", u.ap_url, p.ap_url);
NewLike { NewLike {
post_id: p.id, post_id: p.id,
user_id: u.id, user_id: u.id,
@@ -183,67 +171,3 @@ impl NewLike {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::diesel::Connection;
use crate::{inbox::tests::fill_database, tests::db};
use assert_json_diff::assert_json_eq;
use serde_json::{json, to_value};
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let like = Like::insert(&*conn, NewLike::new(post, user))?;
let act = like.to_activity(&conn).unwrap();
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Like",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_undo() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let like = Like::insert(&*conn, NewLike::new(post, user))?;
let act = like.build_undo(&*conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete",
"object": {
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Like",
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Undo",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}
-1
View File
@@ -143,7 +143,6 @@ macro_rules! func {
} }
} }
#[allow(dead_code)]
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
struct ListElem { struct ListElem {
pub id: i32, pub id: i32,
+37 -47
View File
@@ -2,12 +2,13 @@ use crate::{
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias, ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
users::User, Connection, Error, Result, CONFIG, users::User, Connection, Error, Result, CONFIG,
}; };
use activitystreams::{object::Image, prelude::*}; use activitypub::object::Image;
use askama_escape::escape;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use guid_create::GUID; use guid_create::GUID;
use plume_common::{ use plume_common::{
activity_pub::{inbox::FromId, request, ToAsString, ToAsUri}, activity_pub::{inbox::FromId, Id},
utils::{escape, MediaProcessor}, utils::MediaProcessor,
}; };
use std::{ use std::{
fs::{self, DirBuilder}, fs::{self, DirBuilder},
@@ -103,8 +104,8 @@ impl Media {
pub fn category(&self) -> MediaCategory { pub fn category(&self) -> MediaCategory {
match &*self match &*self
.file_path .file_path
.rsplit_once('.') .rsplitn(2, '.')
.map(|x| x.1) .next()
.expect("Media::category: extension error") .expect("Media::category: extension error")
.to_lowercase() .to_lowercase()
{ {
@@ -207,36 +208,33 @@ impl Media {
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> { pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
let remote_url = image let remote_url = image.object_props.url_string().ok()?;
.url()
.and_then(|url| url.to_as_uri())
.ok_or(Error::MissingApProperty)?;
let path = determine_mirror_file_path(&remote_url); let path = determine_mirror_file_path(&remote_url);
let parent = path.parent().ok_or(Error::InvalidValue)?; let parent = path.parent()?;
if !parent.is_dir() { if !parent.is_dir() {
DirBuilder::new().recursive(true).create(parent)?; DirBuilder::new().recursive(true).create(parent)?;
} }
let mut dest = fs::File::create(path.clone())?; let mut dest = fs::File::create(path.clone()).ok()?;
// TODO: conditional GET // TODO: conditional GET
request::get( if let Some(proxy) = CONFIG.proxy() {
remote_url.as_str(), reqwest::ClientBuilder::new().proxy(proxy.clone()).build()?
User::get_sender(), } else {
CONFIG.proxy().cloned(), reqwest::Client::new()
)? }
.copy_to(&mut dest)?; .get(remote_url.as_str())
.send()
.ok()?
.copy_to(&mut dest)
.ok()?;
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?) Media::find_by_file_path(conn, &path.to_str()?)
.and_then(|mut media| { .and_then(|mut media| {
let mut updated = false; let mut updated = false;
let alt_text = image let alt_text = image.object_props.content_string().ok()?;
.content() let sensitive = image.object_props.summary_string().is_ok();
.and_then(|content| content.to_as_string()) let content_warning = image.object_props.summary_string().ok();
.ok_or(Error::NotFound)?;
let summary = image.summary().and_then(|summary| summary.to_as_string());
let sensitive = summary.is_some();
let content_warning = summary;
if media.alt_text != alt_text { if media.alt_text != alt_text {
media.alt_text = alt_text; media.alt_text = alt_text;
updated = true; updated = true;
@@ -263,25 +261,25 @@ impl Media {
Ok(media) Ok(media)
}) })
.or_else(|_| { .or_else(|_| {
let summary = image.summary().and_then(|summary| summary.to_as_string());
Media::insert( Media::insert(
conn, conn,
NewMedia { NewMedia {
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(), file_path: path.to_str()?.to_string(),
alt_text: image alt_text: image.object_props.content_string().ok()?,
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::NotFound)?,
is_remote: false, is_remote: false,
remote_url: None, remote_url: None,
sensitive: summary.is_some(), sensitive: image.object_props.summary_string().is_ok(),
content_warning: summary, content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id( owner_id: User::from_id(
conn, conn,
&image &Instance::get_local().expect("Failed to get local instance"),
.attributed_to() image
.and_then(|attributed_to| attributed_to.to_as_uri()) .object_props
.ok_or(Error::MissingApProperty)?, .attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@@ -328,7 +326,7 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
.next() .next()
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png")); .unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand(), ext)); file_path.push(format!("{}.{}", GUID::rand().to_string(), ext));
}); });
file_path file_path
} }
@@ -398,15 +396,7 @@ pub(crate) mod tests {
pub(crate) fn clean(conn: &Conn) { pub(crate) fn clean(conn: &Conn) {
//used to remove files generated by tests //used to remove files generated by tests
for media in Media::list_all_medias(conn).unwrap() { for media in Media::list_all_medias(conn).unwrap() {
if let Some(err) = media.delete(conn).err() { media.delete(conn).unwrap();
match &err {
Error::Io(e) => match e.kind() {
std::io::ErrorKind::NotFound => (),
_ => panic!("{:?}", err),
},
_ => panic!("{:?}", err),
}
}
} }
} }
+12 -77
View File
@@ -2,11 +2,7 @@ use crate::{
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions, comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
users::User, Connection, Error, Result, users::User, Connection, Error, Result,
}; };
use activitystreams::{ use activitypub::link;
base::BaseExt,
iri_string::types::IriString,
link::{self, LinkExt},
};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::inbox::AsActor; use plume_common::activity_pub::inbox::AsActor;
@@ -51,28 +47,26 @@ impl Mention {
pub fn get_user(&self, conn: &Connection) -> Result<User> { pub fn get_user(&self, conn: &Connection) -> Result<User> {
match self.get_post(conn) { match self.get_post(conn) {
Ok(p) => Ok(p Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
.get_authors(conn)?
.into_iter()
.next()
.ok_or(Error::NotFound)?),
Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)), Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
} }
} }
pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> { pub fn build_activity(conn: &DbConn, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment)?; let user = User::find_by_fqn(conn, ment)?;
let mut mention = link::Mention::new(); let mut mention = link::Mention::default();
mention.set_href(user.ap_url.parse::<IriString>()?); mention.link_props.set_href_string(user.ap_url)?;
mention.set_name(format!("@{}", ment)); mention.link_props.set_name_string(format!("@{}", ment))?;
Ok(mention) Ok(mention)
} }
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> { pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
let user = self.get_mentioned(conn)?; let user = self.get_mentioned(conn)?;
let mut mention = link::Mention::new(); let mut mention = link::Mention::default();
mention.set_href(user.ap_url.parse::<IriString>()?); mention.link_props.set_href_string(user.ap_url.clone())?;
mention.set_name(format!("@{}", user.fqn)); mention
.link_props
.set_name_string(format!("@{}", user.fqn))?;
Ok(mention) Ok(mention)
} }
@@ -83,8 +77,8 @@ impl Mention {
in_post: bool, in_post: bool,
notify: bool, notify: bool,
) -> Result<Self> { ) -> Result<Self> {
let ap_url = ment.href().ok_or(Error::NotFound)?.as_str(); let ap_url = ment.link_props.href_string().ok()?;
let mentioned = User::find_by_ap_url(conn, ap_url)?; let mentioned = User::find_by_ap_url(conn, &ap_url)?;
if in_post { if in_post {
Post::get(conn, inside).and_then(|post| { Post::get(conn, inside).and_then(|post| {
@@ -147,62 +141,3 @@ impl Mention {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{inbox::tests::fill_database, tests::db, Error};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::{json, to_value};
#[test]
fn build_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (_posts, users, _blogs) = fill_database(&conn);
let user = &users[0];
let name = &user.username;
let act = Mention::build_activity(&conn, name)?;
let expected = json!({
"href": "https://plu.me/@/admin/",
"name": "@admin",
"type": "Mention",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &users[0];
let mention = Mention::insert(
&conn,
NewMention {
mentioned_id: user.id,
post_id: Some(post.id),
comment_id: None,
},
)?;
let act = mention.to_activity(&conn)?;
let expected = json!({
"href": "https://plu.me/@/admin/",
"name": "@admin",
"type": "Mention",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}
+1 -2
View File
@@ -105,8 +105,7 @@ impl ImportedMigrations {
pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> { pub fn rerun_last_migration(&self, conn: &Connection, path: &Path) -> Result<()> {
let latest_migration = conn.latest_run_migration_version()?; let latest_migration = conn.latest_run_migration_version()?;
let id = latest_migration let id = latest_migration
.and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok()) .and_then(|m| self.0.binary_search_by_key(&m.as_str(), |m| m.name).ok())?;
.ok_or(Error::NotFound)?;
let migration = &self.0[id]; let migration = &self.0[id];
conn.transaction(|| { conn.transaction(|| {
migration.revert(conn, path)?; migration.revert(conn, path)?;
+1 -1
View File
@@ -61,7 +61,7 @@ impl PasswordResetRequest {
} }
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> { pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
let request = Self::find_by_token(conn, token)?; let request = Self::find_by_token(&conn, &token)?;
let filter = let filter =
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id)); password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
+237 -458
View File
@@ -3,24 +3,19 @@ use crate::{
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN, Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
}; };
use activitystreams::{ use activitypub::{
activity::{Create, Delete, Update}, activity::{Create, Delete, Update},
base::{AnyBase, Base}, link,
iri_string::types::IriString, object::{Article, Image, Tombstone},
link::{self, kind::MentionType}, CustomObject,
object::{kind::ImageType, ApObject, Article, AsApObject, Image, ObjectExt, Tombstone},
prelude::*,
time::OffsetDateTime,
}; };
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY,
Hashtag, HashtagType, Id, IntoId, Licensed, LicensedArticle, ToAsString, ToAsUri,
PUBLIC_VISIBILITY,
}, },
utils::{iri_percent_encode_seg, md_to_html}, utils::{iri_percent_encode_seg, md_to_html},
}; };
@@ -28,6 +23,8 @@ use riker::actors::{Publish, Tell};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
pub type LicensedArticle = CustomObject<Licensed, Article>;
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new())); static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)] #[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
@@ -69,15 +66,15 @@ impl Post {
find_by!(posts, find_by_ap_url, ap_url as &str); find_by!(posts, find_by_ap_url, ap_url as &str);
last!(posts); last!(posts);
pub fn insert(conn: &Connection, mut new: NewPost) -> Result<Self> { pub fn insert(conn: &Connection, new: NewPost) -> Result<Self> {
if new.ap_url.is_empty() {
let blog = Blog::get(conn, new.blog_id)?;
new.ap_url = Self::ap_url(blog, &new.slug);
}
diesel::insert_into(posts::table) diesel::insert_into(posts::table)
.values(new) .values(new)
.execute(conn)?; .execute(conn)?;
let post = Self::last(conn)?; let mut post = Self::last(conn)?;
if post.ap_url.is_empty() {
post.ap_url = Self::ap_url(post.get_blog(conn)?, &post.slug);
let _: Post = post.save_changes(conn)?;
}
if post.published { if post.published {
post.publish_published(); post.publish_published();
@@ -100,7 +97,7 @@ impl Post {
} }
pub fn delete(&self, conn: &Connection) -> Result<()> { pub fn delete(&self, conn: &Connection) -> Result<()> {
for m in Mention::list_for_post(conn, self.id)? { for m in Mention::list_for_post(&conn, self.id)? {
m.delete(conn)?; m.delete(conn)?;
} }
diesel::delete(self).execute(conn)?; diesel::delete(self).execute(conn)?;
@@ -355,92 +352,92 @@ impl Post {
.collect::<Vec<serde_json::Value>>(); .collect::<Vec<serde_json::Value>>();
mentions_json.append(&mut tags_json); mentions_json.append(&mut tags_json);
let mut article = ApObject::new(Article::new()); let mut article = Article::default();
article.set_name(self.title.clone()); article.object_props.set_name_string(self.title.clone())?;
article.set_id(self.ap_url.parse::<IriString>()?); article.object_props.set_id_string(self.ap_url.clone())?;
let mut authors = self let mut authors = self
.get_authors(conn)? .get_authors(conn)?
.into_iter() .into_iter()
.filter_map(|x| x.ap_url.parse::<IriString>().ok()) .map(|x| Id::new(x.ap_url))
.collect::<Vec<IriString>>(); .collect::<Vec<Id>>();
authors.push(self.get_blog(conn)?.ap_url.parse::<IriString>()?); // add the blog URL here too authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too
article.set_many_attributed_tos(authors); article
article.set_content(self.content.get().clone()); .object_props
let source = AnyBase::from_arbitrary_json(serde_json::json!({ .set_attributed_to_link_vec::<Id>(authors)?;
"content": self.source, article
"mediaType": "text/markdown", .object_props
}))?; .set_content_string(self.content.get().clone())?;
article.set_source(source); article.ap_object_props.set_source_object(Source {
article.set_published( content: self.source.clone(),
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into()) media_type: String::from("text/markdown"),
.expect("OffsetDateTime"), })?;
); article
article.set_summary(&*self.subtitle); .object_props
article.set_many_tags( .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?;
mentions_json article
.iter() .object_props
.filter_map(|mention_json| AnyBase::from_arbitrary_json(mention_json).ok()), .set_summary_string(self.subtitle.clone())?;
); article.object_props.tag = Some(json!(mentions_json));
if let Some(media_id) = self.cover_id { if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id)?; let media = Media::get(conn, media_id)?;
let mut cover = Image::new(); let mut cover = Image::default();
cover.set_url(media.url()?); cover.object_props.set_url_string(media.url()?)?;
if media.sensitive { if media.sensitive {
cover.set_summary(media.content_warning.unwrap_or_default()); cover
.object_props
.set_summary_string(media.content_warning.unwrap_or_default())?;
} }
cover.set_content(media.alt_text); cover.object_props.set_content_string(media.alt_text)?;
cover.set_many_attributed_tos(vec![User::get(conn, media.owner_id)? cover
.ap_url .object_props
.parse::<IriString>()?]); .set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?;
article.set_icon(cover.into_any_base()?); article.object_props.set_icon_object(cover)?;
} }
article.set_url(self.ap_url.parse::<IriString>()?); article.object_props.set_url_string(self.ap_url.clone())?;
article.set_many_tos( article
to.into_iter() .object_props
.filter_map(|to| to.parse::<IriString>().ok()) .set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
.collect::<Vec<IriString>>(), article
); .object_props
article.set_many_ccs( .set_cc_link_vec::<Id>(cc.into_iter().map(Id::new).collect())?;
cc.into_iter() let mut license = Licensed::default();
.filter_map(|cc| cc.parse::<IriString>().ok()) license.set_license_string(self.license.clone())?;
.collect::<Vec<IriString>>(),
);
let license = Licensed {
license: Some(self.license.clone()),
};
Ok(LicensedArticle::new(article, license)) Ok(LicensedArticle::new(article, license))
} }
pub fn create_activity(&self, conn: &Connection) -> Result<Create> { pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
let article = self.to_activity(conn)?; let article = self.to_activity(conn)?;
let to = article.to().ok_or(Error::MissingApProperty)?.clone(); let mut act = Create::default();
let cc = article.cc().ok_or(Error::MissingApProperty)?.clone(); act.object_props
let mut act = Create::new( .set_id_string(format!("{}activity", self.ap_url))?;
self.get_authors(conn)?[0].ap_url.parse::<IriString>()?, act.object_props
Base::retract(article)?.into_generic()?, .set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
); act.object_props
act.set_id(format!("{}/activity", self.ap_url).parse::<IriString>()?); .set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
act.set_many_tos(to); act.create_props
act.set_many_ccs(cc); .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
act.create_props.set_object_object(article)?;
Ok(act) Ok(act)
} }
pub fn update_activity(&self, conn: &Connection) -> Result<Update> { pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
let article = self.to_activity(conn)?; let article = self.to_activity(conn)?;
let to = article.to().ok_or(Error::MissingApProperty)?.clone(); let mut act = Update::default();
let cc = article.cc().ok_or(Error::MissingApProperty)?.clone(); act.object_props.set_id_string(format!(
let mut act = Update::new( "{}/update-{}",
self.get_authors(conn)?[0].ap_url.parse::<IriString>()?, self.ap_url,
Base::retract(article)?.into_generic()?, Utc::now().timestamp()
); ))?;
act.set_id( act.object_props
format!("{}/update-{}", self.ap_url, Utc::now().timestamp()).parse::<IriString>()?, .set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
); act.object_props
act.set_many_tos(to); .set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
act.set_many_ccs(cc); act.update_props
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
act.update_props.set_object_object(article)?;
Ok(act) Ok(act)
} }
@@ -449,8 +446,10 @@ impl Post {
.into_iter() .into_iter()
.map(|m| { .map(|m| {
( (
m.href() m.link_props
.and_then(|ap_url| User::find_by_ap_url(conn, ap_url.as_ref()).ok()) .href_string()
.ok()
.and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok())
.map(|u| u.id), .map(|u| u.id),
m, m,
) )
@@ -458,14 +457,14 @@ impl Post {
.filter_map(|(id, m)| id.map(|id| (m, id))) .filter_map(|(id, m)| id.map(|id| (m, id)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let old_mentions = Mention::list_for_post(conn, self.id)?; let old_mentions = Mention::list_for_post(&conn, self.id)?;
let old_user_mentioned = old_mentions let old_user_mentioned = old_mentions
.iter() .iter()
.map(|m| m.mentioned_id) .map(|m| m.mentioned_id)
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
for (m, id) in &mentions { for (m, id) in &mentions {
if !old_user_mentioned.contains(id) { if !old_user_mentioned.contains(&id) {
Mention::from_activity(&*conn, m, self.id, true, true)?; Mention::from_activity(&*conn, &m, self.id, true, true)?;
} }
} }
@@ -477,7 +476,7 @@ impl Post {
.iter() .iter()
.filter(|m| !new_mentions.contains(&m.mentioned_id)) .filter(|m| !new_mentions.contains(&m.mentioned_id))
{ {
m.delete(conn)?; m.delete(&conn)?;
} }
Ok(()) Ok(())
} }
@@ -485,7 +484,7 @@ impl Post {
pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> { pub fn update_tags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags let tags_name = tags
.iter() .iter()
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string())) .filter_map(|t| t.name_string().ok())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?; let old_tags = Tag::for_post(&*conn, self.id)?;
@@ -502,9 +501,8 @@ impl Post {
for t in tags { for t in tags {
if !t if !t
.name .name_string()
.as_ref() .map(|n| old_tags_name.contains(&n))
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true) .unwrap_or(true)
{ {
Tag::from_activity(conn, &t, self.id, false)?; Tag::from_activity(conn, &t, self.id, false)?;
@@ -522,7 +520,7 @@ impl Post {
pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> { pub fn update_hashtags(&self, conn: &Connection, tags: Vec<Hashtag>) -> Result<()> {
let tags_name = tags let tags_name = tags
.iter() .iter()
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string())) .filter_map(|t| t.name_string().ok())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
let old_tags = Tag::for_post(&*conn, self.id)?; let old_tags = Tag::for_post(&*conn, self.id)?;
@@ -539,9 +537,8 @@ impl Post {
for t in tags { for t in tags {
if !t if !t
.name .name_string()
.as_ref() .map(|n| old_tags_name.contains(&n))
.map(|n| old_tags_name.contains(n.as_str()))
.unwrap_or(true) .unwrap_or(true)
{ {
Tag::from_activity(conn, &t, self.id, true)?; Tag::from_activity(conn, &t, self.id, true)?;
@@ -568,19 +565,18 @@ impl Post {
} }
pub fn build_delete(&self, conn: &Connection) -> Result<Delete> { pub fn build_delete(&self, conn: &Connection) -> Result<Delete> {
let mut tombstone = Tombstone::new(); let mut act = Delete::default();
tombstone.set_id(self.ap_url.parse()?); act.delete_props
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
let mut act = Delete::new( let mut tombstone = Tombstone::default();
self.get_authors(conn)?[0] tombstone.object_props.set_id_string(self.ap_url.clone())?;
.clone() act.delete_props.set_object_object(tombstone)?;
.into_id()
.parse::<IriString>()?,
Base::retract(tombstone)?.into_generic()?,
);
act.set_id(format!("{}#delete", self.ap_url).parse()?); act.object_props
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); .set_id_string(format!("{}#delete", self.ap_url))?;
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
Ok(act) Ok(act)
} }
@@ -624,82 +620,62 @@ impl FromId<DbConn> for Post {
} }
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> { fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
let license = article.ext_one.license.unwrap_or_default(); let conn = conn;
let article = article.inner; let license = article.custom_props.license_string().unwrap_or_default();
let article = article.object;
let (blog, authors) = article let (blog, authors) = article
.ap_object_ref() .object_props
.attributed_to() .attributed_to_link_vec::<Id>()?
.ok_or(Error::MissingApProperty)? .into_iter()
.iter()
.fold((None, vec![]), |(blog, mut authors), link| { .fold((None, vec![]), |(blog, mut authors), link| {
if let Some(url) = link.id() { let url = link;
match User::from_id(conn, url.as_str(), None, CONFIG.proxy()) { match User::from_id(
Ok(u) => { conn,
authors.push(u); &Instance::get_local().expect("Failed to get local instance"),
(blog, authors) &url,
} None,
Err(_) => ( CONFIG.proxy(),
blog.or_else(|| { ) {
Blog::from_id(conn, url.as_str(), None, CONFIG.proxy()).ok() Ok(u) => {
}), authors.push(u);
authors, (blog, authors)
),
} }
} else { Err(_) => (
// logically, url possible to be an object without id proprty like {"type":"Person", "name":"Sally"} but we ignore the case blog.or_else(|| {
(blog, authors) Blog::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&url,
None,
CONFIG.proxy(),
)
.ok()
}),
authors,
),
} }
}); });
let cover = article.icon().and_then(|icon| { let cover = article
icon.iter().next().and_then(|img| { .object_props
let image = img.to_owned().extend::<Image, ImageType>().ok()??; .icon_object::<Image>()
Media::from_activity(conn, &image).ok().map(|m| m.id)
})
});
let title = article
.name()
.and_then(|name| name.to_as_string())
.ok_or(Error::MissingApProperty)?;
let id = AnyBase::from_extended(article.clone()) // FIXME: Don't clone
.ok() .ok()
.ok_or(Error::MissingApProperty)? .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
.id()
.map(|id| id.to_string()); let title = article.object_props.name_string()?;
let ap_url = article let ap_url = article
.url() .object_props
.and_then(|url| url.to_as_uri().or(id)) .url_string()
.ok_or(Error::MissingApProperty)?; .or_else(|_| article.object_props.id_string())?;
let source = article
.source()
.and_then(|s| {
serde_json::to_value(s).ok().and_then(|obj| {
if !obj.is_object() {
return None;
}
obj.get("content")
.and_then(|content| content.as_str().map(|c| c.to_string()))
})
})
.unwrap_or_default();
let post = Post::from_db(conn, &ap_url) let post = Post::from_db(conn, &ap_url)
.and_then(|mut post| { .and_then(|mut post| {
let mut updated = false; let mut updated = false;
let slug = Self::slug(&title); let slug = Self::slug(&title);
let content = SafeString::new( let content = SafeString::new(&article.object_props.content_string()?);
&article let subtitle = article.object_props.summary_string()?;
.content() let source = article.ap_object_props.source_object::<Source>()?.content;
.and_then(|content| content.to_as_string())
.ok_or(Error::MissingApProperty)?,
);
let subtitle = article
.summary()
.and_then(|summary| summary.to_as_string())
.ok_or(Error::MissingApProperty)?;
if post.slug != slug { if post.slug != slug {
post.slug = slug.to_string(); post.slug = slug.to_string();
updated = true; updated = true;
@@ -721,7 +697,7 @@ impl FromId<DbConn> for Post {
updated = true; updated = true;
} }
if post.source != source { if post.source != source {
post.source = source.clone(); post.source = source;
updated = true; updated = true;
} }
if post.cover_id != cover { if post.cover_id != cover {
@@ -739,30 +715,17 @@ impl FromId<DbConn> for Post {
Post::insert( Post::insert(
conn, conn,
NewPost { NewPost {
blog_id: blog.ok_or(Error::NotFound)?.id, blog_id: blog?.id,
slug: Self::slug(&title).to_string(), slug: Self::slug(&title).to_string(),
title, title,
content: SafeString::new( content: SafeString::new(&article.object_props.content_string()?),
&article
.content()
.and_then(|content| content.to_as_string())
.ok_or(Error::MissingApProperty)?,
),
published: true, published: true,
license, license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url, ap_url,
creation_date: article.published().map(|published| { creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
let timestamp_secs = published.unix_timestamp(); subtitle: article.object_props.summary_string()?,
let timestamp_nanos = published.unix_timestamp_nanos() source: article.ap_object_props.source_object::<Source>()?.content,
- (timestamp_secs as i128) * 1000i128 * 1000i128 * 1000i128;
NaiveDateTime::from_timestamp(timestamp_secs, timestamp_nanos as u32)
}),
subtitle: article
.summary()
.and_then(|summary| summary.to_as_string())
.ok_or(Error::MissingApProperty)?,
source,
cover_id: cover, cover_id: cover,
}, },
) )
@@ -786,22 +749,22 @@ impl FromId<DbConn> for Post {
.2 .2
.into_iter() .into_iter()
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(tags) = article.tag() { if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
for tag in tags.iter() { for tag in tags {
tag.clone() serde_json::from_value::<link::Mention>(tag.clone())
.extend::<link::Mention, MentionType>() // FIXME: Don't clone .map(|m| Mention::from_activity(conn, &m, post.id, true, true))
.map(|mention| {
mention.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
})
.ok(); .ok();
tag.clone() serde_json::from_value::<Hashtag>(tag.clone())
.extend::<Hashtag, HashtagType>() // FIXME: Don't clone .map_err(Error::from)
.map(|hashtag| { .and_then(|t| {
hashtag.and_then(|t| { let tag_name = t.name_string()?;
let tag_name = t.name.clone()?.as_str().to_string(); Ok(Tag::from_activity(
Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name)).ok() conn,
}) &t,
post.id,
hashtags.remove(&tag_name),
))
}) })
.ok(); .ok();
} }
@@ -811,17 +774,13 @@ impl FromId<DbConn> for Post {
Ok(post) Ok(post)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to get local instance user")
}
} }
impl AsObject<User, Create, &DbConn> for Post { impl AsObject<User, Create, &DbConn> for Post {
type Error = Error; type Error = Error;
type Output = Self; type Output = Post;
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self::Output> { fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Post> {
// TODO: check that _actor is actually one of the author? // TODO: check that _actor is actually one of the author?
Ok(self) Ok(self)
} }
@@ -831,7 +790,7 @@ impl AsObject<User, Delete, &DbConn> for Post {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<Self::Output> { fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let can_delete = self let can_delete = self
.get_authors(conn)? .get_authors(conn)?
.into_iter() .into_iter()
@@ -864,58 +823,27 @@ impl FromId<DbConn> for PostUpdate {
Err(Error::NotFound) Err(Error::NotFound)
} }
fn from_activity(conn: &DbConn, updated: Self::Object) -> Result<Self> { fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result<Self> {
let mut post_update = PostUpdate { Ok(PostUpdate {
ap_url: updated ap_url: updated.object.object_props.id_string()?,
.ap_object_ref() title: updated.object.object_props.name_string().ok(),
.id_unchecked() subtitle: updated.object.object_props.summary_string().ok(),
.ok_or(Error::MissingApProperty)? content: updated.object.object_props.content_string().ok(),
.to_string(), cover: updated
title: updated .object
.ap_object_ref() .object_props
.name() .icon_object::<Image>()
.and_then(|name| name.to_as_string()), .ok()
subtitle: updated .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)),
.ap_object_ref() source: updated
.summary() .object
.and_then(|summary| summary.to_as_string()), .ap_object_props
content: updated .source_object::<Source>()
.ap_object_ref() .ok()
.content() .map(|x| x.content),
.and_then(|content| content.to_as_string()), license: updated.custom_props.license_string().ok(),
cover: None, tags: updated.object.object_props.tag,
source: updated.source().and_then(|s| { })
serde_json::to_value(s).ok().and_then(|obj| {
if !obj.is_object() {
return None;
}
obj.get("content")
.and_then(|content| content.as_str().map(|c| c.to_string()))
})
}),
license: None,
tags: updated
.tag()
.and_then(|tags| serde_json::to_value(tags).ok()),
};
post_update.cover = updated.ap_object_ref().icon().and_then(|img| {
img.iter()
.next()
.and_then(|img| {
img.clone()
.extend::<Image, ImageType>()
.map(|img| img.and_then(|img| Media::from_activity(conn, &img).ok()))
.ok()
})
.and_then(|m| m.map(|m| m.id))
});
post_update.license = updated.ext_one.license;
Ok(post_update)
}
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
} }
} }
@@ -924,8 +852,14 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let mut post = let mut post = Post::from_id(
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; 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)? { if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime // TODO: maybe the author was added in the meantime
@@ -971,12 +905,8 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
serde_json::from_value::<Hashtag>(tag.clone()) serde_json::from_value::<Hashtag>(tag.clone())
.map_err(Error::from) .map_err(Error::from)
.and_then(|t| { .and_then(|t| {
let tag_name = t.name.as_ref().ok_or(Error::MissingApProperty)?; let tag_name = t.name_string()?;
let tag_name_str = tag_name if txt_hashtags.remove(&tag_name) {
.as_xsd_string()
.or_else(|| tag_name.as_rdf_lang_string().map(|rls| &*rls.value))
.ok_or(Error::MissingApProperty)?;
if txt_hashtags.remove(tag_name_str) {
hashtags.push(t); hashtags.push(t);
} else { } else {
tags.push(t); tags.push(t);
@@ -1024,28 +954,9 @@ impl From<PostEvent> for Arc<Post> {
mod tests { mod tests {
use super::*; use super::*;
use crate::inbox::{inbox, tests::fill_database, InboxResult}; use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::mentions::{Mention, NewMention};
use crate::safe_string::SafeString; use crate::safe_string::SafeString;
use crate::tests::{db, format_datetime}; use crate::tests::db;
use assert_json_diff::assert_json_eq;
use diesel::Connection; use diesel::Connection;
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Post, Mention, Vec<Post>, Vec<User>, Vec<Blog>) {
let (posts, users, blogs) = fill_database(conn);
let post = &posts[0];
let mentioned = &users[1];
let mention = Mention::insert(
&conn,
NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
comment_id: None,
},
)
.unwrap();
(post.to_owned(), mention.to_owned(), posts, users, blogs)
}
// creates a post, get it's Create activity, delete the post, // creates a post, get it's Create activity, delete the post,
// "send" the Create to the inbox, and check it works // "send" the Create to the inbox, and check it works
@@ -1098,177 +1009,45 @@ mod tests {
} }
#[test] #[test]
fn to_activity() { fn licensed_article_serde() {
let conn = db(); let mut article = Article::default();
conn.test_transaction::<_, Error, _>(|| { article.object_props.set_id_string("Yo".into()).unwrap();
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); let mut license = Licensed::default();
let act = post.to_activity(&conn)?; license.set_license_string("WTFPL".into()).unwrap();
let full_article = LicensedArticle::new(article, license);
let expected = json!({ let json = serde_json::to_value(full_article).unwrap();
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], let article_from_json: LicensedArticle = serde_json::from_value(json).unwrap();
"cc": [], assert_eq!(
"content": "Hello", "Yo",
"id": "https://plu.me/~/BlogName/testing", &article_from_json.object.object_props.id_string().unwrap()
"license": "WTFPL", );
"name": "Testing", assert_eq!(
"published": format_datetime(&post.creation_date), "WTFPL",
"source": { &article_from_json.custom_props.license_string().unwrap()
"content": "Hello", );
"mediaType": "text/markdown"
},
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
} }
#[test] #[test]
fn create_activity() { fn licensed_article_deserialization() {
let conn = db(); let json = json!({
conn.test_transaction::<_, Error, _>(|| { "type": "Article",
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); "id": "https://plu.me/~/Blog/my-article",
let act = post.create_activity(&conn)?; "attributedTo": ["https://plu.me/@/Admin", "https://plu.me/~/Blog"],
"content": "Hello.",
let expected = json!({ "name": "My Article",
"actor": "https://plu.me/@/admin/", "summary": "Bye.",
"cc": [], "source": {
"id": "https://plu.me/~/BlogName/testing/activity", "content": "Hello.",
"object": { "mediaType": "text/markdown"
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], },
"cc": [], "published": "2014-12-12T12:12:12Z",
"content": "Hello", "to": [plume_common::activity_pub::PUBLIC_VISIBILITY]
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
"content": "Hello",
"mediaType": "text/markdown"
},
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn update_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.update_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": [],
"id": "https://plu.me/~/BlogName/testing/update-",
"object": {
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
"cc": [],
"content": "Hello",
"id": "https://plu.me/~/BlogName/testing",
"license": "WTFPL",
"name": "Testing",
"published": format_datetime(&post.creation_date),
"source": {
"content": "Hello",
"mediaType": "text/markdown"
},
"summary": "Bye",
"tag": [
{
"href": "https://plu.me/@/user/",
"name": "@user",
"type": "Mention"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Article",
"url": "https://plu.me/~/BlogName/testing"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Update"
});
let actual = to_value(act)?;
let id = actual["id"].to_string();
let (id_pre, id_post) = id.rsplit_once("-").unwrap();
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
assert_eq!(
id_pre,
to_value("\"https://plu.me/~/BlogName/testing/update")
.unwrap()
.as_str()
.unwrap()
);
assert_eq!(id_post.len(), 11);
assert_eq!(
id_post.matches(char::is_numeric).collect::<String>().len(),
10
);
for (key, value) in actual.as_object().unwrap().into_iter() {
if key == "id" {
continue;
}
assert_json_eq!(value, expected.get(key).unwrap());
}
Ok(())
});
}
#[test]
fn build_delete() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn);
let act = post.build_delete(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"id": "https://plu.me/~/BlogName/testing#delete",
"object": {
"id": "https://plu.me/~/BlogName/testing",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Delete"
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
}); });
let article: LicensedArticle = serde_json::from_value(json).unwrap();
assert_eq!(
"https://plu.me/~/Blog/my-article",
&article.object.object_props.id_string().unwrap()
);
} }
} }
+14 -15
View File
@@ -1,16 +1,13 @@
use crate::{ use crate::{
db_conn::{DbConn, DbPool}, db_conn::{DbConn, DbPool},
follows, follows,
posts::Post, instance::Instance,
posts::{LicensedArticle, Post},
users::{User, UserEvent}, users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN, ACTOR_SYS, CONFIG, USER_CHAN,
}; };
use activitystreams::{ use activitypub::activity::Create;
activity::{ActorAndObjectRef, Create}, use plume_common::activity_pub::inbox::FromId;
base::AnyBase,
object::kind::ArticleType,
};
use plume_common::activity_pub::{inbox::FromId, LicensedArticle};
use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell}; use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell};
use std::sync::Arc; use std::sync::Arc;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
@@ -72,17 +69,13 @@ fn fetch_and_cache_articles(user: &Arc<User>, conn: &DbConn) {
match create_acts { match create_acts {
Ok(create_acts) => { Ok(create_acts) => {
for create_act in create_acts { for create_act in create_acts {
match create_act.object_field_ref().as_single_base().map(|base| { match create_act.create_props.object_object::<LicensedArticle>() {
let any_base = AnyBase::from_base(base.clone()); // FIXME: Don't clone() Ok(article) => {
any_base.extend::<LicensedArticle, ArticleType>()
}) {
Some(Ok(Some(article))) => {
Post::from_activity(conn, article) Post::from_activity(conn, article)
.expect("Article from remote user couldn't be saved"); .expect("Article from remote user couldn't be saved");
info!("Fetched article from remote user"); info!("Fetched article from remote user");
} }
Some(Err(e)) => warn!("Error while fetching articles in background: {:?}", e), Err(e) => warn!("Error while fetching articles in background: {:?}", e),
_ => warn!("Error while fetching articles in background"),
} }
} }
} }
@@ -97,7 +90,13 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
match follower_ids { match follower_ids {
Ok(user_ids) => { Ok(user_ids) => {
for user_id in user_ids { for user_id in user_ids {
let follower = User::from_id(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 { match follower {
Ok(follower) => { Ok(follower) => {
let inserted = follows::Follow::insert( let inserted = follows::Follow::insert(
+29 -110
View File
@@ -2,18 +2,12 @@ use crate::{
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares, db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG, timeline::*, users::User, Connection, Error, Result, CONFIG,
}; };
use activitystreams::{ use activitypub::activity::{Announce, Undo};
activity::{ActorAndObjectRef, Announce, Undo},
base::AnyBase,
iri_string::types::IriString,
prelude::*,
};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
sign::Signer, Id, IntoId, PUBLIC_VISIBILITY,
PUBLIC_VISIBILITY,
}; };
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
@@ -66,16 +60,16 @@ impl Reshare {
} }
pub fn to_activity(&self, conn: &Connection) -> Result<Announce> { pub fn to_activity(&self, conn: &Connection) -> Result<Announce> {
let mut act = Announce::new( let mut act = Announce::default();
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, act.announce_props
Post::get(conn, self.post_id)?.ap_url.parse::<IriString>()?, .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
); act.announce_props
act.set_id(self.ap_url.parse::<IriString>()?); .set_object_link(Post::get(conn, self.post_id)?.into_id())?;
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); act.object_props.set_id_string(self.ap_url.clone())?;
act.set_many_ccs(vec![self act.object_props
.get_user(conn)? .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
.followers_endpoint act.object_props
.parse::<IriString>()?]); .set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
Ok(act) Ok(act)
} }
@@ -98,16 +92,16 @@ impl Reshare {
} }
pub fn build_undo(&self, conn: &Connection) -> Result<Undo> { pub fn build_undo(&self, conn: &Connection) -> Result<Undo> {
let mut act = Undo::new( let mut act = Undo::default();
User::get(conn, self.user_id)?.ap_url.parse::<IriString>()?, act.undo_props
AnyBase::from_extended(self.to_activity(conn)?)?, .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
); act.undo_props.set_object_object(self.to_activity(conn)?)?;
act.set_id(format!("{}#delete", self.ap_url).parse::<IriString>()?); act.object_props
act.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); .set_id_string(format!("{}#delete", self.ap_url))?;
act.set_many_ccs(vec![self act.object_props
.get_user(conn)? .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY.to_string())])?;
.followers_endpoint act.object_props
.parse::<IriString>()?]); .set_cc_link_vec(vec![Id::new(self.get_user(conn)?.followers_endpoint)])?;
Ok(act) Ok(act)
} }
@@ -148,10 +142,8 @@ impl FromId<DbConn> for Reshare {
NewReshare { NewReshare {
post_id: Post::from_id( post_id: Post::from_id(
conn, conn,
act.object_field_ref() &Instance::get_local().expect("Failed to get local instance"),
.as_single_id() &act.announce_props.object_link::<Id>()?,
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@@ -159,28 +151,19 @@ impl FromId<DbConn> for Reshare {
.id, .id,
user_id: User::from_id( user_id: User::from_id(
conn, conn,
act.actor_field_ref() &Instance::get_local().expect("Failed to get local instance"),
.as_single_id() &act.announce_props.actor_link::<Id>()?,
.ok_or(Error::MissingApProperty)?
.as_str(),
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
.map_err(|(_, e)| e)? .map_err(|(_, e)| e)?
.id, .id,
ap_url: act ap_url: act.object_props.id_string()?,
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_string(),
}, },
)?; )?;
res.notify(conn)?; res.notify(conn)?;
Ok(res) Ok(res)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsObject<User, Undo, &DbConn> for Reshare { impl AsObject<User, Undo, &DbConn> for Reshare {
@@ -192,7 +175,7 @@ impl AsObject<User, Undo, &DbConn> for Reshare {
diesel::delete(&self).execute(&**conn)?; diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { if let Ok(notif) = Notification::find(&conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(&**conn)?; diesel::delete(&notif).execute(&**conn)?;
} }
@@ -205,7 +188,7 @@ impl AsObject<User, Undo, &DbConn> for Reshare {
impl NewReshare { impl NewReshare {
pub fn new(p: &Post, u: &User) -> Self { pub fn new(p: &Post, u: &User) -> Self {
let ap_url = format!("{}reshare/{}", u.ap_url, p.ap_url); let ap_url = format!("{}/reshare/{}", u.ap_url, p.ap_url);
NewReshare { NewReshare {
post_id: p.id, post_id: p.id,
user_id: u.id, user_id: u.id,
@@ -213,67 +196,3 @@ impl NewReshare {
} }
} }
} }
#[cfg(test)]
mod test {
use super::*;
use crate::diesel::Connection;
use crate::{inbox::tests::fill_database, tests::db};
use assert_json_diff::assert_json_eq;
use serde_json::{json, to_value};
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
let act = reshare.to_activity(&conn).unwrap();
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Announce",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn build_undo() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
let act = reshare.build_undo(&*conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete",
"object": {
"actor": "https://plu.me/@/admin/",
"cc": ["https://plu.me/@/admin/followers"],
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
"object": "https://plu.me/~/BlogName/testing",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Announce"
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Undo",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
}
+1 -1
View File
@@ -102,7 +102,7 @@ pub struct SafeString {
impl SafeString { impl SafeString {
pub fn new(value: &str) -> Self { pub fn new(value: &str) -> Self {
SafeString { SafeString {
value: CLEAN.clean(value).to_string(), value: CLEAN.clean(&value).to_string(),
} }
} }
+4 -14
View File
@@ -73,26 +73,16 @@ table! {
user_id -> Int4, user_id -> Int4,
} }
} }
table! { table! {
email_blocklist (id) { email_blocklist(id){
id -> Int4, id -> Int4,
email_address -> Text, email_address -> VarChar,
note -> Text, note -> Text,
notify_user -> Bool, notify_user -> Bool,
notification_text -> Text, notification_text -> Text,
} }
} }
table! {
email_signups (id) {
id -> Int4,
email -> Varchar,
token -> Varchar,
expiration_date -> Timestamp,
}
}
table! { table! {
follows (id) { follows (id) {
id -> Int4, id -> Int4,
@@ -116,6 +106,8 @@ table! {
default_license -> Text, default_license -> Text,
long_description_html -> Varchar, long_description_html -> Varchar,
short_description_html -> Varchar, short_description_html -> Varchar,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
} }
} }
@@ -316,8 +308,6 @@ allow_tables_to_appear_in_same_query!(
blogs, blogs,
comments, comments,
comment_seers, comment_seers,
email_blocklist,
email_signups,
follows, follows,
instances, instances,
likes, likes,
+2
View File
@@ -164,6 +164,8 @@ mod tests {
name: random_hex().to_string(), name: random_hex().to_string(),
open_registrations: true, open_registrations: true,
public_domain: random_hex().to_string(), public_domain: random_hex().to_string(),
private_key: None,
public_key: None,
}, },
) )
.unwrap(); .unwrap();
+2 -2
View File
@@ -148,7 +148,7 @@ impl PlumeQuery {
/// Parse a query string into this Query /// Parse a query string into this Query
pub fn parse_query(&mut self, query: &str) -> &mut Self { pub fn parse_query(&mut self, query: &str) -> &mut Self {
self.from_str_req(query.trim()) self.from_str_req(&query.trim())
} }
/// Convert this Query to a Tantivy Query /// Convert this Query to a Tantivy Query
@@ -360,7 +360,7 @@ impl std::str::FromStr for PlumeQuery {
fn from_str(query: &str) -> Result<PlumeQuery, !> { fn from_str(query: &str) -> Result<PlumeQuery, !> {
let mut res: PlumeQuery = Default::default(); let mut res: PlumeQuery = Default::default();
res.from_str_req(query.trim()); res.from_str_req(&query.trim());
Ok(res) Ok(res)
} }
} }
+1 -1
View File
@@ -57,7 +57,7 @@ impl<'a> WhitespaceTokenStream<'a> {
.filter(|&(_, ref c)| c.is_whitespace()) .filter(|&(_, ref c)| c.is_whitespace())
.map(|(offset, _)| offset) .map(|(offset, _)| offset)
.next() .next()
.unwrap_or(self.text.len()) .unwrap_or_else(|| self.text.len())
} }
} }
-72
View File
@@ -1,72 +0,0 @@
use crate::CONFIG;
use rocket::request::{FromRequest, Outcome, Request};
use std::fmt;
use std::str::FromStr;
pub enum Strategy {
Password,
Email,
}
impl Default for Strategy {
fn default() -> Self {
Self::Password
}
}
impl FromStr for Strategy {
type Err = StrategyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::Strategy::*;
match s {
"password" => Ok(Password),
"email" => Ok(Email),
s => Err(StrategyError::Unsupported(s.to_string())),
}
}
}
#[derive(Debug)]
pub enum StrategyError {
Unsupported(String),
}
impl fmt::Display for StrategyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::StrategyError::*;
match self {
// FIXME: Calc option strings from enum
Unsupported(s) => write!(f, "Unsupported strategy: {}. Choose password or email", s),
}
}
}
impl std::error::Error for StrategyError {}
pub struct Password();
pub struct Email();
impl<'a, 'r> FromRequest<'a, 'r> for Password {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Password) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}
impl<'a, 'r> FromRequest<'a, 'r> for Email {
type Error = ();
fn from_request(_request: &'a Request<'r>) -> Outcome<Self, ()> {
match matches!(CONFIG.signup, Strategy::Email) {
true => Outcome::Success(Self()),
false => Outcome::Forward(()),
}
}
}
+16 -92
View File
@@ -1,7 +1,6 @@
use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result}; use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result};
use activitystreams::iri_string::types::IriString;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_common::activity_pub::{Hashtag, HashtagExt}; use plume_common::activity_pub::Hashtag;
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Tag { pub struct Tag {
@@ -26,16 +25,13 @@ impl Tag {
list_by!(tags, for_post, post_id as i32); list_by!(tags, for_post, post_id as i32);
pub fn to_activity(&self) -> Result<Hashtag> { pub fn to_activity(&self) -> Result<Hashtag> {
let mut ht = Hashtag::new(); let mut ht = Hashtag::default();
ht.set_href( ht.set_href_string(ap_url(&format!(
ap_url(&format!( "{}/tag/{}",
"{}/tag/{}", Instance::get_local()?.public_domain,
Instance::get_local()?.public_domain, self.tag
self.tag )))?;
)) ht.set_name_string(self.tag.clone())?;
.parse::<IriString>()?,
);
ht.set_name(self.tag.clone());
Ok(ht) Ok(ht)
} }
@@ -48,7 +44,7 @@ impl Tag {
Tag::insert( Tag::insert(
conn, conn,
NewTag { NewTag {
tag: tag.name().ok_or(Error::MissingApProperty)?.as_str().into(), tag: tag.name_string()?,
is_hashtag, is_hashtag,
post_id: post, post_id: post,
}, },
@@ -56,16 +52,13 @@ impl Tag {
} }
pub fn build_activity(tag: String) -> Result<Hashtag> { pub fn build_activity(tag: String) -> Result<Hashtag> {
let mut ht = Hashtag::new(); let mut ht = Hashtag::default();
ht.set_href( ht.set_href_string(ap_url(&format!(
ap_url(&format!( "{}/tag/{}",
"{}/tag/{}", Instance::get_local()?.public_domain,
Instance::get_local()?.public_domain, tag
tag )))?;
)) ht.set_name_string(tag)?;
.parse::<IriString>()?,
);
ht.set_name(tag);
Ok(ht) Ok(ht)
} }
@@ -76,72 +69,3 @@ impl Tag {
.map_err(Error::from) .map_err(Error::from)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::db;
use crate::{diesel::Connection, inbox::tests::fill_database};
use assert_json_diff::assert_json_eq;
use serde_json::to_value;
#[test]
fn from_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (posts, _users, _blogs) = fill_database(conn);
let post_id = posts[0].id;
let mut ht = Hashtag::new();
ht.set_href(ap_url(&format!("https://plu.me/tag/a_tag")).parse::<IriString>()?);
ht.set_name("a_tag".to_string());
let tag = Tag::from_activity(conn, &ht, post_id, true)?;
assert_eq!(&tag.tag, "a_tag");
assert!(tag.is_hashtag);
Ok(())
});
}
#[test]
fn to_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
fill_database(conn);
let tag = Tag {
id: 0,
tag: "a_tag".into(),
is_hashtag: false,
post_id: 0,
};
let act = tag.to_activity()?;
let expected = json!({
"href": "https://plu.me/tag/a_tag",
"name": "a_tag",
"type": "Hashtag"
});
assert_json_eq!(to_value(&act)?, expected);
Ok(())
})
}
#[test]
fn build_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
fill_database(conn);
let act = Tag::build_activity("a_tag".into())?;
let expected = json!({
"href": "https://plu.me/tag/a_tag",
"name": "a_tag",
"type": "Hashtag"
});
assert_json_eq!(to_value(&act)?, expected);
Ok(())
});
}
}
-11
View File
@@ -6,7 +6,6 @@ use crate::{
Connection, Error, Result, Connection, Error, Result,
}; };
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::cmp::Ordering;
use std::ops::Deref; use std::ops::Deref;
pub(crate) mod query; pub(crate) mod query;
@@ -93,16 +92,6 @@ impl Timeline {
.load::<Self>(conn) .load::<Self>(conn)
.map_err(Error::from) .map_err(Error::from)
} }
.map(|mut timelines| {
timelines.sort_by(|t1, t2| {
if t1.user_id.is_some() && t2.user_id.is_none() {
Ordering::Less
} else {
t1.id.cmp(&t2.id)
}
});
timelines
})
} }
pub fn new_for_user( pub fn new_for_user(
+30 -49
View File
@@ -18,6 +18,12 @@ pub enum QueryError {
RuntimeError(String), RuntimeError(String),
} }
impl From<std::option::NoneError> for QueryError {
fn from(_: std::option::NoneError) -> Self {
QueryError::UnexpectedEndOfQuery
}
}
pub type QueryResult<T> = std::result::Result<T, QueryError>; pub type QueryResult<T> = std::result::Result<T, QueryError>;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@@ -233,7 +239,7 @@ impl WithList {
) -> Result<bool> { ) -> Result<bool> {
match list { match list {
List::List(name) => { List::List(name) => {
let list = lists::List::find_for_user_by_name(conn, timeline.user_id, name)?; let list = lists::List::find_for_user_by_name(conn, timeline.user_id, &name)?;
match (self, list.kind()) { match (self, list.kind()) {
(WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id), (WithList::Blog, ListType::Blog) => list.contains_blog(conn, post.blog_id),
(WithList::Author { boosts, likes }, ListType::User) => match kind { (WithList::Author { boosts, likes }, ListType::User) => match kind {
@@ -408,7 +414,7 @@ enum List<'a> {
fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new(); let mut res = Vec::new();
let (left, token) = parse_a(stream)?; let (left, token) = parse_a(&stream)?;
res.push(token); res.push(token);
stream = left; stream = left;
while !stream.is_empty() { while !stream.is_empty() {
@@ -430,7 +436,7 @@ fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> { fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
let mut res = Vec::new(); let mut res = Vec::new();
let (left, token) = parse_b(stream)?; let (left, token) = parse_b(&stream)?;
res.push(token); res.push(token);
stream = left; stream = left;
while !stream.is_empty() { while !stream.is_empty() {
@@ -457,7 +463,7 @@ fn parse_b<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
match left.get(0) { match left.get(0) {
Some(Token::RParent(_)) => Ok((&left[1..], token)), Some(Token::RParent(_)) => Ok((&left[1..], token)),
Some(t) => t.get_error(Token::RParent(0)), Some(t) => t.get_error(Token::RParent(0)),
None => Err(QueryError::UnexpectedEndOfQuery), None => None?,
} }
} }
_ => parse_c(stream), _ => parse_c(stream),
@@ -478,13 +484,9 @@ fn parse_c<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<
} }
fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> { fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> {
match stream match stream.get(0).map(Token::get_text)? {
.get(0)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?
{
s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => { s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => {
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? { match stream.get(1)? {
Token::Word(_, _, r#in) if r#in == &"in" => { Token::Word(_, _, r#in) if r#in == &"in" => {
let (mut left, list) = parse_l(&stream[2..])?; let (mut left, list) = parse_l(&stream[2..])?;
let kind = match s { let kind = match s {
@@ -496,12 +498,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" { if *clude != "include" && *clude != "exclude" {
break; break;
} }
match ( match (*clude, left.get(1).map(Token::get_text)?) {
*clude,
left.get(1)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => { ("include", "reshares") | ("include", "reshare") => {
boosts = true boosts = true
} }
@@ -532,10 +529,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
t => t.get_error(Token::Word(0, 0, "'in'")), t => t.get_error(Token::Word(0, 0, "'in'")),
} }
} }
s @ "title" | s @ "subtitle" | s @ "content" => match ( s @ "title" | s @ "subtitle" | s @ "content" => match (stream.get(1)?, stream.get(2)?) {
stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)?,
stream.get(2).ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
(Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok(( (Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok((
&stream[3..], &stream[3..],
Arg::Contains( Arg::Contains(
@@ -561,13 +555,7 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
if *clude != "include" && *clude != "exclude" { if *clude != "include" && *clude != "exclude" {
break; break;
} }
match ( match (*clude, stream.get(2).map(Token::get_text)?) {
*clude,
stream
.get(2)
.map(Token::get_text)
.ok_or(QueryError::UnexpectedEndOfQuery)?,
) {
("include", "reshares") | ("include", "reshare") => boosts = true, ("include", "reshares") | ("include", "reshare") => boosts = true,
("exclude", "reshares") | ("exclude", "reshare") => boosts = false, ("exclude", "reshares") | ("exclude", "reshare") => boosts = false,
("include", "likes") | ("include", "like") => likes = true, ("include", "likes") | ("include", "like") => likes = true,
@@ -589,23 +577,20 @@ fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>],
"all" => Ok((&stream[1..], Arg::Boolean(Bool::All))), "all" => Ok((&stream[1..], Arg::Boolean(Bool::All))),
_ => unreachable!(), _ => unreachable!(),
}, },
_ => stream _ => stream.get(0)?.get_error(Token::Word(
.get(0) 0,
.ok_or(QueryError::UnexpectedEndOfQuery)? 0,
.get_error(Token::Word( "one of 'blog', 'author', 'license', 'tags', 'lang', \
0,
0,
"one of 'blog', 'author', 'license', 'tags', 'lang', \
'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'", 'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'",
)), )),
} }
} }
fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> { fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> {
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { match stream.get(0)? {
Token::LBracket(_) => { Token::LBracket(_) => {
let (left, list) = parse_m(&stream[1..])?; let (left, list) = parse_m(&stream[1..])?;
match left.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { match left.get(0)? {
Token::RBracket(_) => Ok((&left[1..], List::Array(list))), Token::RBracket(_) => Ok((&left[1..], List::Array(list))),
t => t.get_error(Token::Word(0, 0, "one of ']' or ','")), t => t.get_error(Token::Word(0, 0, "one of ']' or ','")),
} }
@@ -616,20 +601,16 @@ fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Lis
} }
fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> { fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> {
let mut res: Vec<&str> = vec![ let mut res: Vec<&str> = vec![match stream.get(0)? {
match stream.get(0).ok_or(QueryError::UnexpectedEndOfQuery)? { Token::Word(_, _, w) => w,
Token::Word(_, _, w) => w, t => return t.get_error(Token::Word(0, 0, "any word")),
t => return t.get_error(Token::Word(0, 0, "any word")), }];
},
];
stream = &stream[1..]; stream = &stream[1..];
while let Token::Comma(_) = stream[0] { while let Token::Comma(_) = stream[0] {
res.push( res.push(match stream.get(1)? {
match stream.get(1).ok_or(QueryError::UnexpectedEndOfQuery)? { Token::Word(_, _, w) => w,
Token::Word(_, _, w) => w, t => return t.get_error(Token::Word(0, 0, "any word")),
t => return t.get_error(Token::Word(0, 0, "any word")), });
},
);
stream = &stream[2..]; stream = &stream[2..];
} }
+244 -448
View File
@@ -4,15 +4,12 @@ use crate::{
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result, safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN, UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
}; };
use activitystreams::{ use activitypub::{
activity::Delete, activity::Delete,
actor::{ApActor, AsApActor, Endpoints, Person}, actor::Person,
base::{AnyBase, Base},
collection::{OrderedCollection, OrderedCollectionPage}, collection::{OrderedCollection, OrderedCollectionPage},
iri_string::types::IriString, object::{Image, Tombstone},
markers::Activity, Activity, CustomObject, Endpoint,
object::{kind::ImageType, AsObject as _, Image, Tombstone},
prelude::*,
}; };
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
@@ -25,14 +22,17 @@ use openssl::{
}; };
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
ap_accept_header,
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
request::get, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}, ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
ActivityStream, ApSignature, CustomPerson, Id, IntoId, PublicKey, ToAsString, ToAsUri,
PUBLIC_VISIBILITY,
}, },
utils, utils,
}; };
use reqwest::{
header::{HeaderValue, ACCEPT},
ClientBuilder,
};
use riker::actors::{Publish, Tell}; use riker::actors::{Publish, Tell};
use rocket::{ use rocket::{
outcome::IntoOutcome, outcome::IntoOutcome,
@@ -43,13 +43,15 @@ use std::{
hash::{Hash, Hasher}, hash::{Hash, Hasher},
sync::Arc, sync::Arc,
}; };
use url::Url;
use webfinger::*; use webfinger::*;
pub type CustomPerson = CustomObject<ApSignature, Person>;
pub enum Role { pub enum Role {
Admin = 0, Admin = 0,
Moderator = 1, Moderator = 1,
Normal = 2, Normal = 2,
Instance = 3,
} }
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)] #[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
@@ -76,7 +78,6 @@ pub struct User {
pub summary_html: SafeString, pub summary_html: SafeString,
/// 0 = admin /// 0 = admin
/// 1 = moderator /// 1 = moderator
/// 3 = local instance
/// anything else = normal user /// anything else = normal user
pub role: i32, pub role: i32,
pub preferred_theme: Option<String>, pub preferred_theme: Option<String>,
@@ -203,22 +204,6 @@ impl User {
} }
} }
/**
* TODO: Should create user record with normalized(lowercased) email
*/
pub fn email_used(conn: &DbConn, email: &str) -> Result<bool> {
use diesel::dsl::{exists, select};
select(exists(
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::email.eq(email))
.or_filter(users::email.eq(email.to_ascii_lowercase())),
))
.get_result(&**conn)
.map_err(Error::from)
}
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> { fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)? let link = resolve(acct.to_owned(), true)?
.links .links
@@ -227,7 +212,8 @@ impl User {
.ok_or(Error::Webfinger)?; .ok_or(Error::Webfinger)?;
User::from_id( User::from_id(
conn, conn,
link.href.as_ref().ok_or(Error::Webfinger)?, &Instance::get_local().expect("Failed to get local instance"),
link.href.as_ref()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
) )
@@ -244,22 +230,25 @@ impl User {
} }
fn fetch(url: &str) -> Result<CustomPerson> { fn fetch(url: &str) -> Result<CustomPerson> {
let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; let mut res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()?
.get(url)
.header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
// without this workaround, publicKey is not correctly deserialized // without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?; let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let person = serde_json::from_str::<Person>(text)?; let mut json = serde_json::from_str::<CustomPerson>(text)?;
let json = CustomPerson::new( json.custom_props = ap_sign;
ApActor::new(
person
.clone()
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_owned(),
person,
),
ap_sign,
); // FIXME: Don't clone()
Ok(json) Ok(json)
} }
@@ -271,56 +260,35 @@ impl User {
User::fetch(&self.ap_url.clone()).and_then(|json| { User::fetch(&self.ap_url.clone()).and_then(|json| {
let avatar = Media::save_remote( let avatar = Media::save_remote(
conn, conn,
json.ap_actor_ref() json.object
.icon() .object_props
.ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set .icon_image()?
.iter() .object_props
.next() .url_string()?,
.and_then(|i| { &self,
i.clone()
.extend::<Image, ImageType>() // FIXME: Don't clone()
.ok()?
.and_then(|url| Some(url.id_unchecked()?.to_string()))
})
.ok_or(Error::MissingApProperty)?,
self,
) )
.ok(); .ok();
let pub_key = &json.ext_one.public_key.public_key_pem;
diesel::update(self) diesel::update(self)
.set(( .set((
users::username.eq(json users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
.ap_actor_ref() users::display_name.eq(json.object.object_props.name_string()?),
.preferred_username() users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
.ok_or(Error::MissingApProperty)?), users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::display_name.eq(json
.ap_actor_ref()
.name()
.ok_or(Error::MissingApProperty)?
.to_as_string()
.ok_or(Error::MissingApProperty)?),
users::outbox_url.eq(json
.ap_actor_ref()
.outbox()?
.ok_or(Error::MissingApProperty)?
.as_str()),
users::inbox_url.eq(json.ap_actor_ref().inbox()?.as_str()),
users::summary.eq(SafeString::new( users::summary.eq(SafeString::new(
&json &json
.ap_actor_ref() .object
.summary() .object_props
.and_then(|summary| summary.to_as_string()) .summary_string()
.unwrap_or_default(), .unwrap_or_default(),
)), )),
users::followers_endpoint.eq(json users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
.ap_actor_ref()
.followers()?
.ok_or(Error::MissingApProperty)?
.as_str()),
users::avatar_id.eq(avatar.map(|a| a.id)), users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()), users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(pub_key), users::public_key.eq(json
.custom_props
.public_key_publickey()?
.public_key_pem_string()?),
)) ))
.execute(conn) .execute(conn)
.map(|_| ()) .map(|_| ())
@@ -461,63 +429,61 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> { pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
Ok(ActivityStream::new(self.outbox_collection(conn)?)) let mut coll = OrderedCollection::default();
}
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> {
let mut coll = OrderedCollection::new();
let first = &format!("{}?page=1", &self.outbox_url); let first = &format!("{}?page=1", &self.outbox_url);
let last = &format!( let last = &format!(
"{}?page={}", "{}?page={}",
&self.outbox_url, &self.outbox_url,
self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1 self.get_activities_count(&conn) / i64::from(ITEMS_PER_PAGE) + 1
); );
coll.set_first(first.parse::<IriString>()?); coll.collection_props.set_first_link(Id::new(first))?;
coll.set_last(last.parse::<IriString>()?); coll.collection_props.set_last_link(Id::new(last))?;
coll.set_total_items(self.get_activities_count(conn) as u64); coll.collection_props
Ok(coll) .set_total_items_u64(self.get_activities_count(&conn) as u64)?;
Ok(ActivityStream::new(coll))
} }
pub fn outbox_page( pub fn outbox_page(
&self, &self,
conn: &Connection, conn: &Connection,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Result<ActivityStream<OrderedCollectionPage>> { ) -> Result<ActivityStream<OrderedCollectionPage>> {
Ok(ActivityStream::new(
self.outbox_collection_page(conn, (min, max))?,
))
}
pub fn outbox_collection_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<OrderedCollectionPage> {
let acts = self.get_activities_page(conn, (min, max))?; let acts = self.get_activities_page(conn, (min, max))?;
let n_acts = self.get_activities_count(conn); let n_acts = self.get_activities_count(&conn);
let mut coll = OrderedCollectionPage::new(); let mut coll = OrderedCollectionPage::default();
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) { if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
coll.set_next( coll.collection_page_props.set_next_link(Id::new(&format!(
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE + 2) "{}?page={}",
.parse::<IriString>()?, &self.outbox_url,
); min / ITEMS_PER_PAGE + 2
)))?;
} }
if min > 0 { if min > 0 {
coll.set_prev( coll.collection_page_props.set_prev_link(Id::new(&format!(
format!("{}?page={}", &self.outbox_url, min / ITEMS_PER_PAGE) "{}?page={}",
.parse::<IriString>()?, &self.outbox_url,
); min / ITEMS_PER_PAGE
)))?;
} }
coll.set_many_items( coll.collection_props.items = serde_json::to_value(acts)?;
acts.iter() coll.collection_page_props
.filter_map(|value| AnyBase::from_arbitrary_json(value).ok()), .set_part_of_link(Id::new(&self.outbox_url))?;
); Ok(ActivityStream::new(coll))
coll.set_part_of(self.outbox_url.parse::<IriString>()?);
Ok(coll)
} }
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
pub fn fetch_outbox_page<T: Activity + serde::de::DeserializeOwned>( let mut res = ClientBuilder::new()
&self, .connect_timeout(Some(std::time::Duration::from_secs(5)))
url: &str, .build()?
) -> Result<(Vec<T>, Option<String>)> { .get(url)
let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; .header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"] let items = json["items"]
@@ -530,13 +496,21 @@ impl User {
let next = json.get("next").map(|x| x.as_str().unwrap().to_owned()); let next = json.get("next").map(|x| x.as_str().unwrap().to_owned());
Ok((items, next)) Ok((items, next))
} }
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
pub fn fetch_outbox<T: Activity + serde::de::DeserializeOwned>(&self) -> Result<Vec<T>> { let mut res = ClientBuilder::new()
let res = get( .connect_timeout(Some(std::time::Duration::from_secs(5)))
&self.outbox_url[..], .build()?
Self::get_sender(), .get(&self.outbox_url[..])
CONFIG.proxy().cloned(), .header(
)?; ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") { if let Some(first) = json.get("first") {
@@ -546,7 +520,7 @@ impl User {
if page.is_empty() { if page.is_empty() {
break; break;
} }
items.append(&mut page); items.extend(page.drain(..));
if let Some(n) = nxt { if let Some(n) = nxt {
if n == next { if n == next {
break; break;
@@ -568,11 +542,20 @@ impl User {
} }
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> { pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let res = get( let mut res = ClientBuilder::new()
&self.followers_endpoint[..], .connect_timeout(Some(std::time::Duration::from_secs(5)))
Self::get_sender(), .build()?
CONFIG.proxy().cloned(), .get(&self.followers_endpoint[..])
)?; .header(
ACCEPT,
HeaderValue::from_str(
&ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)?,
)
.send()?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"] Ok(json["items"]
@@ -744,7 +727,7 @@ impl User {
pub fn get_keypair(&self) -> Result<PKey<Private>> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa(Rsa::private_key_from_pem( PKey::from_rsa(Rsa::private_key_from_pem(
self.private_key.clone().ok_or(Error::Signature)?.as_ref(), self.private_key.clone()?.as_ref(),
)?) )?)
.map_err(Error::from) .map_err(Error::from)
} }
@@ -776,58 +759,71 @@ impl User {
} }
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
let mut actor = ApActor::new(self.inbox_url.parse()?, Person::new()); let mut actor = Person::default();
let ap_url = self.ap_url.parse::<IriString>()?; actor.object_props.set_id_string(self.ap_url.clone())?;
actor.set_id(ap_url.clone()); actor
actor.set_name(self.display_name.clone()); .object_props
actor.set_summary(self.summary_html.get().clone()); .set_name_string(self.display_name.clone())?;
actor.set_url(ap_url.clone()); actor
actor.set_inbox(self.inbox_url.parse()?); .object_props
actor.set_outbox(self.outbox_url.parse()?); .set_summary_string(self.summary_html.get().clone())?;
actor.set_preferred_username(self.username.clone()); actor.object_props.set_url_string(self.ap_url.clone())?;
actor.set_followers(self.followers_endpoint.parse()?); actor
.ap_actor_props
.set_inbox_string(self.inbox_url.clone())?;
actor
.ap_actor_props
.set_outbox_string(self.outbox_url.clone())?;
actor
.ap_actor_props
.set_preferred_username_string(self.username.clone())?;
actor
.ap_actor_props
.set_followers_string(self.followers_endpoint.clone())?;
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() { if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
let endpoints = Endpoints { let mut endpoints = Endpoint::default();
shared_inbox: Some(shared_inbox_url.parse::<IriString>()?), endpoints.set_shared_inbox_string(shared_inbox_url)?;
..Endpoints::default() actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
};
actor.set_endpoints(endpoints);
} }
let pub_key = PublicKey { let mut public_key = PublicKey::default();
id: format!("{}#main-key", self.ap_url).parse()?, public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
owner: ap_url, public_key.set_owner_string(self.ap_url.clone())?;
public_key_pem: self.public_key.clone(), public_key.set_public_key_pem_string(self.public_key.clone())?;
}; let mut ap_signature = ApSignature::default();
let ap_signature = ApSignature { ap_signature.set_public_key_publickey(public_key)?;
public_key: pub_key,
};
if let Some(avatar_id) = self.avatar_id { let mut avatar = Image::default();
let mut avatar = Image::new(); avatar.object_props.set_url_string(
avatar.set_url(Media::get(conn, avatar_id)?.url()?.parse::<IriString>()?); self.avatar_id
actor.set_icon(avatar.into_any_base()?); .and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
} .unwrap_or_default(),
)?;
actor.object_props.set_icon_object(avatar)?;
Ok(CustomPerson::new(actor, ap_signature)) Ok(CustomPerson::new(actor, ap_signature))
} }
pub fn delete_activity(&self, conn: &Connection) -> Result<Delete> { pub fn delete_activity(&self, conn: &Connection) -> Result<Delete> {
let mut tombstone = Tombstone::new(); let mut del = Delete::default();
tombstone.set_id(self.ap_url.parse()?);
let mut del = Delete::new( let mut tombstone = Tombstone::default();
self.ap_url.parse::<IriString>()?, tombstone.object_props.set_id_string(self.ap_url.clone())?;
Base::retract(tombstone)?.into_generic()?,
); del.delete_props
del.set_id(format!("{}#delete", self.ap_url).parse()?); .set_actor_link(Id::new(self.ap_url.clone()))?;
del.set_many_tos(vec![PUBLIC_VISIBILITY.parse::<IriString>()?]); del.delete_props.set_object_object(tombstone)?;
del.set_many_ccs( del.object_props
.set_id_string(format!("{}#delete", self.ap_url))?;
del.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
del.object_props.set_cc_link_vec(
self.get_followers(conn)? self.get_followers(conn)?
.into_iter() .into_iter()
.filter_map(|f| f.ap_url.parse::<IriString>().ok()), .map(|f| Id::new(f.ap_url))
); .collect(),
)?;
Ok(del) Ok(del)
} }
@@ -953,60 +949,9 @@ impl FromId<DbConn> for User {
} }
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> { fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
let actor = acct.ap_actor_ref(); let url = Url::parse(&acct.object.object_props.id_string()?)?;
let username = actor let inst = url.host_str()?;
.preferred_username() let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
.ok_or(Error::MissingApProperty)?
.to_string();
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
let summary = acct
.object_ref()
.summary()
.and_then(|prop| prop.to_as_string())
.unwrap_or_default();
let mut new_user = NewUser {
display_name: acct
.object_ref()
.name()
.and_then(|prop| prop.to_as_string())
.unwrap_or_else(|| username.clone()),
username: username.clone(),
outbox_url: actor.outbox()?.ok_or(Error::MissingApProperty)?.to_string(),
inbox_url: actor.inbox()?.to_string(),
role: 2,
summary_html: SafeString::new(&summary),
summary,
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
shared_inbox_url: actor
.endpoints()?
.and_then(|e| e.shared_inbox.map(|inbox| inbox.to_string())),
followers_endpoint: actor
.followers()?
.ok_or(Error::MissingApProperty)?
.to_string(),
..NewUser::default()
};
let avatar_id = acct.object_ref().icon().and_then(|icon| icon.to_as_uri());
let (ap_url, inst) = {
let any_base = acct.into_any_base()?;
let id = any_base.id().ok_or(Error::MissingApProperty)?;
(
id.to_string(),
id.authority_components()
.ok_or(Error::Url)?
.host()
.to_string(),
)
};
new_user.ap_url = ap_url;
let instance = Instance::find_by_domain(conn, &inst).or_else(|_| {
Instance::insert( Instance::insert(
conn, conn,
NewInstance { NewInstance {
@@ -1020,33 +965,81 @@ impl FromId<DbConn> for User {
open_registrations: true, open_registrations: true,
short_description_html: String::new(), short_description_html: String::new(),
long_description_html: String::new(), long_description_html: String::new(),
private_key: None,
public_key: None,
}, },
) )
})?; })?;
new_user.instance_id = instance.id;
new_user.fqn = if instance.local { let username = acct.object.ap_actor_props.preferred_username_string()?;
username
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
return Err(Error::InvalidValue);
}
let fqn = if instance.local {
username.clone()
} else { } else {
format!("{}@{}", username, instance.public_domain) format!("{}@{}", username, instance.public_domain)
}; };
let user = User::insert(conn, new_user)?; let user = User::insert(
if let Some(avatar_id) = avatar_id { conn,
let avatar = Media::save_remote(conn, avatar_id, &user); NewUser {
display_name: acct
.object
.object_props
.name_string()
.unwrap_or_else(|_| username.clone()),
username,
outbox_url: acct.object.ap_actor_props.outbox_string()?,
inbox_url: acct.object.ap_actor_props.inbox_string()?,
role: 2,
summary: acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
summary_html: SafeString::new(
&acct
.object
.object_props
.summary_string()
.unwrap_or_default(),
),
email: None,
hashed_password: None,
instance_id: instance.id,
ap_url: acct.object.object_props.id_string()?,
public_key: acct
.custom_props
.public_key_publickey()?
.public_key_pem_string()?,
private_key: None,
shared_inbox_url: acct
.object
.ap_actor_props
.endpoints_endpoint()
.and_then(|e| e.shared_inbox_string())
.ok(),
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
fqn,
avatar_id: None,
},
)?;
if let Ok(avatar) = avatar { if let Ok(icon) = acct.object.object_props.icon_image() {
if let Err(e) = user.set_avatar(conn, avatar.id) { if let Ok(url) = icon.object_props.url_string() {
tracing::error!("{:?}", e); let avatar = Media::save_remote(conn, url, &user);
if let Ok(avatar) = avatar {
user.set_avatar(conn, avatar.id)?;
} }
} }
} }
Ok(user) Ok(user)
} }
fn get_sender() -> &'static dyn Signer {
Instance::get_local_instance_user().expect("Failed to local instance user")
}
} }
impl AsActor<&DbConn> for User { impl AsActor<&DbConn> for User {
@@ -1083,18 +1076,18 @@ impl Signer for User {
format!("{}#main-key", self.ap_url) format!("{}#main-key", self.ap_url)
} }
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> { fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
let key = self.get_keypair().map_err(|_| SignError())?; let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?; let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?; signer.update(to_sign.as_bytes())?;
signer.sign_to_vec().map_err(SignError::from) signer.sign_to_vec().map_err(SignatureError::from)
} }
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<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 key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?; verifier.update(data.as_bytes())?;
verifier.verify(signature).map_err(SignError::from) verifier.verify(&signature).map_err(SignatureError::from)
} }
} }
@@ -1135,7 +1128,7 @@ impl NewUser {
display_name, display_name,
role: role as i32, role: role as i32,
summary: summary.to_owned(), summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(summary, None, false, None).0), summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
email: Some(email), email: Some(email),
hashed_password: password, hashed_password: password,
instance_id: instance.id, instance_id: instance.id,
@@ -1178,13 +1171,10 @@ pub(crate) mod tests {
use super::*; use super::*;
use crate::{ use crate::{
instance::{tests as instance_tests, Instance}, instance::{tests as instance_tests, Instance},
medias::{Media, NewMedia},
tests::db, tests::db,
Connection as Conn, ITEMS_PER_PAGE, Connection as Conn,
}; };
use assert_json_diff::assert_json_eq; use diesel::Connection;
use diesel::{Connection, SaveChangesDsl};
use serde_json::to_value;
pub(crate) fn fill_database(conn: &Conn) -> Vec<User> { pub(crate) fn fill_database(conn: &Conn) -> Vec<User> {
instance_tests::fill_database(conn); instance_tests::fill_database(conn);
@@ -1208,7 +1198,7 @@ pub(crate) mod tests {
Some("invalid_user_password".to_owned()), Some("invalid_user_password".to_owned()),
) )
.unwrap(); .unwrap();
let mut other = NewUser::new_local( let other = NewUser::new_local(
conn, conn,
"other".to_owned(), "other".to_owned(),
"Another user".to_owned(), "Another user".to_owned(),
@@ -1218,73 +1208,9 @@ pub(crate) mod tests {
Some("invalid_other_password".to_owned()), Some("invalid_other_password".to_owned()),
) )
.unwrap(); .unwrap();
let avatar = Media::insert(
conn,
NewMedia {
file_path: "static/media/example.png".into(),
alt_text: "Another user".into(),
is_remote: false,
remote_url: None,
sensitive: false,
content_warning: None,
owner_id: other.id,
},
)
.unwrap();
other.avatar_id = Some(avatar.id);
let other = other.save_changes::<User>(&*conn).unwrap();
vec![admin, user, other] vec![admin, user, other]
} }
fn fill_pages(
conn: &DbConn,
) -> (
Vec<crate::posts::Post>,
Vec<crate::users::User>,
Vec<crate::blogs::Blog>,
) {
use crate::post_authors::NewPostAuthor;
use crate::posts::NewPost;
let (mut posts, users, blogs) = crate::inbox::tests::fill_database(conn);
let user = &users[0];
let blog = &blogs[0];
for i in 1..(ITEMS_PER_PAGE * 4 + 3) {
let title = format!("Post {}", i);
let content = format!("Content for post {}.", i);
let post = Post::insert(
conn,
NewPost {
blog_id: blog.id,
slug: title.clone(),
title: title.clone(),
content: SafeString::new(&content),
published: true,
license: "CC-0".into(),
creation_date: None,
ap_url: format!("{}/{}", blog.ap_url, title),
subtitle: "".into(),
source: content,
cover_id: None,
},
)
.unwrap();
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: user.id,
},
)
.unwrap();
posts.push(post);
}
(posts, users, blogs)
}
#[test] #[test]
fn find_by() { fn find_by() {
let conn = db(); let conn = db();
@@ -1445,134 +1371,4 @@ pub(crate) mod tests {
Ok(()) Ok(())
}); });
} }
#[test]
fn to_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let users = fill_database(&conn);
let user = &users[0];
let act = user.to_activity(&conn)?;
let expected = json!({
"endpoints": {
"sharedInbox": "https://plu.me/inbox"
},
"followers": "https://plu.me/@/admin/followers",
"id": "https://plu.me/@/admin/",
"inbox": "https://plu.me/@/admin/inbox",
"name": "The admin",
"outbox": "https://plu.me/@/admin/outbox",
"preferredUsername": "admin",
"publicKey": {
"id": "https://plu.me/@/admin/#main-key",
"owner": "https://plu.me/@/admin/",
"publicKeyPem": user.public_key,
},
"summary": "<p dir=\"auto\">Hello there, Im the admin</p>\n",
"type": "Person",
"url": "https://plu.me/@/admin/"
});
assert_json_eq!(to_value(act)?, expected);
let other = &users[2];
let other_act = other.to_activity(&conn)?;
let expected_other = json!({
"endpoints": {
"sharedInbox": "https://plu.me/inbox"
},
"followers": "https://plu.me/@/other/followers",
"icon": {
"url": "https://plu.me/static/media/example.png",
"type": "Image",
},
"id": "https://plu.me/@/other/",
"inbox": "https://plu.me/@/other/inbox",
"name": "Another user",
"outbox": "https://plu.me/@/other/outbox",
"preferredUsername": "other",
"publicKey": {
"id": "https://plu.me/@/other/#main-key",
"owner": "https://plu.me/@/other/",
"publicKeyPem": other.public_key,
},
"summary": "<p dir=\"auto\">Hello there, Im someone else</p>\n",
"type": "Person",
"url": "https://plu.me/@/other/"
});
assert_json_eq!(to_value(other_act)?, expected_other);
Ok(())
});
}
#[test]
fn delete_activity() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let users = fill_database(&conn);
let user = &users[1];
let act = user.delete_activity(&conn)?;
let expected = json!({
"actor": "https://plu.me/@/user/",
"cc": [],
"id": "https://plu.me/@/user/#delete",
"object": {
"id": "https://plu.me/@/user/",
"type": "Tombstone",
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Delete",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let (_pages, users, _blogs) = fill_pages(&conn);
let user = &users[0];
let act = user.outbox_collection(&conn)?;
let expected = json!({
"first": "https://plu.me/@/admin/outbox?page=1",
"last": "https://plu.me/@/admin/outbox?page=5",
"totalItems": 51,
"type": "OrderedCollection",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
#[test]
fn outbox_collection_page() {
let conn = db();
conn.test_transaction::<_, Error, _>(|| {
let users = fill_database(&conn);
let user = &users[0];
let act = user.outbox_collection_page(&conn, (33, 36))?;
let expected = json!({
"items": [],
"partOf": "https://plu.me/@/admin/outbox",
"prev": "https://plu.me/@/admin/outbox?page=2",
"type": "OrderedCollectionPage",
});
assert_json_eq!(to_value(act)?, expected);
Ok(())
});
}
} }
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Afrikaans\n" "Language-Team: Afrikaans\n"
"Language: af_ZA\n" "Language: af_ZA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Language: ar_SA\n" "Language: ar_SA\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "فتح محرر النصوص الغني" msgstr "فتح محرر النصوص الغني"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "العنوان" msgstr "العنوان"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "العنوان الثانوي أو الملخص" msgstr "العنوان الثانوي أو الملخص"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم." msgstr "اكتب مقالك هنا. ماركداون مُدَعَّم."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "يتبقا {} حرفا تقريبا" msgstr "يتبقا {} حرفا تقريبا"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "الوسوم" msgstr "الوسوم"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "الرخصة" msgstr "الرخصة"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "الغلاف" msgstr "الغلاف"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "هذه مسودة" msgstr "هذه مسودة"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "نشر كتابا" msgstr "نشر كتابا"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Language: bg_BG\n" "Language: bg_BG\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Искате ли да активирате локално автоматично запаметяване, последно редактирано в {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Отворете редактора с богат текст" msgstr "Отворете редактора с богат текст"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Заглавие" msgstr "Заглавие"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Подзаглавие или резюме" msgstr "Подзаглавие или резюме"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Напишете статията си тук. Поддържа се Markdown." msgstr "Напишете статията си тук. Поддържа се Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Остават {} знака вляво" msgstr "Остават {} знака вляво"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Етикети" msgstr "Етикети"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Лиценз" msgstr "Лиценз"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Основно изображение" msgstr "Основно изображение"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Това е проект" msgstr "Това е проект"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Публикувай" msgstr "Публикувай"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Obre leditor de text enriquit" msgstr "Obre leditor de text enriquit"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Títol" msgstr "Títol"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítol o resum" msgstr "Subtítol o resum"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown." msgstr "Escriviu el vostre article ací. Podeu fer servir el Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Queden uns {} caràcters" msgstr "Queden uns {} caràcters"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Llicència" msgstr "Llicència"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Coberta" msgstr "Coberta"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Açò és un esborrany" msgstr "Açò és un esborrany"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publica" msgstr "Publica"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-05-09 09:58\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Language: cs_CZ\n" "Language: cs_CZ\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otevřít editor formátovaného textu" msgstr "Otevřít editor formátovaného textu"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Nadpis" msgstr "Nadpis"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Podnadpis, nebo shrnutí" msgstr "Podnadpis, nebo shrnutí"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Sem napište svůj článek. Markdown je podporován." msgstr "Sem napište svůj článek. Markdown je podporován."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Zbývá kolem {} znaků" msgstr "Zbývá kolem {} znaků"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagy" msgstr "Tagy"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licence" msgstr "Licence"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Titulka" msgstr "Titulka"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Tohle je koncept" msgstr "Tohle je koncept"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Zveřejnit" msgstr "Zveřejnit"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Language: da_DK\n" "Language: da_DK\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-26 13:16\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Möchten Sie die lokale automatische Speicherung laden, die zuletzt um {} bearbeitet wurde?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr " Rich Text Editor (RTE) öffnen" msgstr " Rich Text Editor (RTE) öffnen"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Untertitel oder Zusammenfassung" msgstr "Untertitel oder Zusammenfassung"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt." msgstr "Schreiben deinen Artikel hier. Markdown wird unterstützt."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Ungefähr {} Zeichen übrig" msgstr "Ungefähr {} Zeichen übrig"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Schlagwörter" msgstr "Schlagwörter"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Lizenz" msgstr "Lizenz"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Einband" msgstr "Einband"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Dies ist ein Entwurf" msgstr "Dies ist ein Entwurf"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Veröffentlichen" msgstr "Veröffentlichen"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Greek\n" "Language-Team: Greek\n"
"Language: el_GR\n" "Language: el_GR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
"Language: en_US\n" "Language: en_US\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Esperanto\n" "Language-Team: Esperanto\n"
"Language: eo_UY\n" "Language: eo_UY\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Malfermi la riĉan redaktilon" msgstr "Malfermi la riĉan redaktilon"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titolo" msgstr "Titolo"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita." msgstr "Verku vian artikolon ĉi tie. Markdown estas subtenita."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Proksimume {} signoj restantaj" msgstr "Proksimume {} signoj restantaj"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etikedoj" msgstr "Etikedoj"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Permesilo" msgstr "Permesilo"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Kovro" msgstr "Kovro"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Malfinias" msgstr "Malfinias"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Eldoni" msgstr "Eldoni"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-26 13:16\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Language: es_ES\n" "Language: es_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "¿Quieres cargar el guardado automático local editado por última vez en {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abrir el editor de texto enriquecido" msgstr "Abrir el editor de texto enriquecido"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo, o resumen" msgstr "Subtítulo, o resumen"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escriba su artículo aquí. Puede utilizar Markdown." msgstr "Escriba su artículo aquí. Puede utilizar Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Quedan unos {} caracteres" msgstr "Quedan unos {} caracteres"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licencia" msgstr "Licencia"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Cubierta" msgstr "Cubierta"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Esto es un borrador" msgstr "Esto es un borrador"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
-63
View File
@@ -1,63 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-05-09 09:58\n"
"Last-Translator: \n"
"Language-Team: Basque\n"
"Language: eu_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: plume\n"
"X-Crowdin-Project-ID: 352097\n"
"X-Crowdin-Language: eu\n"
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172
msgid "Do you want to load the local autosave last edited at {}?"
msgstr "{}(t)an automatikoki gordetako azken kopia lokala kargatu nahi al duzu?"
# plume-front/src/editor.rs:326
msgid "Open the rich text editor"
msgstr "Ireki testu-formatutzaile aberatsa"
# plume-front/src/editor.rs:385
msgid "Title"
msgstr "Izenburua"
# plume-front/src/editor.rs:389
msgid "Subtitle, or summary"
msgstr "Azpititulua edo laburpena"
# plume-front/src/editor.rs:396
msgid "Write your article here. Markdown is supported."
msgstr "Idatzi hemen testua. Markdown erabil dezakezu."
# plume-front/src/editor.rs:407
msgid "Around {} characters left"
msgstr "%{count} karaktere geratzen dira"
# plume-front/src/editor.rs:517
msgid "Tags"
msgstr "Etiketak"
# plume-front/src/editor.rs:518
msgid "License"
msgstr "Lizentzia"
# plume-front/src/editor.rs:524
msgid "Cover"
msgstr "Azala"
# plume-front/src/editor.rs:564
msgid "This is a draft"
msgstr "Zirriborro bat da"
# plume-front/src/editor.rs:575
msgid "Publish"
msgstr "Argitaratu"
+16 -16
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-05-10 17:54\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"Language: fa_IR\n" "Language: fa_IR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "آیا می‌خواهید نسخهٔ ذخیره شدهٔ خودکار محلّی از آخرین ویرایش در {} را بار کنید؟" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "گشودن ویرایشگر غنی" msgstr "باز کردن ویرایشگر غنی"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "عنوان" msgstr "عنوان"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "زیرعنوان، یا چکیده" msgstr "زیرعنوان، یا چکیده"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "مقاله‌تان را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود." msgstr "مقاله را اینجا بنویسید. از مارک‌داون پشتیبانی می‌شود."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "نزدیک به {} حرف باقی مانده است" msgstr "نزدیک به {} حرف باقی مانده است"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "برچسب‌ها" msgstr "برچسب‌ها"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "پروانه" msgstr "پروانه"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "جلد" msgstr "تصویر شاخص"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "این، یک پیش‌نویس است" msgstr "این، یک پیش‌نویس است"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "انتشار" msgstr "انتشار"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Finnish\n" "Language-Team: Finnish\n"
"Language: fi_FI\n" "Language: fi_FI\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Avaa edistynyt tekstieditori" msgstr "Avaa edistynyt tekstieditori"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Otsikko" msgstr "Otsikko"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Alaotsikko tai tiivistelmä" msgstr "Alaotsikko tai tiivistelmä"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu." msgstr "Kirjoita artikkelisi tähän. Markdown -kuvauskieli on tuettu."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "%{count} merkkiä jäljellä" msgstr "%{count} merkkiä jäljellä"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagit" msgstr "Tagit"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Lisenssi" msgstr "Lisenssi"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Kansi" msgstr "Kansi"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Tämä on luonnos" msgstr "Tämä on luonnos"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Julkaise" msgstr "Julkaise"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-05-09 09:59\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Voulez vous charger la sauvegarde automatique locale, éditée la dernière fois à {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Ouvrir l'éditeur de texte avancé" msgstr "Ouvrir l'éditeur de texte avancé"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titre" msgstr "Titre"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Sous-titre ou résumé" msgstr "Sous-titre ou résumé"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown." msgstr "Écrivez votre article ici. Vous pouvez utiliser du Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Environ {} caractères restant" msgstr "Environ {} caractères restant"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Étiquettes" msgstr "Étiquettes"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licence" msgstr "Licence"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Illustration" msgstr "Illustration"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Ceci est un brouillon" msgstr "Ceci est un brouillon"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publier" msgstr "Publier"
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-26 13:16\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Galician\n" "Language-Team: Galician\n"
"Language: gl_ES\n" "Language: gl_ES\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Queres cargar a última copia gardada editada o {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abre o editor de texto enriquecido" msgstr "Abre o editor de texto enriquecido"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo, ou resumo" msgstr "Subtítulo, ou resumo"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escribe aquí o teu artigo: podes utilizar Markdown." msgstr "Escribe aquí o teu artigo: podes utilizar Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Dispós de {} caracteres" msgstr "Dispós de {} caracteres"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenza" msgstr "Licenza"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Portada" msgstr "Portada"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Este é un borrador" msgstr "Este é un borrador"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hebrew\n" "Language-Team: Hebrew\n"
"Language: he_IL\n" "Language: he_IL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hindi\n" "Language-Team: Hindi\n"
"Language: hi_IN\n" "Language: hi_IN\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "शीर्षक" msgstr "शीर्षक"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है." msgstr "अपना आर्टिकल या लेख यहाँ लिखें. Markdown उपलब्ध है."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "लगभग {} अक्षर बाकी हैं" msgstr "लगभग {} अक्षर बाकी हैं"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "टैग्स" msgstr "टैग्स"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "लाइसेंस" msgstr "लाइसेंस"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "पब्लिश करें" msgstr "पब्लिश करें"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Language: hr_HR\n" "Language: hr_HR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Naslov" msgstr "Naslov"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagovi" msgstr "Tagovi"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenca" msgstr "Licenca"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Objavi" msgstr "Objavi"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
"Language: hu_HU\n" "Language: hu_HU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Language: it_IT\n" "Language: it_IT\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Apri il compositore di testo avanzato" msgstr "Apri il compositore di testo avanzato"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titolo" msgstr "Titolo"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Sottotitolo, o sommario" msgstr "Sottotitolo, o sommario"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Scrivi qui il tuo articolo. È supportato il Markdown." msgstr "Scrivi qui il tuo articolo. È supportato il Markdown."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Circa {} caratteri rimasti" msgstr "Circa {} caratteri rimasti"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etichette" msgstr "Etichette"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenza" msgstr "Licenza"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Copertina" msgstr "Copertina"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Questa è una bozza" msgstr "Questa è una bozza"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Pubblica" msgstr "Pubblica"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"Language: ja_JP\n" "Language: ja_JP\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "リッチテキストエディターを開く" msgstr "リッチテキストエディターを開く"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "タイトル" msgstr "タイトル"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "サブタイトル、または概要" msgstr "サブタイトル、または概要"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "投稿をここに書きます。Markdown がサポートされています。" msgstr "投稿をここに書きます。Markdown がサポートされています。"
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "残り約 {} 文字" msgstr "残り約 {} 文字"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "タグ" msgstr "タグ"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "ライセンス" msgstr "ライセンス"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "カバー" msgstr "カバー"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "これは下書きです" msgstr "これは下書きです"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "公開" msgstr "公開"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Korean\n" "Language-Team: Korean\n"
"Language: ko_KR\n" "Language: ko_KR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:55\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Dutch\n" "Language-Team: Dutch\n"
"Language: nl_NL\n" "Language: nl_NL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Wilt u de lokale auto-opslaan laden? Laatst bewerkt om: {}" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Open de rich-text editor" msgstr "Open de rich-text editor"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Ondertitel of samenvatting" msgstr "Ondertitel of samenvatting"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Schrijf hier je artikel. Markdown wordt ondersteund." msgstr "Schrijf hier je artikel. Markdown wordt ondersteund."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Ongeveer {} tekens over" msgstr "Ongeveer {} tekens over"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licentie" msgstr "Licentie"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Hoofdafbeelding" msgstr "Hoofdafbeelding"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Dit is een concept" msgstr "Dit is een concept"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publiceren" msgstr "Publiceren"
+14 -14
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Norwegian\n" "Language-Team: Norwegian\n"
"Language: no_NO\n" "Language: no_NO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Lisens" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publiser" msgstr ""
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Polish\n" "Language-Team: Polish\n"
"Language: pl_PL\n" "Language: pl_PL\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Otwórz edytor tekstu sformatowanego" msgstr "Otwórz edytor tekstu sformatowanego"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Tytuł" msgstr "Tytuł"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Podtytuł, lub podsumowanie" msgstr "Podtytuł, lub podsumowanie"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany." msgstr "Napisz swój artykuł tutaj. Markdown jest obsługiwany."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Pozostało w okolicy {} znaków" msgstr "Pozostało w okolicy {} znaków"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tagi" msgstr "Tagi"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licencja" msgstr "Licencja"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Okładka" msgstr "Okładka"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "To jest szkic" msgstr "To jest szkic"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publikuj" msgstr "Publikuj"
+11
View File
@@ -12,35 +12,46 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
# plume-front/src/editor.rs:188
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:281
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "" msgstr ""
# plume-front/src/editor.rs:314
msgid "Title" msgid "Title"
msgstr "" msgstr ""
# plume-front/src/editor.rs:318
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:325
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "" msgstr ""
# plume-front/src/editor.rs:336
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "" msgstr ""
# plume-front/src/editor.rs:413
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
# plume-front/src/editor.rs:414
msgid "License" msgid "License"
msgstr "" msgstr ""
# plume-front/src/editor.rs:417
msgid "Cover" msgid "Cover"
msgstr "" msgstr ""
# plume-front/src/editor.rs:437
msgid "This is a draft" msgid "This is a draft"
msgstr "" msgstr ""
# plume-front/src/editor.rs:444
msgid "Publish" msgid "Publish"
msgstr "" msgstr ""
+13 -13
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n" "Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n" "Language: pt_BR\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Você quer carregar o último conteúdo salvo localmente editado em {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Abrir o editor de rich text" msgstr "Abrir o editor de rich text"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Título" msgstr "Título"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Subtítulo ou resumo" msgstr "Subtítulo ou resumo"
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Escreva seu artigo aqui. Markdown é suportado." msgstr "Escreva seu artigo aqui. Markdown é suportado."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Cerca de {} caracteres restantes" msgstr "Cerca de {} caracteres restantes"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licença" msgstr "Licença"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Capa" msgstr "Capa"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Isso é um rascunho" msgstr "Isso é um rascunho"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publicar" msgstr "Publicar"
+12 -12
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-01-12 01:20\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Romanian\n" "Language-Team: Romanian\n"
"Language: ro_RO\n" "Language: ro_RO\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Deschide editorul de text" msgstr "Deschide editorul de text"
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Titlu" msgstr "Titlu"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Scrie articolul tău aici. Markdown este acceptat." msgstr "Scrie articolul tău aici. Markdown este acceptat."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "În apropiere de {} caractere rămase" msgstr "În apropiere de {} caractere rămase"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Etichete" msgstr "Etichete"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Licenţă" msgstr "Licenţă"
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Coperta" msgstr "Coperta"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Aceasta este o ciornă" msgstr "Aceasta este o ciornă"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Publică" msgstr "Publică"
+16 -16
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: plume\n" "Project-Id-Version: plume\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n" "POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2022-05-09 09:59\n" "PO-Revision-Date: 2020-12-19 09:56\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Language: ru_RU\n" "Language: ru_RU\n"
@@ -17,47 +17,47 @@ msgstr ""
"X-Crowdin-File: /master/po/plume-front/plume-front.pot\n" "X-Crowdin-File: /master/po/plume-front/plume-front.pot\n"
"X-Crowdin-File-ID: 12\n" "X-Crowdin-File-ID: 12\n"
# plume-front/src/editor.rs:172 # plume-front/src/editor.rs:189
msgid "Do you want to load the local autosave last edited at {}?" msgid "Do you want to load the local autosave last edited at {}?"
msgstr "Хотите ли вы загрузить локальное автосохранение, сделанное в {}?" msgstr ""
# plume-front/src/editor.rs:326 # plume-front/src/editor.rs:282
msgid "Open the rich text editor" msgid "Open the rich text editor"
msgstr "Открыть в визуальном редакторе" msgstr ""
# plume-front/src/editor.rs:385 # plume-front/src/editor.rs:315
msgid "Title" msgid "Title"
msgstr "Заголовок" msgstr "Заголовок"
# plume-front/src/editor.rs:389 # plume-front/src/editor.rs:319
msgid "Subtitle, or summary" msgid "Subtitle, or summary"
msgstr "Подзаголовок или резюме" msgstr ""
# plume-front/src/editor.rs:396 # plume-front/src/editor.rs:326
msgid "Write your article here. Markdown is supported." msgid "Write your article here. Markdown is supported."
msgstr "Пишите свою статью здесь. Markdown поддерживается." msgstr "Пишите свою статью здесь. Markdown поддерживается."
# plume-front/src/editor.rs:407 # plume-front/src/editor.rs:337
msgid "Around {} characters left" msgid "Around {} characters left"
msgstr "Осталось около {} символов" msgstr "Осталось около {} символов"
# plume-front/src/editor.rs:517 # plume-front/src/editor.rs:414
msgid "Tags" msgid "Tags"
msgstr "Теги" msgstr "Теги"
# plume-front/src/editor.rs:518 # plume-front/src/editor.rs:415
msgid "License" msgid "License"
msgstr "Лицензия" msgstr ""
# plume-front/src/editor.rs:524 # plume-front/src/editor.rs:418
msgid "Cover" msgid "Cover"
msgstr "Обложка" msgstr "Обложка"
# plume-front/src/editor.rs:564 # plume-front/src/editor.rs:438
msgid "This is a draft" msgid "This is a draft"
msgstr "Это черновик" msgstr "Это черновик"
# plume-front/src/editor.rs:575 # plume-front/src/editor.rs:445
msgid "Publish" msgid "Publish"
msgstr "Опубликовать" msgstr "Опубликовать"

Some files were not shown because too many files have changed in this diff Show More