Compare commits
38 Commits
main
...
blog-title
Author | SHA1 | Date | |
---|---|---|---|
|
9bd8f5272f | ||
|
39cd4f830d | ||
|
cd9cb311c7 | ||
|
83ed168f9c | ||
|
83c628d490 | ||
|
badff3f3cb | ||
|
ba00e36884 | ||
|
5ee84427bf | ||
|
f203dddae5 | ||
|
ba1eac9482 | ||
|
3dad83b179 | ||
|
4eab51b159 | ||
|
abf0b28fd4 | ||
|
115b5b31a4 | ||
|
5a03fd7340 | ||
|
e75449410f | ||
|
c9bb31b8f5 | ||
|
0c2eaf0f1b | ||
|
71824aa524 | ||
|
fc848a8d53 | ||
|
53cdd8198b | ||
|
08f4dac3d3 | ||
|
18a9ed5504 | ||
|
631359c3f7 | ||
|
3111fa0735 | ||
|
890c9a0da4 | ||
|
22b03710be | ||
|
e3609f7863 | ||
|
0714d2d010 | ||
|
5bd084eff7 | ||
|
f369fa9b25 | ||
|
8afcc1511e | ||
|
ce89faef84 | ||
|
e18b6e78f2 | ||
|
31e817385d | ||
|
af7ed450e2 | ||
|
55a5a64b1a | ||
|
08b7d100fd |
2
.gitignore
vendored
2
.gitignore
vendored
@ -20,5 +20,3 @@ search_index
|
|||||||
__pycache__
|
__pycache__
|
||||||
.vscode/
|
.vscode/
|
||||||
*-journal
|
*-journal
|
||||||
.direnv/
|
|
||||||
build.log*
|
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -8,16 +8,11 @@
|
|||||||
|
|
||||||
- Add 'My feed' to i18n timeline name (#1084)
|
- Add 'My feed' to i18n timeline name (#1084)
|
||||||
- Bidirectional support for user page header (#1092)
|
- Bidirectional support for user page header (#1092)
|
||||||
- Add non anonymous bind to LDAP server, taken from https://git.joinplu.me/Plume/Plume/src/branch/ldap-non-anon PR
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Use blog title as slug (#1094, #1126, #1127)
|
- Use blog title as slug (#1094, #1126, #1127)
|
||||||
- Bump Rust to nightly 2022-07-19 (#1119)
|
- Bump Rust to nightly 2022-07-19 (#1119)
|
||||||
- Force LDAP simple bind with *cn* rdn instead of *uid*
|
|
||||||
- Update rust-toolchain to nightly-2023-04-14
|
|
||||||
- Update chrono from 0.4.0 to 0.4.31
|
|
||||||
- Update scheduled-thread-pool from 0.2.6 to 0.2.7
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@ -27,12 +22,6 @@
|
|||||||
- Allow empty avatar for remote users (#1129)
|
- Allow empty avatar for remote users (#1129)
|
||||||
- Percent encode blog FQN for federation interoperability (#1129)
|
- Percent encode blog FQN for federation interoperability (#1129)
|
||||||
- The same to `preferredUsername` (#1129)
|
- The same to `preferredUsername` (#1129)
|
||||||
- Deprecation warnings during build process(see rust crate updates)
|
|
||||||
- Server error 500 creating new blog with white spaces inside title. Bug reported on https://git.joinplu.me/Plume/Plume/issues/1152
|
|
||||||
- Show _Subscribe_ button in column format instead of row format in screen smaller than 600px. https://git.lainoa.eus/aitzol/Plume/commit/db8cc6e7e8351a5d74f7ce0399126e13493c62d9
|
|
||||||
### To do
|
|
||||||
|
|
||||||
- Choose rdn via environment variables for LDAP simple bind
|
|
||||||
|
|
||||||
## [[0.7.2]] - 2022-05-11
|
## [[0.7.2]] - 2022-05-11
|
||||||
|
|
||||||
|
573
Cargo.lock
generated
573
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
version = "0.7.3-dev-fork"
|
version = "0.7.3-dev"
|
||||||
repository = "https://git.lainoa.eus/aitzol/Plume"
|
repository = "https://github.com/Plume-org/Plume"
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atom_syndication = "0.12.0"
|
atom_syndication = "0.11.0"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
gettext = "0.4.0"
|
gettext = "0.4.0"
|
||||||
@ -14,11 +14,11 @@ gettext-macros = "0.6.1"
|
|||||||
gettext-utils = "0.1.0"
|
gettext-utils = "0.1.0"
|
||||||
guid-create = "0.2"
|
guid-create = "0.2"
|
||||||
lettre_email = "0.9.2"
|
lettre_email = "0.9.2"
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.10"
|
||||||
rocket = "0.4.11"
|
rocket = "0.4.11"
|
||||||
rocket_contrib = { version = "0.4.11", features = ["json"] }
|
rocket_contrib = { version = "0.4.11", features = ["json"] }
|
||||||
rocket_i18n = "0.4.1"
|
rocket_i18n = "0.4.1"
|
||||||
scheduled-thread-pool = "0.2.7"
|
scheduled-thread-pool = "0.2.6"
|
||||||
serde = "1.0.137"
|
serde = "1.0.137"
|
||||||
serde_json = "1.0.81"
|
serde_json = "1.0.81"
|
||||||
shrinkwraprs = "0.3.0"
|
shrinkwraprs = "0.3.0"
|
||||||
@ -35,7 +35,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4.31"
|
version = "0.4"
|
||||||
|
|
||||||
[dependencies.ctrlc]
|
[dependencies.ctrlc]
|
||||||
features = ["termination"]
|
features = ["termination"]
|
||||||
@ -64,17 +64,16 @@ git = "https://git.joinplu.me/plume/rocket_csrf"
|
|||||||
rev = "0.1.2"
|
rev = "0.1.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = "0.15.0"
|
ructe = "0.14.0"
|
||||||
rsass = "0.26"
|
rsass = "0.25"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["postgres", "s3"]
|
default = ["postgres"]
|
||||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
debug-mailer = []
|
debug-mailer = []
|
||||||
test = []
|
test = []
|
||||||
search-lindera = ["plume-models/search-lindera"]
|
search-lindera = ["plume-models/search-lindera"]
|
||||||
s3 = ["plume-models/s3"]
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
||||||
|
@ -30,7 +30,8 @@ FROM debian:stable-slim
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
libpq5
|
libpq5 \
|
||||||
|
libssl1.1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
@ -228,7 +228,7 @@ main .article-meta {
|
|||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
.action.liked:hover svg.feather {
|
.action.liked:hover svg.feather {
|
||||||
background: transparentize($red, 0.75);
|
background: transparentize($red, 0.75)
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +252,7 @@ main .article-meta {
|
|||||||
background: $primary;
|
background: $primary;
|
||||||
}
|
}
|
||||||
.action.reshared:hover svg.feather {
|
.action.reshared:hover svg.feather {
|
||||||
background: transparentize($primary, 0.75);
|
background: transparentize($primary, 0.75)
|
||||||
color: $primary;
|
color: $primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
116
flake.lock
116
flake.lock
@ -1,116 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681202837,
|
|
||||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681202837,
|
|
||||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1683408522,
|
|
||||||
"narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1683857898,
|
|
||||||
"narHash": "sha256-pyVY4UxM6zUX97g6bk6UyCbZGCWZb2Zykrne8YxacRA=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "4e7fba3f37f5e184ada0ef3cf1e4d8ef450f240b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
60
flake.nix
60
flake.nix
@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Developpment shell for Plume including nightly Rust compiler";
|
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
inputs.rust-overlay = {
|
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
overlays = [ (import rust-overlay) ];
|
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
|
||||||
inputs = with pkgs; [
|
|
||||||
(rust-bin.nightly.latest.default.override {
|
|
||||||
targets = [ "wasm32-unknown-unknown" ];
|
|
||||||
})
|
|
||||||
wasm-pack
|
|
||||||
openssl
|
|
||||||
pkg-config
|
|
||||||
gettext
|
|
||||||
postgresql
|
|
||||||
sqlite
|
|
||||||
];
|
|
||||||
in {
|
|
||||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
|
||||||
pname = "plume";
|
|
||||||
version = "0.7.3-dev";
|
|
||||||
|
|
||||||
src = ./.;
|
|
||||||
|
|
||||||
cargoLock = {
|
|
||||||
lockFile = ./Cargo.lock;
|
|
||||||
outputHashes = {
|
|
||||||
"pulldown-cmark-0.8.0" = "sha256-lpfoRDuY3zJ3QmUqJ5k9OL0MEdGDpwmpJ+u5BCj2kIA=";
|
|
||||||
"rocket_csrf-0.1.2" = "sha256-WywZfMiwZqTPfSDcAE7ivTSYSaFX+N9fjnRsLSLb9wE=";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildNoDefaultFeatures = true;
|
|
||||||
buildFeatures = ["postgresql" "s3"];
|
|
||||||
|
|
||||||
nativeBuildInputs = inputs;
|
|
||||||
|
|
||||||
buildPhase = ''
|
|
||||||
wasm-pack build --target web --release plume-front
|
|
||||||
cargo build --no-default-features --features postgresql,s3 --path .
|
|
||||||
cargo build --no-default-features --features postgresql,s3 --path plume-cli
|
|
||||||
'';
|
|
||||||
installPhase = ''
|
|
||||||
cargo install --no-default-features --features postgresql,s3 --path . --target-dir $out
|
|
||||||
cargo install --no-default-features --features postgresql,s3 --path plume-cli --target-dir $out
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
packages = inputs;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE users DROP CONSTRAINT users_fqn;
|
||||||
|
ALTER TABLE blogs DROP CONSTRAINT blogs_actor_id;
|
||||||
|
ALTER TABLE blogs DROP CONSTRAINT blogs_fqn;
|
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE users ADD CONSTRAINT users_fqn UNIQUE (fqn);
|
||||||
|
ALTER TABLE blogs ADD CONSTRAINT blogs_actor_id UNIQUE (actor_id);
|
||||||
|
ALTER TABLE blogs ADD CONSTRAINT blogs_fqn UNIQUE (fqn);
|
@ -0,0 +1,3 @@
|
|||||||
|
DROP INDEX users_fqn;
|
||||||
|
DROP INDEX blogs_actor_id;
|
||||||
|
DROP INDEX blogs_fqn;
|
@ -0,0 +1,3 @@
|
|||||||
|
CREATE UNIQUE INDEX users_fqn ON users (fqn);
|
||||||
|
CREATE UNIQUE INDEX blogs_actor_id ON blogs (actor_id);
|
||||||
|
CREATE UNIQUE INDEX blogs_fqn ON blogs (fqn);
|
@ -24,4 +24,3 @@ path = "../plume-models"
|
|||||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
search-lindera = ["plume-models/search-lindera"]
|
search-lindera = ["plume-models/search-lindera"]
|
||||||
s3 = ["plume-models/s3"]
|
|
||||||
|
@ -1,262 +0,0 @@
|
|||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
|
|
||||||
use plume_models::{blogs::Blog, instance::Instance, lists::*, users::User, Connection};
|
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
|
||||||
SubCommand::with_name("lists")
|
|
||||||
.about("Manage lists")
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("new")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of this list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("type")
|
|
||||||
.short("t")
|
|
||||||
.long("type")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
r#"The type of this list (one of "user", "blog", "word" or "prefix")"#,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list is for. Empty for an instance list"),
|
|
||||||
)
|
|
||||||
.about("Create a new list"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("delete")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the list to delete"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list was for. Empty for instance list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("yes")
|
|
||||||
.short("y")
|
|
||||||
.long("yes")
|
|
||||||
.help("Confirm the deletion"),
|
|
||||||
)
|
|
||||||
.about("Delete a list"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("add")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the list to add an element to"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list is for. Empty for instance list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("value")
|
|
||||||
.short("v")
|
|
||||||
.long("value")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The value to add"),
|
|
||||||
)
|
|
||||||
.about("Add element to a list"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("rm")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the list to remove an element from"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this list is for. Empty for instance list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("value")
|
|
||||||
.short("v")
|
|
||||||
.long("value")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The value to remove"),
|
|
||||||
)
|
|
||||||
.about("Remove element from list"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|
||||||
let conn = conn;
|
|
||||||
match args.subcommand() {
|
|
||||||
("new", Some(x)) => new(x, conn),
|
|
||||||
("delete", Some(x)) => delete(x, conn),
|
|
||||||
("add", Some(x)) => add(x, conn),
|
|
||||||
("rm", Some(x)) => rm(x, conn),
|
|
||||||
("", None) => command().print_help().unwrap(),
|
|
||||||
_ => println!("Unknown subcommand"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
|
|
||||||
let name = args
|
|
||||||
.value_of("name")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No name provided for the list");
|
|
||||||
let user = args.value_of("user").map(String::from);
|
|
||||||
(name, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_type(args: &ArgMatches<'_>) -> ListType {
|
|
||||||
let typ = args
|
|
||||||
.value_of("type")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No name type for the list");
|
|
||||||
match typ.as_str() {
|
|
||||||
"user" => ListType::User,
|
|
||||||
"blog" => ListType::Blog,
|
|
||||||
"word" => ListType::Word,
|
|
||||||
"prefix" => ListType::Prefix,
|
|
||||||
_ => panic!("Invalid list type: {}", typ),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_value(args: &ArgMatches<'_>) -> String {
|
|
||||||
args.value_of("value")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No query provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_user(username: &str, conn: &Connection) -> User {
|
|
||||||
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
|
|
||||||
|
|
||||||
User::find_by_name(conn, username, instance.id).expect("User not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
let typ = get_list_type(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
List::new(conn, &name, user.as_ref(), typ).expect("failed to create list");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
|
|
||||||
if !args.is_present("yes") {
|
|
||||||
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let list =
|
|
||||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
|
||||||
|
|
||||||
list.delete(conn).expect("Failed to update list");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
let value = get_value(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let list =
|
|
||||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
|
||||||
|
|
||||||
match list.kind() {
|
|
||||||
ListType::Blog => {
|
|
||||||
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
|
|
||||||
if !list.contains_blog(conn, blog_id).unwrap() {
|
|
||||||
list.add_blogs(conn, &[blog_id]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::User => {
|
|
||||||
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
|
|
||||||
if !list.contains_user(conn, user_id).unwrap() {
|
|
||||||
list.add_users(conn, &[user_id]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Word => {
|
|
||||||
if !list.contains_word(conn, &value).unwrap() {
|
|
||||||
list.add_words(conn, &[&value]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Prefix => {
|
|
||||||
if !list.contains_prefix(conn, &value).unwrap() {
|
|
||||||
list.add_prefixes(conn, &[&value]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rm(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_list_identifier(args);
|
|
||||||
let value = get_value(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let list =
|
|
||||||
List::find_for_user_by_name(conn, user.map(|u| u.id), &name).expect("list not found");
|
|
||||||
|
|
||||||
match list.kind() {
|
|
||||||
ListType::Blog => {
|
|
||||||
let blog_id = Blog::find_by_fqn(conn, &value).expect("unknown blog").id;
|
|
||||||
let mut blogs = list.list_blogs(conn).unwrap();
|
|
||||||
if let Some(index) = blogs.iter().position(|b| b.id == blog_id) {
|
|
||||||
blogs.swap_remove(index);
|
|
||||||
let blogs = blogs.iter().map(|b| b.id).collect::<Vec<_>>();
|
|
||||||
list.set_blogs(conn, &blogs).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::User => {
|
|
||||||
let user_id = User::find_by_fqn(conn, &value).expect("unknown user").id;
|
|
||||||
let mut users = list.list_users(conn).unwrap();
|
|
||||||
if let Some(index) = users.iter().position(|u| u.id == user_id) {
|
|
||||||
users.swap_remove(index);
|
|
||||||
let users = users.iter().map(|u| u.id).collect::<Vec<_>>();
|
|
||||||
list.set_users(conn, &users).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Word => {
|
|
||||||
let mut words = list.list_words(conn).unwrap();
|
|
||||||
if let Some(index) = words.iter().position(|w| *w == value) {
|
|
||||||
words.swap_remove(index);
|
|
||||||
let words = words.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
list.set_words(conn, &words).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListType::Prefix => {
|
|
||||||
let mut prefixes = list.list_prefixes(conn).unwrap();
|
|
||||||
if let Some(index) = prefixes.iter().position(|p| *p == value) {
|
|
||||||
prefixes.swap_remove(index);
|
|
||||||
let prefixes = prefixes.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
list.set_prefixes(conn, &prefixes).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,10 +4,8 @@ use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
|||||||
use std::io::{self, prelude::*};
|
use std::io::{self, prelude::*};
|
||||||
|
|
||||||
mod instance;
|
mod instance;
|
||||||
mod list;
|
|
||||||
mod migration;
|
mod migration;
|
||||||
mod search;
|
mod search;
|
||||||
mod timeline;
|
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -18,8 +16,6 @@ fn main() {
|
|||||||
.subcommand(instance::command())
|
.subcommand(instance::command())
|
||||||
.subcommand(migration::command())
|
.subcommand(migration::command())
|
||||||
.subcommand(search::command())
|
.subcommand(search::command())
|
||||||
.subcommand(timeline::command())
|
|
||||||
.subcommand(list::command())
|
|
||||||
.subcommand(users::command());
|
.subcommand(users::command());
|
||||||
let matches = app.clone().get_matches();
|
let matches = app.clone().get_matches();
|
||||||
|
|
||||||
@ -41,10 +37,6 @@ fn main() {
|
|||||||
("search", Some(args)) => {
|
("search", Some(args)) => {
|
||||||
search::run(args, &conn.expect("Couldn't connect to the database."))
|
search::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
}
|
}
|
||||||
("timeline", Some(args)) => {
|
|
||||||
timeline::run(args, &conn.expect("Couldn't connect to the database."))
|
|
||||||
}
|
|
||||||
("lists", Some(args)) => list::run(args, &conn.expect("Couldn't connect to the database.")),
|
|
||||||
("users", Some(args)) => {
|
("users", Some(args)) => {
|
||||||
users::run(args, &conn.expect("Couldn't connect to the database."))
|
users::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
}
|
}
|
||||||
|
@ -1,257 +0,0 @@
|
|||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
|
|
||||||
use plume_models::{instance::Instance, posts::Post, timeline::*, users::*, Connection};
|
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
|
||||||
SubCommand::with_name("timeline")
|
|
||||||
.about("Manage public timeline")
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("new")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of this timeline"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("query")
|
|
||||||
.short("q")
|
|
||||||
.long("query")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The query posts in this timelines have to match"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"Username of whom this timeline is for. Empty for an instance timeline",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("preload-count")
|
|
||||||
.short("p")
|
|
||||||
.long("preload-count")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Number of posts to try to preload in this timeline at its creation"),
|
|
||||||
)
|
|
||||||
.about("Create a new timeline"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("delete")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the timeline to delete"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"Username of whom this timeline was for. Empty for instance timeline",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("yes")
|
|
||||||
.short("y")
|
|
||||||
.long("yes")
|
|
||||||
.help("Confirm the deletion"),
|
|
||||||
)
|
|
||||||
.about("Delete a timeline"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("edit")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the timeline to edit"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Username of whom this timeline is for. Empty for instance timeline"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("query")
|
|
||||||
.short("q")
|
|
||||||
.long("query")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The query posts in this timelines have to match"),
|
|
||||||
)
|
|
||||||
.about("Edit the query of a timeline"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
SubCommand::with_name("repopulate")
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("name")
|
|
||||||
.short("n")
|
|
||||||
.long("name")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("The name of the timeline to repopulate"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("user")
|
|
||||||
.short("u")
|
|
||||||
.long("user")
|
|
||||||
.takes_value(true)
|
|
||||||
.help(
|
|
||||||
"Username of whom this timeline was for. Empty for instance timeline",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("preload-count")
|
|
||||||
.short("p")
|
|
||||||
.long("preload-count")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Number of posts to try to preload in this timeline at its creation"),
|
|
||||||
)
|
|
||||||
.about("Repopulate a timeline. Run this after modifying a list the timeline depends on."),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|
||||||
let conn = conn;
|
|
||||||
match args.subcommand() {
|
|
||||||
("new", Some(x)) => new(x, conn),
|
|
||||||
("edit", Some(x)) => edit(x, conn),
|
|
||||||
("delete", Some(x)) => delete(x, conn),
|
|
||||||
("repopulate", Some(x)) => repopulate(x, conn),
|
|
||||||
("", None) => command().print_help().unwrap(),
|
|
||||||
_ => println!("Unknown subcommand"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_timeline_identifier(args: &ArgMatches<'_>) -> (String, Option<String>) {
|
|
||||||
let name = args
|
|
||||||
.value_of("name")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No name provided for the timeline");
|
|
||||||
let user = args.value_of("user").map(String::from);
|
|
||||||
(name, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_query(args: &ArgMatches<'_>) -> String {
|
|
||||||
let query = args
|
|
||||||
.value_of("query")
|
|
||||||
.map(String::from)
|
|
||||||
.expect("No query provided");
|
|
||||||
|
|
||||||
match TimelineQuery::parse(&query) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(QueryError::SyntaxError(start, end, message)) => panic!(
|
|
||||||
"Query parsing error between {} and {}: {}",
|
|
||||||
start, end, message
|
|
||||||
),
|
|
||||||
Err(QueryError::UnexpectedEndOfQuery) => {
|
|
||||||
panic!("Query parsing error: unexpected end of query")
|
|
||||||
}
|
|
||||||
Err(QueryError::RuntimeError(message)) => panic!("Query parsing error: {}", message),
|
|
||||||
}
|
|
||||||
|
|
||||||
query
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_preload_count(args: &ArgMatches<'_>) -> usize {
|
|
||||||
args.value_of("preload-count")
|
|
||||||
.map(|arg| arg.parse().expect("invalid preload-count"))
|
|
||||||
.unwrap_or(plume_models::ITEMS_PER_PAGE as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_user(username: &str, conn: &Connection) -> User {
|
|
||||||
let instance = Instance::get_local_uncached(conn).expect("Failed to load local instance");
|
|
||||||
|
|
||||||
User::find_by_name(conn, username, instance.id).expect("User not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preload(timeline: Timeline, count: usize, conn: &Connection) {
|
|
||||||
timeline.remove_all_posts(conn).unwrap();
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut posts = Vec::with_capacity(count as usize);
|
|
||||||
for post in Post::list_filtered(conn, None, None, None)
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
{
|
|
||||||
if timeline.matches(conn, &post, Kind::Original).unwrap() {
|
|
||||||
posts.push(post);
|
|
||||||
if posts.len() >= count {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for post in posts.iter().rev() {
|
|
||||||
timeline.add_post(conn, post).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
let query = get_query(args);
|
|
||||||
let preload_count = get_preload_count(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let timeline = if let Some(user) = user {
|
|
||||||
Timeline::new_for_user(conn, user.id, name, query)
|
|
||||||
} else {
|
|
||||||
Timeline::new_for_instance(conn, name, query)
|
|
||||||
}
|
|
||||||
.expect("Failed to create new timeline");
|
|
||||||
|
|
||||||
preload(timeline, preload_count, conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn edit(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
let query = get_query(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let mut timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
|
||||||
.expect("timeline not found");
|
|
||||||
|
|
||||||
timeline.query = query;
|
|
||||||
|
|
||||||
timeline.update(conn).expect("Failed to update timeline");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
|
|
||||||
if !args.is_present("yes") {
|
|
||||||
panic!("Warning, this operation is destructive. Add --yes to confirm you want to do it.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
|
||||||
.expect("timeline not found");
|
|
||||||
|
|
||||||
timeline.delete(conn).expect("Failed to update timeline");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn repopulate(args: &ArgMatches<'_>, conn: &Connection) {
|
|
||||||
let (name, user) = get_timeline_identifier(args);
|
|
||||||
let preload_count = get_preload_count(args);
|
|
||||||
|
|
||||||
let user = user.map(|user| resolve_user(&user, conn));
|
|
||||||
|
|
||||||
let timeline = Timeline::find_for_user_by_name(conn, user.map(|u| u.id), &name)
|
|
||||||
.expect("timeline not found");
|
|
||||||
preload(timeline, preload_count, conn);
|
|
||||||
}
|
|
@ -25,10 +25,11 @@ url = "2.2.2"
|
|||||||
flume = "0.10.13"
|
flume = "0.10.13"
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
|
thiserror = "1.0.38"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4.31"
|
version = "0.4"
|
||||||
|
|
||||||
[dependencies.pulldown-cmark]
|
[dependencies.pulldown-cmark]
|
||||||
default-features = false
|
default-features = false
|
||||||
|
@ -18,6 +18,11 @@ use rocket::{
|
|||||||
response::{Responder, Response},
|
response::{Responder, Response},
|
||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
fmt,
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
runtime,
|
runtime,
|
||||||
time::{sleep, Duration},
|
time::{sleep, Duration},
|
||||||
@ -241,6 +246,97 @@ pub trait IntoId {
|
|||||||
fn into_id(self) -> Id;
|
fn into_id(self) -> Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum PreferredUsernameError {
|
||||||
|
#[error("preferredUsername must be longer than 2 characters")]
|
||||||
|
TooShort,
|
||||||
|
#[error("Invaliad character at {character:?}: {position:?}")]
|
||||||
|
InvalidCharacter { character: char, position: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Shrinkwrap, PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct PreferredUsername(String);
|
||||||
|
|
||||||
|
// Mastodon allows only /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i for `preferredUsername`
|
||||||
|
impl PreferredUsername {
|
||||||
|
fn validate(name: &str) -> std::result::Result<(), PreferredUsernameError> {
|
||||||
|
let len = name.len();
|
||||||
|
if len < 3 {
|
||||||
|
return Err(PreferredUsernameError::TooShort);
|
||||||
|
}
|
||||||
|
match name.chars().enumerate().find(|(pos, c)| {
|
||||||
|
if pos == &0 || pos == &(len - 1) {
|
||||||
|
c != &'_' && !c.is_ascii_alphanumeric()
|
||||||
|
} else {
|
||||||
|
match c {
|
||||||
|
'_' | '\\' | '.' | '-' => false,
|
||||||
|
_ => !c.is_ascii_alphanumeric(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Some((pos, c)) => Err(PreferredUsernameError::InvalidCharacter {
|
||||||
|
character: c,
|
||||||
|
position: pos,
|
||||||
|
}),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The given string must be match against /\A[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?\z/i in Ruby's RegExp which is required by Mastodon.
|
||||||
|
pub unsafe fn new_unchecked(name: String) -> Self {
|
||||||
|
Self(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(name: String) -> std::result::Result<Self, PreferredUsernameError> {
|
||||||
|
Self::validate(&name).map(|_| unsafe { Self::new_unchecked(name) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PreferredUsername {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for PreferredUsername {
|
||||||
|
type Error = PreferredUsernameError;
|
||||||
|
|
||||||
|
fn try_from(name: String) -> std::result::Result<Self, Self::Error> {
|
||||||
|
Self::new(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for PreferredUsername {
|
||||||
|
type Error = PreferredUsernameError;
|
||||||
|
|
||||||
|
fn try_from(name: &str) -> std::result::Result<Self, Self::Error> {
|
||||||
|
Self::new(name.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PreferredUsername> for String {
|
||||||
|
fn from(preferred_username: PreferredUsername) -> Self {
|
||||||
|
preferred_username.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for PreferredUsername {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PreferredUsername {
|
||||||
|
type Err = PreferredUsernameError;
|
||||||
|
|
||||||
|
fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
name.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ApSignature {
|
pub struct ApSignature {
|
||||||
@ -524,6 +620,35 @@ mod tests {
|
|||||||
use assert_json_diff::assert_json_eq;
|
use assert_json_diff::assert_json_eq;
|
||||||
use serde_json::{from_str, json, to_value};
|
use serde_json::{from_str, json, to_value};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preferred_username() {
|
||||||
|
assert!(PreferredUsername::new("".into()).is_err());
|
||||||
|
assert!(PreferredUsername::new("a".into()).is_err());
|
||||||
|
assert!(PreferredUsername::new("ab".into()).is_err());
|
||||||
|
assert_eq!(
|
||||||
|
"abc",
|
||||||
|
PreferredUsername::new("abc".into()).unwrap().as_str()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"abcd",
|
||||||
|
PreferredUsername::new("abcd".into()).unwrap().as_str()
|
||||||
|
);
|
||||||
|
assert!(PreferredUsername::new("abc-".into()).is_err());
|
||||||
|
assert!(PreferredUsername::new("日本語".into()).is_err());
|
||||||
|
assert_eq!("abc", "abc".parse::<PreferredUsername>().unwrap().as_str());
|
||||||
|
assert!("abc-".parse::<PreferredUsername>().is_err());
|
||||||
|
assert_eq!(
|
||||||
|
PreferredUsername::new("admin".into()).unwrap(),
|
||||||
|
PreferredUsername("admin".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefferred_username_to_string() {
|
||||||
|
let pu = PreferredUsername::new("admin".into()).unwrap();
|
||||||
|
assert_eq!("admin".to_string(), pu.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn se_ap_signature() {
|
fn se_ap_signature() {
|
||||||
let ap_signature = ApSignature {
|
let ap_signature = ApSignature {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
use activitystreams::iri_string::percent_encode::PercentEncodedForIri;
|
||||||
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 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;
|
||||||
@ -21,51 +21,7 @@ pub fn random_hex() -> String {
|
|||||||
* Intended to be used for generating Post ap_url.
|
* Intended to be used for generating Post ap_url.
|
||||||
*/
|
*/
|
||||||
pub fn iri_percent_encode_seg(segment: &str) -> String {
|
pub fn iri_percent_encode_seg(segment: &str) -> String {
|
||||||
segment.chars().map(iri_percent_encode_seg_char).collect()
|
PercentEncodedForIri::from_path_segment(segment).to_string()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iri_percent_encode_seg_char(c: char) -> String {
|
|
||||||
if c.is_alphanumeric() {
|
|
||||||
c.to_string()
|
|
||||||
} else {
|
|
||||||
match c {
|
|
||||||
'-'
|
|
||||||
| '.'
|
|
||||||
| '_'
|
|
||||||
| '~'
|
|
||||||
| '\u{A0}'..='\u{D7FF}'
|
|
||||||
| '\u{20000}'..='\u{2FFFD}'
|
|
||||||
| '\u{30000}'..='\u{3FFFD}'
|
|
||||||
| '\u{40000}'..='\u{4FFFD}'
|
|
||||||
| '\u{50000}'..='\u{5FFFD}'
|
|
||||||
| '\u{60000}'..='\u{6FFFD}'
|
|
||||||
| '\u{70000}'..='\u{7FFFD}'
|
|
||||||
| '\u{80000}'..='\u{8FFFD}'
|
|
||||||
| '\u{90000}'..='\u{9FFFD}'
|
|
||||||
| '\u{A0000}'..='\u{AFFFD}'
|
|
||||||
| '\u{B0000}'..='\u{BFFFD}'
|
|
||||||
| '\u{C0000}'..='\u{CFFFD}'
|
|
||||||
| '\u{D0000}'..='\u{DFFFD}'
|
|
||||||
| '\u{E0000}'..='\u{EFFFD}'
|
|
||||||
| '!'
|
|
||||||
| '$'
|
|
||||||
| '&'
|
|
||||||
| '\''
|
|
||||||
| '('
|
|
||||||
| ')'
|
|
||||||
| '*'
|
|
||||||
| '+'
|
|
||||||
| ','
|
|
||||||
| ';'
|
|
||||||
| '='
|
|
||||||
| ':'
|
|
||||||
| '@' => c.to_string(),
|
|
||||||
_ => {
|
|
||||||
let s = c.to_string();
|
|
||||||
Uri::percent_encode(&s).to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -16,9 +16,8 @@ openssl = "0.10.40"
|
|||||||
rocket = "0.4.11"
|
rocket = "0.4.11"
|
||||||
rocket_i18n = "0.4.1"
|
rocket_i18n = "0.4.1"
|
||||||
reqwest = "0.11.11"
|
reqwest = "0.11.11"
|
||||||
scheduled-thread-pool = "0.2.7"
|
scheduled-thread-pool = "0.2.6"
|
||||||
serde = "1.0.137"
|
serde = "1.0.137"
|
||||||
rust-s3 = { version = "0.33.0", optional = true, features = ["blocking"] }
|
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0.81"
|
serde_json = "1.0.81"
|
||||||
tantivy = "0.13.3"
|
tantivy = "0.13.3"
|
||||||
@ -28,7 +27,7 @@ webfinger = "0.4.1"
|
|||||||
whatlang = "0.16.2"
|
whatlang = "0.16.2"
|
||||||
shrinkwraprs = "0.3.0"
|
shrinkwraprs = "0.3.0"
|
||||||
diesel-derive-newtype = "1.0.0"
|
diesel-derive-newtype = "1.0.0"
|
||||||
glob = "0.3.1"
|
glob = "0.3.0"
|
||||||
lindera-tantivy = { version = "0.7.1", optional = true }
|
lindera-tantivy = { version = "0.7.1", optional = true }
|
||||||
tracing = "0.1.35"
|
tracing = "0.1.35"
|
||||||
riker = "0.4.2"
|
riker = "0.4.2"
|
||||||
@ -36,12 +35,11 @@ once_cell = "1.12.0"
|
|||||||
lettre = "0.9.6"
|
lettre = "0.9.6"
|
||||||
native-tls = "0.2.10"
|
native-tls = "0.2.10"
|
||||||
activitystreams = "=0.7.0-alpha.20"
|
activitystreams = "=0.7.0-alpha.20"
|
||||||
ahash = "=0.8.6"
|
heck = "0.4.0"
|
||||||
heck = "0.4.1"
|
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
version = "0.4.31"
|
version = "0.4"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
@ -64,4 +62,3 @@ diesel_migrations = "1.3.0"
|
|||||||
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
||||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
||||||
search-lindera = ["lindera-tantivy"]
|
search-lindera = ["lindera-tantivy"]
|
||||||
s3 = ["rust-s3"]
|
|
||||||
|
@ -5,7 +5,7 @@ use rocket::{
|
|||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Wrapper around User to use as a request guard on pages exclusively reserved to admins.
|
/// Wrapper around User to use as a request guard on pages reserved to admins.
|
||||||
pub struct Admin(pub User);
|
pub struct Admin(pub User);
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
||||||
@ -21,23 +21,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `Admin` but it forwards to next guard if the user is not an admin.
|
|
||||||
/// It's useful when there are multiple implementations of routes for admin and moderator.
|
|
||||||
pub struct InclusiveAdmin(pub User);
|
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for InclusiveAdmin {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<InclusiveAdmin, ()> {
|
|
||||||
let user = request.guard::<User>()?;
|
|
||||||
if user.is_admin() {
|
|
||||||
Outcome::Success(InclusiveAdmin(user))
|
|
||||||
} else {
|
|
||||||
Outcome::Forward(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `Admin` but for moderators.
|
/// Same as `Admin` but for moderators.
|
||||||
pub struct Moderator(pub User);
|
pub struct Moderator(pub User);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use heck::ToUpperCamelCase;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs, users::User,
|
db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
|
||||||
Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
|
schema::blogs, users::User, Connection, Error, Fqn, PlumeRocket, Result, CONFIG,
|
||||||
|
ITEMS_PER_PAGE,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
actor::{ApActor, ApActorExt, AsApActor, Group},
|
actor::{ApActor, ApActorExt, AsApActor, Group},
|
||||||
@ -43,14 +43,14 @@ pub struct Blog {
|
|||||||
pub ap_url: String,
|
pub ap_url: String,
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
pub fqn: String,
|
pub fqn: Fqn,
|
||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
pub icon_id: Option<i32>,
|
pub icon_id: Option<i32>,
|
||||||
pub banner_id: Option<i32>,
|
pub banner_id: Option<i32>,
|
||||||
pub theme: Option<String>,
|
pub theme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
#[derive(Insertable)]
|
||||||
#[table_name = "blogs"]
|
#[table_name = "blogs"]
|
||||||
pub struct NewBlog {
|
pub struct NewBlog {
|
||||||
pub actor_id: String,
|
pub actor_id: String,
|
||||||
@ -62,6 +62,7 @@ pub struct NewBlog {
|
|||||||
pub ap_url: String,
|
pub ap_url: String,
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
|
pub fqn: Fqn,
|
||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
pub icon_id: Option<i32>,
|
pub icon_id: Option<i32>,
|
||||||
pub banner_id: Option<i32>,
|
pub banner_id: Option<i32>,
|
||||||
@ -85,15 +86,13 @@ impl Blog {
|
|||||||
inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
|
inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.fqn.is_empty() {
|
if inserted.fqn.to_string().is_empty() {
|
||||||
|
// This might not enough for some titles such as all-Japanese title,
|
||||||
|
// but better than doing nothing.
|
||||||
if instance.local {
|
if instance.local {
|
||||||
inserted.fqn = iri_percent_encode_seg(&inserted.actor_id);
|
inserted.fqn = Fqn::make_local(&inserted.title)?;
|
||||||
} else {
|
} else {
|
||||||
inserted.fqn = format!(
|
inserted.fqn = Fqn::make_remote(&inserted.title, instance.public_domain)?;
|
||||||
"{}@{}",
|
|
||||||
iri_percent_encode_seg(&inserted.actor_id),
|
|
||||||
instance.public_domain
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,18 +102,9 @@ impl Blog {
|
|||||||
find_by!(blogs, find_by_ap_url, ap_url as &str);
|
find_by!(blogs, find_by_ap_url, ap_url as &str);
|
||||||
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
|
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
|
||||||
|
|
||||||
/// Remove non alphanumeric characters and CamelCase a string
|
pub fn slug(title: &str) -> String {
|
||||||
pub fn slug(title: &str) -> String {
|
iri_percent_encode_seg(title)
|
||||||
title.to_upper_camel_case()
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_alphanumeric())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
pub fn slug(title: &str) -> &str {
|
|
||||||
title
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
|
pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
|
||||||
Instance::get(conn, self.instance_id)
|
Instance::get(conn, self.instance_id)
|
||||||
@ -152,10 +142,10 @@ impl Blog {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<Blog> {
|
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<Blog> {
|
||||||
let from_db = blogs::table
|
let from_db = blogs::table
|
||||||
.filter(blogs::fqn.eq(fqn))
|
.filter(blogs::fqn.eq(fqn))
|
||||||
.first(conn)
|
.first(&**conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
if let Some(from_db) = from_db {
|
if let Some(from_db) = from_db {
|
||||||
Ok(from_db)
|
Ok(from_db)
|
||||||
@ -164,7 +154,7 @@ impl Blog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
|
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<Blog> {
|
||||||
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||||
.links
|
.links
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -183,7 +173,7 @@ 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 = ApActor::new(self.inbox_url.parse()?, Group::new());
|
||||||
blog.set_preferred_username(iri_percent_encode_seg(&self.actor_id));
|
blog.set_preferred_username(self.fqn.to_string());
|
||||||
blog.set_name(self.title.clone());
|
blog.set_name(self.title.clone());
|
||||||
blog.set_outbox(self.outbox_url.parse()?);
|
blog.set_outbox(self.outbox_url.parse()?);
|
||||||
blog.set_summary(self.summary_html.to_string());
|
blog.set_summary(self.summary_html.to_string());
|
||||||
@ -382,15 +372,15 @@ impl IntoId for Blog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Blog {
|
impl FromId<DbConn> for Blog {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = CustomGroup;
|
type Object = CustomGroup;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, acct: CustomGroup) -> Result<Self> {
|
fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result<Self> {
|
||||||
let (name, outbox_url, inbox_url) = {
|
let (name, outbox_url, inbox_url) = {
|
||||||
let actor = acct.ap_actor_ref();
|
let actor = acct.ap_actor_ref();
|
||||||
let name = actor
|
let name = actor
|
||||||
@ -408,22 +398,18 @@ impl FromId<Connection> for Blog {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut new_blog = NewBlog {
|
let actor_id = iri_percent_encode_seg(
|
||||||
actor_id: name.to_string(),
|
&acct
|
||||||
outbox_url,
|
.name()
|
||||||
inbox_url,
|
.and_then(|name| name.to_as_string())
|
||||||
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
|
.ok_or(Error::MissingApProperty)?,
|
||||||
private_key: None,
|
);
|
||||||
theme: None,
|
|
||||||
..NewBlog::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let object = ApObject::new(acct.inner);
|
let object = ApObject::new(acct.inner);
|
||||||
new_blog.title = object
|
let title = object
|
||||||
.name()
|
.name()
|
||||||
.and_then(|name| name.to_as_string())
|
.and_then(|name| name.to_as_string())
|
||||||
.unwrap_or(name);
|
.unwrap_or(name.clone());
|
||||||
new_blog.summary_html = SafeString::new(
|
let summary_html = SafeString::new(
|
||||||
&object
|
&object
|
||||||
.summary()
|
.summary()
|
||||||
.and_then(|summary| summary.to_as_string())
|
.and_then(|summary| summary.to_as_string())
|
||||||
@ -445,7 +431,6 @@ impl FromId<Connection> for Blog {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map(|m| m.id);
|
.map(|m| m.id);
|
||||||
new_blog.icon_id = icon_id;
|
|
||||||
|
|
||||||
let banner_id = object
|
let banner_id = object
|
||||||
.image()
|
.image()
|
||||||
@ -462,13 +447,12 @@ impl FromId<Connection> for Blog {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map(|m| m.id);
|
.map(|m| m.id);
|
||||||
new_blog.banner_id = banner_id;
|
|
||||||
|
|
||||||
new_blog.summary = acct.ext_two.source.content;
|
let summary = acct.ext_two.source.content;
|
||||||
|
|
||||||
let any_base = AnyBase::from_extended(object)?;
|
let any_base = AnyBase::from_extended(object)?;
|
||||||
let id = any_base.id().ok_or(Error::MissingApProperty)?;
|
let id = any_base.id().ok_or(Error::MissingApProperty)?;
|
||||||
new_blog.ap_url = id.to_string();
|
let ap_url = id.to_string();
|
||||||
|
|
||||||
let inst = id
|
let inst = id
|
||||||
.authority_components()
|
.authority_components()
|
||||||
@ -492,7 +476,29 @@ impl FromId<Connection> for Blog {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
new_blog.instance_id = instance.id;
|
let instance_id = instance.id;
|
||||||
|
let fqn = if instance.local {
|
||||||
|
Fqn::new_local(name)?
|
||||||
|
} else {
|
||||||
|
Fqn::new_remote(name, instance.public_domain)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_blog = NewBlog {
|
||||||
|
actor_id,
|
||||||
|
outbox_url,
|
||||||
|
inbox_url,
|
||||||
|
fqn,
|
||||||
|
public_key: acct.ext_one.public_key.public_key_pem.to_string(),
|
||||||
|
private_key: None,
|
||||||
|
theme: None,
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
ap_url,
|
||||||
|
summary_html,
|
||||||
|
icon_id,
|
||||||
|
banner_id,
|
||||||
|
instance_id,
|
||||||
|
};
|
||||||
|
|
||||||
Blog::insert(conn, new_blog)
|
Blog::insert(conn, new_blog)
|
||||||
}
|
}
|
||||||
@ -548,12 +554,19 @@ impl NewBlog {
|
|||||||
let (pub_key, priv_key) = sign::gen_keypair();
|
let (pub_key, priv_key) = sign::gen_keypair();
|
||||||
Ok(NewBlog {
|
Ok(NewBlog {
|
||||||
actor_id,
|
actor_id,
|
||||||
|
fqn: Fqn::make_local(&title)?,
|
||||||
title,
|
title,
|
||||||
summary,
|
summary,
|
||||||
instance_id,
|
instance_id,
|
||||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||||
..NewBlog::default()
|
outbox_url: Default::default(),
|
||||||
|
inbox_url: Default::default(),
|
||||||
|
ap_url: Default::default(),
|
||||||
|
summary_html: Default::default(),
|
||||||
|
icon_id: Default::default(),
|
||||||
|
banner_id: Default::default(),
|
||||||
|
theme: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -575,8 +588,8 @@ pub(crate) mod tests {
|
|||||||
let mut blog1 = Blog::insert(
|
let mut blog1 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"BlogName".to_owned(),
|
"Blog%20Name".to_owned(),
|
||||||
"Blog name".to_owned(),
|
"Blog Name".to_owned(),
|
||||||
"This is a small blog".to_owned(),
|
"This is a small blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
)
|
)
|
||||||
@ -586,7 +599,7 @@ pub(crate) mod tests {
|
|||||||
let blog2 = Blog::insert(
|
let blog2 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"MyBlog".to_owned(),
|
"My%20Blog".to_owned(),
|
||||||
"My blog".to_owned(),
|
"My blog".to_owned(),
|
||||||
"Welcome to my blog".to_owned(),
|
"Welcome to my blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
@ -597,7 +610,7 @@ pub(crate) mod tests {
|
|||||||
let blog3 = Blog::insert(
|
let blog3 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"WhyILikePlume".to_owned(),
|
"Why%20I%20Like%20Plume".to_owned(),
|
||||||
"Why I like Plume".to_owned(),
|
"Why I like Plume".to_owned(),
|
||||||
"In this blog I will explay you why I like Plume so much".to_owned(),
|
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
@ -692,7 +705,7 @@ pub(crate) mod tests {
|
|||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"Some%20Name".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
@ -719,7 +732,7 @@ pub(crate) mod tests {
|
|||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"Some%20Name".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
@ -820,7 +833,7 @@ pub(crate) mod tests {
|
|||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"Some%20Name".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
@ -840,10 +853,10 @@ pub(crate) mod tests {
|
|||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
fill_database(conn);
|
fill_database(conn);
|
||||||
|
|
||||||
let blog = Blog::insert(
|
let _ = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"Some%20Name".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
@ -852,7 +865,6 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(blog.fqn, "SomeName");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -878,7 +890,7 @@ pub(crate) mod tests {
|
|||||||
let b1 = Blog::insert(
|
let b1 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"SomeName".to_owned(),
|
"Some%20Name".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local().unwrap().id,
|
Instance::get_local().unwrap().id,
|
||||||
@ -978,6 +990,7 @@ pub(crate) mod tests {
|
|||||||
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();
|
||||||
|
eprintln!("{:#?}", &ap_repr);
|
||||||
let blog = Blog::from_activity(conn, ap_repr).unwrap();
|
let blog = Blog::from_activity(conn, ap_repr).unwrap();
|
||||||
|
|
||||||
assert_eq!(blog.actor_id, blogs[0].actor_id);
|
assert_eq!(blog.actor_id, blogs[0].actor_id);
|
||||||
@ -1011,19 +1024,19 @@ pub(crate) mod tests {
|
|||||||
"type": "Image",
|
"type": "Image",
|
||||||
"url": "https://plu.me/aaa.png"
|
"url": "https://plu.me/aaa.png"
|
||||||
},
|
},
|
||||||
"id": "https://plu.me/~/BlogName/",
|
"id": "https://plu.me/~/Blog%20Name/",
|
||||||
"image": {
|
"image": {
|
||||||
"attributedTo": "https://plu.me/@/admin/",
|
"attributedTo": "https://plu.me/@/admin/",
|
||||||
"type": "Image",
|
"type": "Image",
|
||||||
"url": "https://plu.me/bbb.png"
|
"url": "https://plu.me/bbb.png"
|
||||||
},
|
},
|
||||||
"inbox": "https://plu.me/~/BlogName/inbox",
|
"inbox": "https://plu.me/~/Blog%20Name/inbox",
|
||||||
"name": "Blog name",
|
"name": "Blog Name",
|
||||||
"outbox": "https://plu.me/~/BlogName/outbox",
|
"outbox": "https://plu.me/~/Blog%20Name/outbox",
|
||||||
"preferredUsername": "BlogName",
|
"preferredUsername": "BlogName",
|
||||||
"publicKey": {
|
"publicKey": {
|
||||||
"id": "https://plu.me/~/BlogName/#main-key",
|
"id": "https://plu.me/~/Blog%20Name/#main-key",
|
||||||
"owner": "https://plu.me/~/BlogName/",
|
"owner": "https://plu.me/~/Blog%20Name/",
|
||||||
"publicKeyPem": blog.public_key
|
"publicKeyPem": blog.public_key
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
@ -1051,8 +1064,8 @@ pub(crate) mod tests {
|
|||||||
let expected = json!({
|
let expected = json!({
|
||||||
"items": [],
|
"items": [],
|
||||||
"totalItems": 0,
|
"totalItems": 0,
|
||||||
"first": "https://plu.me/~/BlogName/outbox?page=1",
|
"first": "https://plu.me/~/Blog%20Name/outbox?page=1",
|
||||||
"last": "https://plu.me/~/BlogName/outbox?page=0",
|
"last": "https://plu.me/~/Blog%20Name/outbox?page=0",
|
||||||
"type": "OrderedCollection"
|
"type": "OrderedCollection"
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1071,8 +1084,8 @@ pub(crate) mod tests {
|
|||||||
let act = blog.outbox_collection_page(conn, (33, 36))?;
|
let act = blog.outbox_collection_page(conn, (33, 36))?;
|
||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
"next": "https://plu.me/~/BlogName/outbox?page=3",
|
"next": "https://plu.me/~/Blog%20Name/outbox?page=3",
|
||||||
"prev": "https://plu.me/~/BlogName/outbox?page=1",
|
"prev": "https://plu.me/~/Blog%20Name/outbox?page=1",
|
||||||
"items": [],
|
"items": [],
|
||||||
"type": "OrderedCollectionPage"
|
"type": "OrderedCollectionPage"
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comment_seers::{CommentSeers, NewCommentSeers},
|
comment_seers::{CommentSeers, NewCommentSeers},
|
||||||
|
db_conn::DbConn,
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
medias::Media,
|
medias::Media,
|
||||||
mentions::Mention,
|
mentions::Mention,
|
||||||
@ -73,7 +74,6 @@ impl Comment {
|
|||||||
});
|
});
|
||||||
get!(comments);
|
get!(comments);
|
||||||
list_by!(comments, list_by_post, post_id as i32);
|
list_by!(comments, list_by_post, post_id as i32);
|
||||||
list_by!(comments, list_by_author, author_id as i32);
|
|
||||||
find_by!(comments, find_by_ap_url, ap_url as &str);
|
find_by!(comments, find_by_ap_url, ap_url as &str);
|
||||||
|
|
||||||
pub fn get_author(&self, conn: &Connection) -> Result<User> {
|
pub fn get_author(&self, conn: &Connection) -> Result<User> {
|
||||||
@ -111,7 +111,7 @@ impl Comment {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
pub fn to_activity(&self, conn: &DbConn) -> Result<Note> {
|
||||||
let author = User::get(conn, self.author_id)?;
|
let author = User::get(conn, self.author_id)?;
|
||||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||||
self.content.get().as_ref(),
|
self.content.get().as_ref(),
|
||||||
@ -136,7 +136,7 @@ impl Comment {
|
|||||||
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
|
|id| Comment::get(conn, id).map(|comment| comment.ap_url.unwrap_or_default()),
|
||||||
)?);
|
)?);
|
||||||
note.set_published(
|
note.set_published(
|
||||||
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
|
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
|
||||||
.expect("OffsetDateTime"),
|
.expect("OffsetDateTime"),
|
||||||
);
|
);
|
||||||
note.set_attributed_to(author.into_id().parse::<IriString>()?);
|
note.set_attributed_to(author.into_id().parse::<IriString>()?);
|
||||||
@ -149,7 +149,7 @@ impl Comment {
|
|||||||
Ok(note)
|
Ok(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_activity(&self, conn: &Connection) -> 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)?;
|
||||||
@ -217,15 +217,15 @@ impl Comment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Comment {
|
impl FromId<DbConn> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = Note;
|
type Object = Note;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, 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
|
||||||
.in_reply_to()
|
.in_reply_to()
|
||||||
@ -354,21 +354,21 @@ impl FromId<Connection> for Comment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Create, &Connection> for Comment {
|
impl AsObject<User, Create, &DbConn> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result<Self> {
|
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self> {
|
||||||
// The actual creation takes place in the FromId impl
|
// The actual creation takes place in the FromId impl
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Delete, &Connection> for Comment {
|
impl AsObject<User, Delete, &DbConn> for Comment {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||||
if self.author_id != actor.id {
|
if self.author_id != actor.id {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
@ -387,8 +387,8 @@ impl AsObject<User, Delete, &Connection> for Comment {
|
|||||||
diesel::update(comments::table)
|
diesel::update(comments::table)
|
||||||
.filter(comments::in_response_to_id.eq(self.id))
|
.filter(comments::in_response_to_id.eq(self.id))
|
||||||
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
||||||
.execute(conn)?;
|
.execute(&**conn)?;
|
||||||
diesel::delete(&self).execute(conn)?;
|
diesel::delete(&self).execute(&**conn)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -423,7 +423,6 @@ impl CommentTree {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::blogs::Blog;
|
use crate::blogs::Blog;
|
||||||
use crate::db_conn::DbConn;
|
|
||||||
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, format_datetime};
|
||||||
@ -464,13 +463,13 @@ mod tests {
|
|||||||
assert_json_eq!(to_value(&act).unwrap(), json!({
|
assert_json_eq!(to_value(&act).unwrap(), json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
"cc": ["https://plu.me/@/admin/followers"],
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id),
|
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}/activity", original_comm.id),
|
||||||
"object": {
|
"object": {
|
||||||
"attributedTo": "https://plu.me/@/admin/",
|
"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>
|
"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),
|
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", original_comm.id),
|
||||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
"inReplyTo": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"published": format_datetime(&original_comm.creation_date),
|
"published": format_datetime(&original_comm.creation_date),
|
||||||
"summary": "My CW",
|
"summary": "My CW",
|
||||||
"tag": [
|
"tag": [
|
||||||
@ -506,12 +505,12 @@ mod tests {
|
|||||||
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
|
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
|
||||||
"actor": "https://plu.me/@/user/",
|
"actor": "https://plu.me/@/user/",
|
||||||
"cc": ["https://plu.me/@/user/followers"],
|
"cc": ["https://plu.me/@/user/followers"],
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id),
|
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}/activity", reply.id),
|
||||||
"object": {
|
"object": {
|
||||||
"attributedTo": "https://plu.me/@/user/",
|
"attributedTo": "https://plu.me/@/user/",
|
||||||
"content": "",
|
"content": "",
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id),
|
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", reply.id),
|
||||||
"inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id),
|
"inReplyTo": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", original_comm.id),
|
||||||
"published": format_datetime(&reply.creation_date),
|
"published": format_datetime(&reply.creation_date),
|
||||||
"summary": "",
|
"summary": "",
|
||||||
"tag": [],
|
"tag": [],
|
||||||
@ -555,8 +554,8 @@ mod tests {
|
|||||||
"attributedTo": "https://plu.me/@/admin/",
|
"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>
|
"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),
|
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", comment.id),
|
||||||
"inReplyTo": "https://plu.me/~/BlogName/testing",
|
"inReplyTo": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"published": format_datetime(&comment.creation_date),
|
"published": format_datetime(&comment.creation_date),
|
||||||
"summary": "My CW",
|
"summary": "My CW",
|
||||||
"tag": [
|
"tag": [
|
||||||
@ -585,9 +584,9 @@ mod tests {
|
|||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id),
|
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}#delete", comment.id),
|
||||||
"object": {
|
"object": {
|
||||||
"id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id),
|
"id": format!("https://plu.me/~/Blog%20Name/testing/comment/{}", comment.id),
|
||||||
"type": "Tombstone"
|
"type": "Tombstone"
|
||||||
},
|
},
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
@ -6,9 +6,6 @@ use rocket::Config as RocketConfig;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::env::{self, var};
|
use std::env::{self, var};
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
use s3::{Bucket, Region, creds::Credentials};
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
const DB_NAME: &str = "plume";
|
const DB_NAME: &str = "plume";
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -30,23 +27,13 @@ pub struct Config {
|
|||||||
pub mail: Option<MailConfig>,
|
pub mail: Option<MailConfig>,
|
||||||
pub ldap: Option<LdapConfig>,
|
pub ldap: Option<LdapConfig>,
|
||||||
pub proxy: Option<ProxyConfig>,
|
pub proxy: Option<ProxyConfig>,
|
||||||
pub s3: Option<S3Config>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
||||||
self.proxy.as_ref().map(|p| &p.proxy)
|
self.proxy.as_ref().map(|p| &p.proxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn string_to_bool(val: &str, name: &str) -> bool {
|
|
||||||
match val {
|
|
||||||
"1" | "true" | "TRUE" => true,
|
|
||||||
"0" | "false" | "FALSE" => false,
|
|
||||||
_ => panic!("Invalid configuration: {} is not boolean", name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum InvalidRocketConfig {
|
pub enum InvalidRocketConfig {
|
||||||
Env,
|
Env,
|
||||||
@ -54,7 +41,7 @@ pub enum InvalidRocketConfig {
|
|||||||
SecretKey,
|
SecretKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
pub fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
||||||
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
||||||
|
|
||||||
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
||||||
@ -293,7 +280,6 @@ pub struct LdapConfig {
|
|||||||
pub tls: bool,
|
pub tls: bool,
|
||||||
pub user_name_attr: String,
|
pub user_name_attr: String,
|
||||||
pub mail_attr: String,
|
pub mail_attr: String,
|
||||||
pub user: Option<(String, String)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ldap_config() -> Option<LdapConfig> {
|
fn get_ldap_config() -> Option<LdapConfig> {
|
||||||
@ -302,29 +288,23 @@ fn get_ldap_config() -> Option<LdapConfig> {
|
|||||||
match (addr, base_dn) {
|
match (addr, base_dn) {
|
||||||
(Some(addr), Some(base_dn)) => {
|
(Some(addr), Some(base_dn)) => {
|
||||||
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
||||||
let tls = string_to_bool(&tls, "LDAP_TLS");
|
let tls = match tls.as_ref() {
|
||||||
|
"1" | "true" | "TRUE" => true,
|
||||||
|
"0" | "false" | "FALSE" => false,
|
||||||
|
_ => panic!("Invalid LDAP configuration : tls"),
|
||||||
|
};
|
||||||
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
|
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
|
||||||
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
|
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
|
||||||
//2023-12-30
|
|
||||||
let user = var("LDAP_USER").ok();
|
|
||||||
let password = var("LDAP_PASSWORD").ok();
|
|
||||||
let user = match (user, password) {
|
|
||||||
(Some(user), Some(password)) => Some((user, password)),
|
|
||||||
(None, None) => None,
|
|
||||||
_ => panic!("Invalid LDAP configuration both or neither of LDAP_USER and LDAP_PASSWORD must be set")
|
|
||||||
};
|
|
||||||
//
|
|
||||||
Some(LdapConfig {
|
Some(LdapConfig {
|
||||||
addr,
|
addr,
|
||||||
base_dn,
|
base_dn,
|
||||||
tls,
|
tls,
|
||||||
user_name_attr,
|
user_name_attr,
|
||||||
mail_attr,
|
mail_attr,
|
||||||
user,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
_ => {
|
(_, _) => {
|
||||||
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
|
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,104 +349,6 @@ fn get_proxy_config() -> Option<ProxyConfig> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct S3Config {
|
|
||||||
pub bucket: String,
|
|
||||||
pub access_key_id: String,
|
|
||||||
pub access_key_secret: String,
|
|
||||||
|
|
||||||
// region? If not set, default to us-east-1
|
|
||||||
pub region: String,
|
|
||||||
// hostname for s3. If not set, default to $region.amazonaws.com
|
|
||||||
pub hostname: String,
|
|
||||||
// may be useful when using self hosted s3. Won't work with recent AWS buckets
|
|
||||||
pub path_style: bool,
|
|
||||||
// http or https
|
|
||||||
pub protocol: String,
|
|
||||||
|
|
||||||
// download directly from s3 to user, wihout going through Plume. Require public read on bucket
|
|
||||||
pub direct_download: bool,
|
|
||||||
// use this hostname for downloads, can be used with caching proxy in front of s3 (expected to
|
|
||||||
// be reachable through https)
|
|
||||||
pub alias: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl S3Config {
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
pub fn get_bucket(&self) -> Bucket {
|
|
||||||
let region = Region::Custom {
|
|
||||||
region: self.region.clone(),
|
|
||||||
endpoint: format!("{}://{}", self.protocol, self.hostname),
|
|
||||||
};
|
|
||||||
let credentials = Credentials {
|
|
||||||
access_key: Some(self.access_key_id.clone()),
|
|
||||||
secret_key: Some(self.access_key_secret.clone()),
|
|
||||||
security_token: None,
|
|
||||||
session_token: None,
|
|
||||||
expiration: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let bucket = Bucket::new(&self.bucket, region, credentials).unwrap();
|
|
||||||
if self.path_style {
|
|
||||||
bucket.with_path_style()
|
|
||||||
} else {
|
|
||||||
bucket
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_s3_config() -> Option<S3Config> {
|
|
||||||
let bucket = var("S3_BUCKET").ok();
|
|
||||||
let access_key_id = var("AWS_ACCESS_KEY_ID").ok();
|
|
||||||
let access_key_secret = var("AWS_SECRET_ACCESS_KEY").ok();
|
|
||||||
if bucket.is_none() && access_key_id.is_none() && access_key_secret.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "s3"))]
|
|
||||||
panic!("S3 support is not enabled in this build");
|
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
{
|
|
||||||
if bucket.is_none() || access_key_id.is_none() || access_key_secret.is_none() {
|
|
||||||
panic!("Invalid S3 configuration: some required values are set, but not others");
|
|
||||||
}
|
|
||||||
let bucket = bucket.unwrap();
|
|
||||||
let access_key_id = access_key_id.unwrap();
|
|
||||||
let access_key_secret = access_key_secret.unwrap();
|
|
||||||
|
|
||||||
let region = var("S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned());
|
|
||||||
let hostname = var("S3_HOSTNAME").unwrap_or_else(|_| format!("{}.amazonaws.com", region));
|
|
||||||
|
|
||||||
let protocol = var("S3_PROTOCOL").unwrap_or_else(|_| "https".to_owned());
|
|
||||||
if protocol != "http" && protocol != "https" {
|
|
||||||
panic!("Invalid S3 configuration: invalid protocol {}", protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_style = var("S3_PATH_STYLE").unwrap_or_else(|_| "false".to_owned());
|
|
||||||
let path_style = string_to_bool(&path_style, "S3_PATH_STYLE");
|
|
||||||
let direct_download = var("S3_DIRECT_DOWNLOAD").unwrap_or_else(|_| "false".to_owned());
|
|
||||||
let direct_download = string_to_bool(&direct_download, "S3_DIRECT_DOWNLOAD");
|
|
||||||
|
|
||||||
let alias = var("S3_ALIAS_HOST").ok();
|
|
||||||
|
|
||||||
if direct_download && protocol == "http" && alias.is_none() {
|
|
||||||
panic!("S3 direct download is disabled because bucket is accessed through plain HTTP. Use HTTPS or set an alias hostname (S3_ALIAS_HOST).");
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(S3Config {
|
|
||||||
bucket,
|
|
||||||
access_key_id,
|
|
||||||
access_key_secret,
|
|
||||||
region,
|
|
||||||
hostname,
|
|
||||||
protocol,
|
|
||||||
path_style,
|
|
||||||
direct_download,
|
|
||||||
alias,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref CONFIG: Config = Config {
|
pub static ref CONFIG: Config = Config {
|
||||||
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
||||||
@ -498,6 +380,5 @@ lazy_static! {
|
|||||||
mail: get_mail_config(),
|
mail: get_mail_config(),
|
||||||
ldap: get_ldap_config(),
|
ldap: get_ldap_config(),
|
||||||
proxy: get_proxy_config(),
|
proxy: get_proxy_config(),
|
||||||
s3: get_s3_config(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, instance::Instance, notifications::*, schema::follows, users::User, Connection, Error,
|
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
|
||||||
Result, CONFIG,
|
Connection, Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
|
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
|
||||||
@ -150,11 +150,11 @@ impl Follow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, FollowAct, &Connection> for User {
|
impl AsObject<User, FollowAct, &DbConn> for User {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Follow;
|
type Output = Follow;
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, 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 follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
|
||||||
@ -162,15 +162,15 @@ impl AsObject<User, FollowAct, &Connection> for User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Follow {
|
impl FromId<DbConn> for Follow {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = FollowAct;
|
type Object = FollowAct;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||||
Follow::find_by_ap_url(conn, id)
|
Follow::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, 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
|
follow
|
||||||
@ -202,18 +202,18 @@ impl FromId<Connection> for Follow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &Connection> for Follow {
|
impl AsObject<User, Undo, &DbConn> for Follow {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||||
let conn = conn;
|
let conn = conn;
|
||||||
if self.follower_id == actor.id {
|
if self.follower_id == actor.id {
|
||||||
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(¬if).execute(conn)?;
|
diesel::delete(¬if).execute(&**conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -232,9 +232,7 @@ impl IntoId for Follow {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{tests::db, users::tests as user_tests, users::tests::fill_database};
|
||||||
db_conn::DbConn, tests::db, users::tests as user_tests, users::tests::fill_database,
|
|
||||||
};
|
|
||||||
use assert_json_diff::assert_json_eq;
|
use assert_json_diff::assert_json_eq;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use serde_json::{json, to_value};
|
use serde_json::{json, to_value};
|
||||||
|
@ -2,11 +2,12 @@ use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Up
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
comments::Comment,
|
comments::Comment,
|
||||||
|
db_conn::DbConn,
|
||||||
follows, likes,
|
follows, likes,
|
||||||
posts::{Post, PostUpdate},
|
posts::{Post, PostUpdate},
|
||||||
reshares::Reshare,
|
reshares::Reshare,
|
||||||
users::User,
|
users::User,
|
||||||
Connection, Error, CONFIG,
|
Error, CONFIG,
|
||||||
};
|
};
|
||||||
use plume_common::activity_pub::inbox::Inbox;
|
use plume_common::activity_pub::inbox::Inbox;
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ impl_into_inbox_result! {
|
|||||||
Reshare => Reshared
|
Reshare => Reshared
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inbox(conn: &Connection, 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(conn, act)
|
||||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||||
@ -267,7 +268,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%20Name/testing",
|
||||||
"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",
|
||||||
|
@ -9,7 +9,7 @@ use crate::{
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use plume_common::utils::{iri_percent_encode_seg, md_to_html};
|
use plume_common::utils::md_to_html;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable)]
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
@ -173,8 +173,8 @@ impl Instance {
|
|||||||
"{instance}/{prefix}/{name}/{box_name}",
|
"{instance}/{prefix}/{name}/{box_name}",
|
||||||
instance = self.public_domain,
|
instance = self.public_domain,
|
||||||
prefix = prefix,
|
prefix = prefix,
|
||||||
name = iri_percent_encode_seg(name),
|
name = name,
|
||||||
box_name = iri_percent_encode_seg(box_name)
|
box_name = box_name
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,12 +17,18 @@ extern crate serde_json;
|
|||||||
extern crate tantivy;
|
extern crate tantivy;
|
||||||
|
|
||||||
use activitystreams::iri_string;
|
use activitystreams::iri_string;
|
||||||
|
use diesel::backend::Backend;
|
||||||
|
use diesel::sql_types::Text;
|
||||||
|
use diesel::types::{FromSql, ToSql};
|
||||||
|
use heck::ToUpperCamelCase;
|
||||||
pub use lettre;
|
pub use lettre;
|
||||||
pub use lettre::smtp;
|
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::PreferredUsernameError;
|
||||||
|
use plume_common::activity_pub::{inbox::InboxError, request, sign, PreferredUsername};
|
||||||
use posts::PostEvent;
|
use posts::PostEvent;
|
||||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||||
|
use std::{fmt, io::Write, string::ToString};
|
||||||
use users::UserEvent;
|
use users::UserEvent;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||||
@ -69,8 +75,6 @@ pub enum Error {
|
|||||||
Webfinger,
|
Webfinger,
|
||||||
Expired,
|
Expired,
|
||||||
UserAlreadyExists,
|
UserAlreadyExists,
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
S3(s3::error::S3Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bcrypt::BcryptError> for Error {
|
impl From<bcrypt::BcryptError> for Error {
|
||||||
@ -172,10 +176,10 @@ impl From<request::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
impl From<PreferredUsernameError> for Error {
|
||||||
impl From<s3::error::S3Error> for Error {
|
fn from(err: PreferredUsernameError) -> Error {
|
||||||
fn from(err: s3::error::S3Error) -> Error {
|
tracing::trace!("{:?}", err);
|
||||||
Error::S3(err)
|
Error::InvalidValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +314,7 @@ macro_rules! last {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
pub use config::CONFIG;
|
pub use config::{get_rocket_config, Config, SearchTokenizerConfig, CONFIG};
|
||||||
|
|
||||||
pub fn ap_url(url: &str) -> String {
|
pub fn ap_url(url: &str) -> String {
|
||||||
format!("https://{}", url)
|
format!("https://{}", url)
|
||||||
@ -343,10 +347,97 @@ impl SmtpNewWithAddr for smtp::SmtpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(AsExpression, PartialEq, Eq, Clone, FromSqlRow, Debug)]
|
||||||
|
#[sql_type = "Text"]
|
||||||
|
pub enum Fqn {
|
||||||
|
Local(PreferredUsername),
|
||||||
|
Remote(PreferredUsername, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fqn {
|
||||||
|
pub fn new_local(username: String) -> Result<Self> {
|
||||||
|
Ok(Self::Local(
|
||||||
|
PreferredUsername::new(username)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_remote(username: String, domain: String) -> Result<Self> {
|
||||||
|
Ok(Self::Remote(
|
||||||
|
PreferredUsername::new(username)?,
|
||||||
|
domain,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_local_string(base: &str) -> String {
|
||||||
|
base.to_upper_camel_case()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_ascii_alphanumeric())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_local(base: &str) -> Result<Self> {
|
||||||
|
Self::new_local(Self::make_local_string(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_remote(base: &str, domain: String) -> Result<Self> {
|
||||||
|
Self::new_remote(Self::make_local_string(base), domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Fqn> for String {
|
||||||
|
fn from(fqn: &Fqn) -> Self {
|
||||||
|
match fqn {
|
||||||
|
Fqn::Local(username) => username.to_string(),
|
||||||
|
Fqn::Remote(username, domain) => format!("{}@{}", username, domain),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Fqn {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
String::from(self).fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DB> ToSql<Text, DB> for Fqn
|
||||||
|
where
|
||||||
|
DB: diesel::backend::Backend,
|
||||||
|
{
|
||||||
|
fn to_sql<W: Write>(
|
||||||
|
&self,
|
||||||
|
out: &mut diesel::serialize::Output<W, DB>,
|
||||||
|
) -> diesel::serialize::Result {
|
||||||
|
let fqn = match self {
|
||||||
|
Self::Local(username) => username.to_string(),
|
||||||
|
Self::Remote(username, domain) => format!("{}@{}", username, domain),
|
||||||
|
};
|
||||||
|
ToSql::<Text, DB>::to_sql::<W>(&fqn, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DB> FromSql<Text, DB> for Fqn
|
||||||
|
where
|
||||||
|
DB: diesel::backend::Backend,
|
||||||
|
String: FromSql<Text, DB>,
|
||||||
|
{
|
||||||
|
/// We use PreferredUsername::new_unchecked() because, even if bytes is invalid as `preferredUsername`,
|
||||||
|
/// we need return some value.
|
||||||
|
fn from_sql(bytes: Option<&<DB as Backend>::RawValue>) -> diesel::deserialize::Result<Self> {
|
||||||
|
let value = <String as FromSql<Text, DB>>::from_sql(bytes)?;
|
||||||
|
Ok(match value.rsplit_once('@') {
|
||||||
|
None => Self::Local(unsafe { PreferredUsername::new_unchecked(value) }),
|
||||||
|
Some((username, domain)) => Self::Remote(
|
||||||
|
unsafe { PreferredUsername::new_unchecked(username.into()) },
|
||||||
|
domain.into(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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, Fqn};
|
||||||
use chrono::{naive::NaiveDateTime, Datelike, Timelike};
|
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;
|
||||||
@ -407,6 +498,20 @@ mod tests {
|
|||||||
dt.second()
|
dt.second()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fqn() {
|
||||||
|
assert_eq!(
|
||||||
|
Fqn::new_local("admin".to_string()).unwrap(),
|
||||||
|
Fqn::new_local("admin".to_string()).unwrap()
|
||||||
|
);
|
||||||
|
assert!(Fqn::new_local("admin".to_string()).is_ok());
|
||||||
|
let fqn = Fqn::new_local("admin".to_string()).unwrap();
|
||||||
|
assert_eq!("admin".to_string(), String::from(&fqn));
|
||||||
|
let fqn = Fqn::new_local("admin".to_string()).unwrap();
|
||||||
|
assert_eq!("admin".to_string(), ToString::to_string(&fqn));
|
||||||
|
assert_eq!("admin".to_string(), fqn.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
|
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
|
||||||
Connection, Error, Result, CONFIG,
|
users::User, Connection, Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
|
activity::{ActorAndObjectRef, Like as LikeAct, Undo},
|
||||||
@ -85,11 +85,11 @@ impl Like {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, LikeAct, &Connection> for Post {
|
impl AsObject<User, LikeAct, &DbConn> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Like;
|
type Output = Like;
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Like> {
|
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Like> {
|
||||||
let res = Like::insert(
|
let res = Like::insert(
|
||||||
conn,
|
conn,
|
||||||
NewLike {
|
NewLike {
|
||||||
@ -105,15 +105,15 @@ impl AsObject<User, LikeAct, &Connection> for Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Like {
|
impl FromId<DbConn> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = LikeAct;
|
type Object = LikeAct;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, act: LikeAct) -> Result<Self> {
|
fn from_activity(conn: &DbConn, act: LikeAct) -> Result<Self> {
|
||||||
let res = Like::insert(
|
let res = Like::insert(
|
||||||
conn,
|
conn,
|
||||||
NewLike {
|
NewLike {
|
||||||
@ -154,17 +154,17 @@ impl FromId<Connection> for Like {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &Connection> for Like {
|
impl AsObject<User, Undo, &DbConn> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||||
if actor.id == self.user_id {
|
if actor.id == self.user_id {
|
||||||
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(¬if).execute(conn)?;
|
diesel::delete(¬if).execute(&**conn)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@ -205,8 +205,8 @@ mod tests {
|
|||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
"cc": ["https://plu.me/@/admin/followers"],
|
||||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing",
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Like",
|
"type": "Like",
|
||||||
});
|
});
|
||||||
@ -229,12 +229,12 @@ mod tests {
|
|||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
"cc": ["https://plu.me/@/admin/followers"],
|
||||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete",
|
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing#delete",
|
||||||
"object": {
|
"object": {
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
"cc": ["https://plu.me/@/admin/followers"],
|
||||||
"id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/@/admin/like/https://plu.me/~/Blog%20Name/testing",
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Like",
|
"type": "Like",
|
||||||
},
|
},
|
||||||
|
@ -297,28 +297,6 @@ impl List {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
|
||||||
if let Some(user_id) = self.user_id {
|
|
||||||
diesel::delete(
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.eq(user_id))
|
|
||||||
.filter(lists::name.eq(&self.name)),
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(Error::from)
|
|
||||||
} else {
|
|
||||||
diesel::delete(
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.is_null())
|
|
||||||
.filter(lists::name.eq(&self.name)),
|
|
||||||
)
|
|
||||||
.execute(conn)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func! {set: set_users, User, add_users}
|
func! {set: set_users, User, add_users}
|
||||||
func! {set: set_blogs, Blog, add_blogs}
|
func! {set: set_blogs, Blog, add_blogs}
|
||||||
func! {set: set_words, Word, add_words}
|
func! {set: set_words, Word, add_words}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
|
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
|
||||||
Error, Result, CONFIG,
|
users::User, Connection, Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{object::Image, prelude::*};
|
use activitystreams::{object::Image, prelude::*};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
@ -16,9 +16,6 @@ use std::{
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
use crate::config::S3Config;
|
|
||||||
|
|
||||||
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
|
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
|
||||||
@ -108,7 +105,7 @@ impl Media {
|
|||||||
.file_path
|
.file_path
|
||||||
.rsplit_once('.')
|
.rsplit_once('.')
|
||||||
.map(|x| x.1)
|
.map(|x| x.1)
|
||||||
.unwrap_or("")
|
.expect("Media::category: extension error")
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
{
|
{
|
||||||
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
|
"png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image,
|
||||||
@ -154,99 +151,26 @@ impl Media {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns full file path for medias stored in the local media directory.
|
|
||||||
pub fn local_path(&self) -> Option<PathBuf> {
|
|
||||||
if self.file_path.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if CONFIG.s3.is_some() {
|
|
||||||
#[cfg(feature="s3")]
|
|
||||||
unreachable!("Called Media::local_path() but media are stored on S3");
|
|
||||||
#[cfg(not(feature="s3"))]
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative_path = self
|
|
||||||
.file_path
|
|
||||||
.trim_start_matches(&CONFIG.media_directory)
|
|
||||||
.trim_start_matches(path::MAIN_SEPARATOR)
|
|
||||||
.trim_start_matches("static/media/");
|
|
||||||
|
|
||||||
Some(Path::new(&CONFIG.media_directory).join(relative_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the relative URL to access this file, which is also the key at which
|
|
||||||
/// it is stored in the S3 bucket if we are using S3 storage.
|
|
||||||
/// Does not start with a '/', it is of the form "static/media/<...>"
|
|
||||||
pub fn relative_url(&self) -> Option<String> {
|
|
||||||
if self.file_path.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative_path = self
|
|
||||||
.file_path
|
|
||||||
.trim_start_matches(&CONFIG.media_directory)
|
|
||||||
.replace(path::MAIN_SEPARATOR, "/");
|
|
||||||
|
|
||||||
let relative_path = relative_path
|
|
||||||
.trim_start_matches('/')
|
|
||||||
.trim_start_matches("static/media/");
|
|
||||||
|
|
||||||
Some(format!("static/media/{}", relative_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a public URL through which this media file can be accessed
|
|
||||||
pub fn url(&self) -> Result<String> {
|
pub fn url(&self) -> Result<String> {
|
||||||
if self.is_remote {
|
if self.is_remote {
|
||||||
Ok(self.remote_url.clone().unwrap_or_default())
|
Ok(self.remote_url.clone().unwrap_or_default())
|
||||||
} else {
|
} else {
|
||||||
let relative_url = self.relative_url().unwrap_or_default();
|
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
|
||||||
|
&CONFIG.media_directory,
|
||||||
#[cfg(feature="s3")]
|
"static/media",
|
||||||
if CONFIG.s3.as_ref().map(|x| x.direct_download).unwrap_or(false) {
|
1,
|
||||||
let s3_url = match CONFIG.s3.as_ref().unwrap() {
|
); // "static/media" from plume::routs::plume_media_files()
|
||||||
S3Config { alias: Some(alias), .. } => {
|
|
||||||
format!("https://{}/{}", alias, relative_url)
|
|
||||||
}
|
|
||||||
S3Config { path_style: true, hostname, bucket, .. } => {
|
|
||||||
format!("https://{}/{}/{}",
|
|
||||||
hostname,
|
|
||||||
bucket,
|
|
||||||
relative_url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
S3Config { path_style: false, hostname, bucket, .. } => {
|
|
||||||
format!("https://{}.{}/{}",
|
|
||||||
bucket,
|
|
||||||
hostname,
|
|
||||||
relative_url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return Ok(s3_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ap_url(&format!(
|
Ok(ap_url(&format!(
|
||||||
"{}/{}",
|
"{}/{}",
|
||||||
Instance::get_local()?.public_domain,
|
Instance::get_local()?.public_domain,
|
||||||
relative_url
|
&file_path
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
||||||
if !self.is_remote {
|
if !self.is_remote {
|
||||||
if CONFIG.s3.is_some() {
|
fs::remove_file(self.file_path.as_str())?;
|
||||||
#[cfg(not(feature="s3"))]
|
|
||||||
unreachable!();
|
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
CONFIG.s3.as_ref().unwrap().get_bucket()
|
|
||||||
.delete_object_blocking(&self.relative_url().ok_or(Error::NotFound)?)?;
|
|
||||||
} else {
|
|
||||||
fs::remove_file(self.local_path().ok_or(Error::NotFound)?)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
diesel::delete(self)
|
diesel::delete(self)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
@ -282,65 +206,27 @@ impl Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with save_remote?
|
// TODO: merge with save_remote?
|
||||||
pub fn from_activity(conn: &Connection, image: &Image) -> Result<Media> {
|
pub fn from_activity(conn: &DbConn, image: &Image) -> Result<Media> {
|
||||||
let remote_url = image
|
let remote_url = image
|
||||||
.url()
|
.url()
|
||||||
.and_then(|url| url.to_as_uri())
|
.and_then(|url| url.to_as_uri())
|
||||||
.ok_or(Error::MissingApProperty)?;
|
.ok_or(Error::MissingApProperty)?;
|
||||||
|
let path = determine_mirror_file_path(&remote_url);
|
||||||
|
let parent = path.parent().ok_or(Error::InvalidValue)?;
|
||||||
|
if !parent.is_dir() {
|
||||||
|
DirBuilder::new().recursive(true).create(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
let file_path = if CONFIG.s3.is_some() {
|
let mut dest = fs::File::create(path.clone())?;
|
||||||
#[cfg(not(feature="s3"))]
|
// TODO: conditional GET
|
||||||
unreachable!();
|
request::get(
|
||||||
|
remote_url.as_str(),
|
||||||
|
User::get_sender(),
|
||||||
|
CONFIG.proxy().cloned(),
|
||||||
|
)?
|
||||||
|
.copy_to(&mut dest)?;
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?)
|
||||||
{
|
|
||||||
use rocket::http::ContentType;
|
|
||||||
|
|
||||||
let dest = determine_mirror_s3_path(&remote_url);
|
|
||||||
|
|
||||||
let media = request::get(
|
|
||||||
remote_url.as_str(),
|
|
||||||
User::get_sender(),
|
|
||||||
CONFIG.proxy().cloned(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let content_type = media
|
|
||||||
.headers()
|
|
||||||
.get(reqwest::header::CONTENT_TYPE)
|
|
||||||
.and_then(|x| x.to_str().ok())
|
|
||||||
.and_then(ContentType::parse_flexible)
|
|
||||||
.unwrap_or(ContentType::Binary);
|
|
||||||
|
|
||||||
let bytes = media.bytes()?;
|
|
||||||
|
|
||||||
let bucket = CONFIG.s3.as_ref().unwrap().get_bucket();
|
|
||||||
bucket.put_object_with_content_type_blocking(
|
|
||||||
&dest,
|
|
||||||
&bytes,
|
|
||||||
&content_type.to_string()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
dest
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let path = determine_mirror_file_path(&remote_url);
|
|
||||||
let parent = path.parent().ok_or(Error::InvalidValue)?;
|
|
||||||
if !parent.is_dir() {
|
|
||||||
DirBuilder::new().recursive(true).create(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dest = fs::File::create(path.clone())?;
|
|
||||||
// TODO: conditional GET
|
|
||||||
request::get(
|
|
||||||
remote_url.as_str(),
|
|
||||||
User::get_sender(),
|
|
||||||
CONFIG.proxy().cloned(),
|
|
||||||
)?
|
|
||||||
.copy_to(&mut dest)?;
|
|
||||||
path.to_str().ok_or(Error::InvalidValue)?.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Media::find_by_file_path(conn, &file_path)
|
|
||||||
.and_then(|mut media| {
|
.and_then(|mut media| {
|
||||||
let mut updated = false;
|
let mut updated = false;
|
||||||
|
|
||||||
@ -372,7 +258,7 @@ impl Media {
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if updated {
|
if updated {
|
||||||
diesel::update(&media).set(&media).execute(conn)?;
|
diesel::update(&media).set(&media).execute(&**conn)?;
|
||||||
}
|
}
|
||||||
Ok(media)
|
Ok(media)
|
||||||
})
|
})
|
||||||
@ -381,7 +267,7 @@ impl Media {
|
|||||||
Media::insert(
|
Media::insert(
|
||||||
conn,
|
conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path,
|
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
|
||||||
alt_text: image
|
alt_text: image
|
||||||
.content()
|
.content()
|
||||||
.and_then(|content| content.to_as_string())
|
.and_then(|content| content.to_as_string())
|
||||||
@ -421,10 +307,12 @@ impl Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn determine_mirror_file_path(url: &str) -> PathBuf {
|
fn determine_mirror_file_path(url: &str) -> PathBuf {
|
||||||
let mut file_path = Path::new(&CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
|
||||||
|
Url::parse(url)
|
||||||
match Url::parse(url) {
|
.map(|url| {
|
||||||
Ok(url) if url.has_host() => {
|
if !url.has_host() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
file_path.push(url.host_str().unwrap());
|
file_path.push(url.host_str().unwrap());
|
||||||
for segment in url.path_segments().expect("FIXME") {
|
for segment in url.path_segments().expect("FIXME") {
|
||||||
file_path.push(segment);
|
file_path.push(segment);
|
||||||
@ -432,54 +320,19 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
|
|||||||
// TODO: handle query
|
// TODO: handle query
|
||||||
// HINT: Use characters which must be percent-encoded in path as separator between path and query
|
// HINT: Use characters which must be percent-encoded in path as separator between path and query
|
||||||
// HINT: handle extension
|
// HINT: handle extension
|
||||||
}
|
})
|
||||||
other => {
|
.unwrap_or_else(|err| {
|
||||||
if let Err(err) = other {
|
warn!("Failed to parse url: {} {}", &url, err);
|
||||||
warn!("Failed to parse url: {} {}", &url, err);
|
|
||||||
} else {
|
|
||||||
warn!("Error without a host: {}", &url);
|
|
||||||
}
|
|
||||||
let ext = url
|
let ext = url
|
||||||
.rsplit('.')
|
.rsplit('.')
|
||||||
.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(), ext));
|
||||||
}
|
});
|
||||||
}
|
|
||||||
file_path
|
file_path
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature="s3")]
|
|
||||||
fn determine_mirror_s3_path(url: &str) -> String {
|
|
||||||
match Url::parse(url) {
|
|
||||||
Ok(url) if url.has_host() => {
|
|
||||||
format!("static/media/{}/{}/{}",
|
|
||||||
REMOTE_MEDIA_DIRECTORY,
|
|
||||||
url.host_str().unwrap(),
|
|
||||||
url.path().trim_start_matches('/'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
if let Err(err) = other {
|
|
||||||
warn!("Failed to parse url: {} {}", &url, err);
|
|
||||||
} else {
|
|
||||||
warn!("Error without a host: {}", &url);
|
|
||||||
}
|
|
||||||
let ext = url
|
|
||||||
.rsplit('.')
|
|
||||||
.next()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.unwrap_or_else(|| String::from("png"));
|
|
||||||
format!("static/media/{}/{}.{}",
|
|
||||||
REMOTE_MEDIA_DIRECTORY,
|
|
||||||
GUID::rand(),
|
|
||||||
ext,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
|
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
|
||||||
Error, Result,
|
users::User, Connection, Error, Result,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
base::BaseExt,
|
base::BaseExt,
|
||||||
@ -60,7 +60,7 @@ impl Mention {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_activity(conn: &Connection, 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::new();
|
||||||
mention.set_href(user.ap_url.parse::<IriString>()?);
|
mention.set_href(user.ap_url.parse::<IriString>()?);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
|
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
|
||||||
safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, Connection, Error,
|
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
|
||||||
PostEvent::*, Result, CONFIG, POST_CHAN,
|
Connection, Error, Fqn, PostEvent::*, Result, CONFIG, POST_CHAN,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{Create, Delete, Update},
|
activity::{Create, Delete, Update},
|
||||||
@ -28,7 +28,7 @@ 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};
|
||||||
|
|
||||||
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, Fqn>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
|
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
@ -255,7 +255,7 @@ impl Post {
|
|||||||
ap_url(&format!(
|
ap_url(&format!(
|
||||||
"{}/~/{}/{}/",
|
"{}/~/{}/{}/",
|
||||||
CONFIG.base_url,
|
CONFIG.base_url,
|
||||||
iri_percent_encode_seg(&blog.fqn),
|
&blog.fqn,
|
||||||
iri_percent_encode_seg(slug)
|
iri_percent_encode_seg(slug)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -298,9 +298,9 @@ impl Post {
|
|||||||
/// This caches query result. The best way to cache query result is holding it in `Post`s field
|
/// This caches query result. The best way to cache query result is holding it in `Post`s field
|
||||||
/// but Diesel doesn't allow it currently.
|
/// but Diesel doesn't allow it currently.
|
||||||
/// If sometime Diesel allow it, this method should be removed.
|
/// If sometime Diesel allow it, this method should be removed.
|
||||||
pub fn get_blog_fqn(&self, conn: &Connection) -> String {
|
pub fn get_blog_fqn(&self, conn: &Connection) -> Fqn {
|
||||||
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
|
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
|
||||||
return blog_fqn.to_string();
|
return blog_fqn.to_owned();
|
||||||
}
|
}
|
||||||
let blog_fqn = self.get_blog(conn).unwrap().fqn;
|
let blog_fqn = self.get_blog(conn).unwrap().fqn;
|
||||||
BLOG_FQN_CACHE
|
BLOG_FQN_CACHE
|
||||||
@ -373,7 +373,7 @@ impl Post {
|
|||||||
}))?;
|
}))?;
|
||||||
article.set_source(source);
|
article.set_source(source);
|
||||||
article.set_published(
|
article.set_published(
|
||||||
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos_opt().unwrap().into())
|
OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into())
|
||||||
.expect("OffsetDateTime"),
|
.expect("OffsetDateTime"),
|
||||||
);
|
);
|
||||||
article.set_summary(&*self.subtitle);
|
article.set_summary(&*self.subtitle);
|
||||||
@ -615,15 +615,15 @@ impl Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Post {
|
impl FromId<DbConn> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = LicensedArticle;
|
type Object = LicensedArticle;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, article: LicensedArticle) -> Result<Self> {
|
fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result<Self> {
|
||||||
let license = article.ext_one.license.unwrap_or_default();
|
let license = article.ext_one.license.unwrap_or_default();
|
||||||
let article = article.inner;
|
let article = article.inner;
|
||||||
|
|
||||||
@ -821,21 +821,21 @@ impl FromId<Connection> for Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Create, &Connection> for Post {
|
impl AsObject<User, Create, &DbConn> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn activity(self, _conn: &Connection, _actor: User, _id: &str) -> Result<Self::Output> {
|
fn activity(self, _conn: &DbConn, _actor: User, _id: &str) -> Result<Self::Output> {
|
||||||
// TODO: check that _actor is actually one of the author?
|
// TODO: check that _actor is actually one of the author?
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Delete, &Connection> for Post {
|
impl AsObject<User, Delete, &DbConn> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<Self::Output> {
|
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<Self::Output> {
|
||||||
let can_delete = self
|
let can_delete = self
|
||||||
.get_authors(conn)?
|
.get_authors(conn)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -859,16 +859,16 @@ pub struct PostUpdate {
|
|||||||
pub tags: Option<serde_json::Value>,
|
pub tags: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for PostUpdate {
|
impl FromId<DbConn> for PostUpdate {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = LicensedArticle;
|
type Object = LicensedArticle;
|
||||||
|
|
||||||
fn from_db(_: &Connection, _: &str) -> Result<Self> {
|
fn from_db(_: &DbConn, _: &str) -> Result<Self> {
|
||||||
// Always fail because we always want to deserialize the AP object
|
// Always fail because we always want to deserialize the AP object
|
||||||
Err(Error::NotFound)
|
Err(Error::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, updated: Self::Object) -> Result<Self> {
|
fn from_activity(conn: &DbConn, updated: Self::Object) -> Result<Self> {
|
||||||
let mut post_update = PostUpdate {
|
let mut post_update = PostUpdate {
|
||||||
ap_url: updated
|
ap_url: updated
|
||||||
.ap_object_ref()
|
.ap_object_ref()
|
||||||
@ -923,11 +923,11 @@ impl FromId<Connection> for PostUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Update, &Connection> for PostUpdate {
|
impl AsObject<User, Update, &DbConn> for PostUpdate {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||||
let mut post =
|
let mut post =
|
||||||
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
||||||
|
|
||||||
@ -1027,7 +1027,6 @@ impl From<PostEvent> for Arc<Post> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db_conn::DbConn;
|
|
||||||
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
use crate::inbox::{inbox, tests::fill_database, InboxResult};
|
||||||
use crate::mentions::{Mention, NewMention};
|
use crate::mentions::{Mention, NewMention};
|
||||||
use crate::safe_string::SafeString;
|
use crate::safe_string::SafeString;
|
||||||
@ -1110,10 +1109,10 @@ mod tests {
|
|||||||
let act = post.to_activity(&conn)?;
|
let act = post.to_activity(&conn)?;
|
||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
|
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
|
||||||
"cc": [],
|
"cc": [],
|
||||||
"content": "Hello",
|
"content": "Hello",
|
||||||
"id": "https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"name": "Testing",
|
"name": "Testing",
|
||||||
"published": format_datetime(&post.creation_date),
|
"published": format_datetime(&post.creation_date),
|
||||||
@ -1131,7 +1130,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Article",
|
"type": "Article",
|
||||||
"url": "https://plu.me/~/BlogName/testing"
|
"url": "https://plu.me/~/Blog%20Name/testing"
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_json_eq!(to_value(act)?, expected);
|
assert_json_eq!(to_value(act)?, expected);
|
||||||
@ -1150,12 +1149,12 @@ mod tests {
|
|||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": [],
|
"cc": [],
|
||||||
"id": "https://plu.me/~/BlogName/testing/activity",
|
"id": "https://plu.me/~/Blog%20Name/testing/activity",
|
||||||
"object": {
|
"object": {
|
||||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
|
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
|
||||||
"cc": [],
|
"cc": [],
|
||||||
"content": "Hello",
|
"content": "Hello",
|
||||||
"id": "https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"name": "Testing",
|
"name": "Testing",
|
||||||
"published": format_datetime(&post.creation_date),
|
"published": format_datetime(&post.creation_date),
|
||||||
@ -1173,7 +1172,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Article",
|
"type": "Article",
|
||||||
"url": "https://plu.me/~/BlogName/testing"
|
"url": "https://plu.me/~/Blog%20Name/testing"
|
||||||
},
|
},
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Create"
|
"type": "Create"
|
||||||
@ -1195,12 +1194,12 @@ mod tests {
|
|||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": [],
|
"cc": [],
|
||||||
"id": "https://plu.me/~/BlogName/testing/update-",
|
"id": "https://plu.me/~/Blog%20Name/testing/update-",
|
||||||
"object": {
|
"object": {
|
||||||
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"],
|
"attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/Blog%20Name/"],
|
||||||
"cc": [],
|
"cc": [],
|
||||||
"content": "Hello",
|
"content": "Hello",
|
||||||
"id": "https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"name": "Testing",
|
"name": "Testing",
|
||||||
"published": format_datetime(&post.creation_date),
|
"published": format_datetime(&post.creation_date),
|
||||||
@ -1218,7 +1217,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Article",
|
"type": "Article",
|
||||||
"url": "https://plu.me/~/BlogName/testing"
|
"url": "https://plu.me/~/Blog%20Name/testing"
|
||||||
},
|
},
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Update"
|
"type": "Update"
|
||||||
@ -1227,10 +1226,10 @@ mod tests {
|
|||||||
|
|
||||||
let id = actual["id"].to_string();
|
let id = actual["id"].to_string();
|
||||||
let (id_pre, id_post) = id.rsplit_once('-').unwrap();
|
let (id_pre, id_post) = id.rsplit_once('-').unwrap();
|
||||||
assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing");
|
assert_eq!(post.ap_url, "https://plu.me/~/Blog%20Name/testing");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
id_pre,
|
id_pre,
|
||||||
to_value("\"https://plu.me/~/BlogName/testing/update")
|
to_value("\"https://plu.me/~/Blog%20Name/testing/update")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -1260,9 +1259,9 @@ mod tests {
|
|||||||
|
|
||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"id": "https://plu.me/~/BlogName/testing#delete",
|
"id": "https://plu.me/~/Blog%20Name/testing#delete",
|
||||||
"object": {
|
"object": {
|
||||||
"id": "https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"type": "Tombstone"
|
"type": "Tombstone"
|
||||||
},
|
},
|
||||||
"to": [
|
"to": [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
instance::Instance, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
|
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
|
||||||
Connection, Error, Result, CONFIG,
|
timeline::*, users::User, Connection, Error, Result, CONFIG,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{ActorAndObjectRef, Announce, Undo},
|
activity::{ActorAndObjectRef, Announce, Undo},
|
||||||
@ -113,11 +113,11 @@ impl Reshare {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Announce, &Connection> for Post {
|
impl AsObject<User, Announce, &DbConn> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = Reshare;
|
type Output = Reshare;
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, id: &str) -> Result<Reshare> {
|
fn activity(self, conn: &DbConn, actor: User, id: &str) -> Result<Reshare> {
|
||||||
let conn = conn;
|
let conn = conn;
|
||||||
let reshare = Reshare::insert(
|
let reshare = Reshare::insert(
|
||||||
conn,
|
conn,
|
||||||
@ -134,15 +134,15 @@ impl AsObject<User, Announce, &Connection> for Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromId<Connection> for Reshare {
|
impl FromId<DbConn> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = Announce;
|
type Object = Announce;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, id: &str) -> Result<Self> {
|
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||||
Reshare::find_by_ap_url(conn, id)
|
Reshare::find_by_ap_url(conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, act: Announce) -> Result<Self> {
|
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
|
||||||
let res = Reshare::insert(
|
let res = Reshare::insert(
|
||||||
conn,
|
conn,
|
||||||
NewReshare {
|
NewReshare {
|
||||||
@ -183,17 +183,17 @@ impl FromId<Connection> for Reshare {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Undo, &Connection> for Reshare {
|
impl AsObject<User, Undo, &DbConn> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||||
if actor.id == self.user_id {
|
if actor.id == self.user_id {
|
||||||
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(¬if).execute(conn)?;
|
diesel::delete(¬if).execute(&**conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -235,8 +235,8 @@ mod test {
|
|||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
"cc": ["https://plu.me/@/admin/followers"],
|
||||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing",
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Announce",
|
"type": "Announce",
|
||||||
});
|
});
|
||||||
@ -259,12 +259,12 @@ mod test {
|
|||||||
let expected = json!({
|
let expected = json!({
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
"cc": ["https://plu.me/@/admin/followers"],
|
||||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete",
|
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing#delete",
|
||||||
"object": {
|
"object": {
|
||||||
"actor": "https://plu.me/@/admin/",
|
"actor": "https://plu.me/@/admin/",
|
||||||
"cc": ["https://plu.me/@/admin/followers"],
|
"cc": ["https://plu.me/@/admin/followers"],
|
||||||
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing",
|
"id": "https://plu.me/@/admin/reshare/https://plu.me/~/Blog%20Name/testing",
|
||||||
"object": "https://plu.me/~/BlogName/testing",
|
"object": "https://plu.me/~/Blog%20Name/testing",
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
"type": "Announce"
|
"type": "Announce"
|
||||||
},
|
},
|
||||||
|
@ -51,6 +51,14 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment_seers (id) {
|
||||||
|
id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
comments (id) {
|
comments (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@ -66,14 +74,6 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
|
||||||
comment_seers (id) {
|
|
||||||
id -> Int4,
|
|
||||||
comment_id -> Int4,
|
|
||||||
user_id -> Int4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
email_blocklist (id) {
|
email_blocklist (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@ -314,8 +314,8 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
apps,
|
apps,
|
||||||
blog_authors,
|
blog_authors,
|
||||||
blogs,
|
blogs,
|
||||||
comments,
|
|
||||||
comment_seers,
|
comment_seers,
|
||||||
|
comments,
|
||||||
email_blocklist,
|
email_blocklist,
|
||||||
email_signups,
|
email_signups,
|
||||||
follows,
|
follows,
|
||||||
|
@ -77,6 +77,7 @@ impl ActorFactoryArgs<(Arc<Searcher>, DbPool)> for SearchActor {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::diesel::Connection;
|
use crate::diesel::Connection;
|
||||||
|
use crate::Fqn;
|
||||||
use crate::{
|
use crate::{
|
||||||
blog_authors::{BlogAuthor, NewBlogAuthor},
|
blog_authors::{BlogAuthor, NewBlogAuthor},
|
||||||
blogs::{Blog, NewBlog},
|
blogs::{Blog, NewBlog},
|
||||||
@ -190,13 +191,22 @@ mod tests {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let title = random_hex();
|
||||||
let blog = NewBlog {
|
let blog = NewBlog {
|
||||||
instance_id: instance.id,
|
instance_id: instance.id,
|
||||||
actor_id: random_hex(),
|
actor_id: random_hex(),
|
||||||
ap_url: random_hex(),
|
ap_url: random_hex(),
|
||||||
inbox_url: random_hex(),
|
inbox_url: random_hex(),
|
||||||
outbox_url: random_hex(),
|
outbox_url: random_hex(),
|
||||||
..Default::default()
|
fqn: Fqn::make_local(&title).unwrap(),
|
||||||
|
title,
|
||||||
|
summary: Default::default(),
|
||||||
|
summary_html: Default::default(),
|
||||||
|
private_key: Default::default(),
|
||||||
|
public_key: Default::default(),
|
||||||
|
icon_id: Default::default(),
|
||||||
|
banner_id: Default::default(),
|
||||||
|
theme: Default::default(),
|
||||||
};
|
};
|
||||||
let blog = Blog::insert(conn, blog).unwrap();
|
let blog = Blog::insert(conn, blog).unwrap();
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
db_conn::DbConn,
|
||||||
lists::List,
|
lists::List,
|
||||||
posts::Post,
|
posts::Post,
|
||||||
schema::{posts, timeline, timeline_definition},
|
schema::{posts, timeline, timeline_definition},
|
||||||
@ -11,7 +12,7 @@ use std::ops::Deref;
|
|||||||
pub(crate) mod query;
|
pub(crate) mod query;
|
||||||
|
|
||||||
pub use self::query::Kind;
|
pub use self::query::Kind;
|
||||||
pub use self::query::{QueryError, TimelineQuery};
|
use self::query::{QueryError, TimelineQuery};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, AsChangeset)]
|
#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, AsChangeset)]
|
||||||
#[table_name = "timeline_definition"]
|
#[table_name = "timeline_definition"]
|
||||||
@ -219,10 +220,9 @@ impl Timeline {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_to_all_timelines(conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<()> {
|
pub fn add_to_all_timelines(conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<()> {
|
||||||
let timelines = timeline_definition::table
|
let timelines = timeline_definition::table
|
||||||
//.load::<Self>(conn.deref())
|
.load::<Self>(conn.deref())
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)?;
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
for t in timelines {
|
for t in timelines {
|
||||||
@ -246,26 +246,7 @@ impl Timeline {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_post(&self, conn: &Connection, post: &Post) -> Result<bool> {
|
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
||||||
if self.includes_post(conn, post)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
diesel::delete(
|
|
||||||
timeline::table
|
|
||||||
.filter(timeline::timeline_id.eq(self.id))
|
|
||||||
.filter(timeline::post_id.eq(post.id)),
|
|
||||||
)
|
|
||||||
.execute(conn)?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_all_posts(&self, conn: &Connection) -> Result<u64> {
|
|
||||||
let count = diesel::delete(timeline::table.filter(timeline::timeline_id.eq(self.id)))
|
|
||||||
.execute(conn)?;
|
|
||||||
Ok(count as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(&self, conn: &Connection, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
|
||||||
let query = TimelineQuery::parse(&self.query)?;
|
let query = TimelineQuery::parse(&self.query)?;
|
||||||
query.matches(conn, self, post, kind)
|
query.matches(conn, self, post, kind)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
blogs::Blog,
|
blogs::Blog,
|
||||||
|
db_conn::DbConn,
|
||||||
lists::{self, ListType},
|
lists::{self, ListType},
|
||||||
posts::Post,
|
posts::Post,
|
||||||
tags::Tag,
|
tags::Tag,
|
||||||
timeline::Timeline,
|
timeline::Timeline,
|
||||||
users::User,
|
users::User,
|
||||||
Connection, Result,
|
Result,
|
||||||
};
|
};
|
||||||
use plume_common::activity_pub::inbox::AsActor;
|
use plume_common::activity_pub::inbox::AsActor;
|
||||||
use whatlang::{self, Lang};
|
use whatlang::{self, Lang};
|
||||||
@ -154,7 +155,7 @@ enum TQ<'a> {
|
|||||||
impl<'a> TQ<'a> {
|
impl<'a> TQ<'a> {
|
||||||
fn matches(
|
fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &DbConn,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
@ -199,7 +200,7 @@ enum Arg<'a> {
|
|||||||
impl<'a> Arg<'a> {
|
impl<'a> Arg<'a> {
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &DbConn,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
@ -224,7 +225,7 @@ enum WithList {
|
|||||||
impl WithList {
|
impl WithList {
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &DbConn,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
list: &List<'_>,
|
list: &List<'_>,
|
||||||
@ -360,7 +361,7 @@ enum Bool {
|
|||||||
impl Bool {
|
impl Bool {
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &DbConn,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
@ -653,7 +654,7 @@ impl<'a> TimelineQuery<'a> {
|
|||||||
|
|
||||||
pub fn matches(
|
pub fn matches(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &DbConn,
|
||||||
timeline: &Timeline,
|
timeline: &Timeline,
|
||||||
post: &Post,
|
post: &Post,
|
||||||
kind: Kind<'_>,
|
kind: Kind<'_>,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, comments::Comment, db_conn::DbConn,
|
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
|
||||||
follows::Follow, instance::*, medias::Media, notifications::Notification,
|
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
|
||||||
post_authors::PostAuthor, posts::Post, safe_string::SafeString, schema::users,
|
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
|
||||||
timeline::Timeline, Connection, Error, Result, UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
|
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::Delete,
|
activity::Delete,
|
||||||
@ -15,10 +15,7 @@ use activitystreams::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use diesel::{
|
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
||||||
self, BelongingToDsl, BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl,
|
|
||||||
RunQueryDsl, TextExpressionMethods,
|
|
||||||
};
|
|
||||||
use ldap3::{LdapConn, Scope, SearchEntry};
|
use ldap3::{LdapConn, Scope, SearchEntry};
|
||||||
use openssl::{
|
use openssl::{
|
||||||
hash::MessageDigest,
|
hash::MessageDigest,
|
||||||
@ -168,14 +165,6 @@ impl User {
|
|||||||
notif.delete(conn)?
|
notif.delete(conn)?
|
||||||
}
|
}
|
||||||
|
|
||||||
for comment in Comment::list_by_author(conn, self.id)? {
|
|
||||||
let delete_activity = comment.build_delete(conn)?;
|
|
||||||
crate::inbox::inbox(
|
|
||||||
conn,
|
|
||||||
serde_json::to_value(&delete_activity).map_err(Error::from)?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::delete(self)
|
diesel::delete(self)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@ -197,16 +186,15 @@ impl User {
|
|||||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
.filter(users::role.ne(Role::Instance as i32))
|
|
||||||
.count()
|
.count()
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<User> {
|
pub fn find_by_fqn(conn: &DbConn, fqn: &str) -> Result<User> {
|
||||||
let from_db = users::table
|
let from_db = users::table
|
||||||
.filter(users::fqn.eq(fqn))
|
.filter(users::fqn.eq(fqn))
|
||||||
.first(conn)
|
.first(&**conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
if let Some(from_db) = from_db {
|
if let Some(from_db) = from_db {
|
||||||
Ok(from_db)
|
Ok(from_db)
|
||||||
@ -215,27 +203,6 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_local_by_name(
|
|
||||||
conn: &Connection,
|
|
||||||
name: &str,
|
|
||||||
(min, max): (i32, i32),
|
|
||||||
) -> Result<Vec<User>> {
|
|
||||||
users::table
|
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
|
||||||
.filter(users::role.ne(Role::Instance as i32))
|
|
||||||
// TODO: use `ilike` instead of `like` for PostgreSQL
|
|
||||||
.filter(
|
|
||||||
users::username
|
|
||||||
.like(format!("%{}%", name))
|
|
||||||
.or(users::display_name.like(format!("%{}%", name))),
|
|
||||||
)
|
|
||||||
.order(users::username.asc())
|
|
||||||
.offset(min.into())
|
|
||||||
.limit((max - min).into())
|
|
||||||
.load::<User>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Should create user record with normalized(lowercased) email
|
* TODO: Should create user record with normalized(lowercased) email
|
||||||
*/
|
*/
|
||||||
@ -252,7 +219,7 @@ impl User {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_from_webfinger(conn: &Connection, 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
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -342,42 +309,6 @@ impl User {
|
|||||||
bcrypt::hash(pass, 10).map_err(Error::from)
|
bcrypt::hash(pass, 10).map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
// [[ LDAP non anonymous bind copied from ....
|
|
||||||
fn ldap_preconn(ldap_conn: &mut LdapConn) -> Result<()> {
|
|
||||||
let ldap = CONFIG.ldap.as_ref().unwrap();
|
|
||||||
|
|
||||||
if let Some((user, password)) = ldap.user.as_ref() {
|
|
||||||
let bind = ldap_conn
|
|
||||||
.simple_bind(user, password)
|
|
||||||
.map_err(|_| Error::NotFound)?;
|
|
||||||
|
|
||||||
if bind.success().is_err() {
|
|
||||||
return Err(Error::NotFound);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_dn(ldap_conn: &mut LdapConn, fil: &str) -> Result<String> {
|
|
||||||
let mut cn: String = String::new();
|
|
||||||
let ldap = CONFIG.ldap.as_ref().unwrap();
|
|
||||||
let (rs, _res) = ldap_conn.search(
|
|
||||||
&ldap.base_dn,
|
|
||||||
Scope::Subtree,
|
|
||||||
fil,
|
|
||||||
vec!["cn"]
|
|
||||||
)
|
|
||||||
.map_err(|_| Error::NotFound)?
|
|
||||||
.success()
|
|
||||||
.map_err(|_| Error::NotFound)?;
|
|
||||||
for entry in rs {
|
|
||||||
println!("{:?}", SearchEntry::construct(entry.clone()).dn);
|
|
||||||
cn = SearchEntry::construct(entry).dn;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(cn.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
|
fn ldap_register(conn: &Connection, name: &str, password: &str) -> Result<User> {
|
||||||
if CONFIG.ldap.is_none() {
|
if CONFIG.ldap.is_none() {
|
||||||
return Err(Error::NotFound);
|
return Err(Error::NotFound);
|
||||||
@ -385,12 +316,7 @@ impl User {
|
|||||||
let ldap = CONFIG.ldap.as_ref().unwrap();
|
let ldap = CONFIG.ldap.as_ref().unwrap();
|
||||||
|
|
||||||
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
|
let mut ldap_conn = LdapConn::new(&ldap.addr).map_err(|_| Error::NotFound)?;
|
||||||
|
let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
|
||||||
User::ldap_preconn(&mut ldap_conn)?;
|
|
||||||
|
|
||||||
let _filter = format!("(&(objectClass=*)(uid={}))", name).to_owned();
|
|
||||||
let ldap_name = User::search_dn(&mut ldap_conn, &_filter).unwrap();
|
|
||||||
//let ldap_name = format!("{}={},{}", ldap.user_name_attr, name, ldap.base_dn);
|
|
||||||
let bind = ldap_conn
|
let bind = ldap_conn
|
||||||
.simple_bind(&ldap_name, password)
|
.simple_bind(&ldap_name, password)
|
||||||
.map_err(|_| Error::NotFound)?;
|
.map_err(|_| Error::NotFound)?;
|
||||||
@ -436,18 +362,10 @@ impl User {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if User::ldap_preconn(&mut conn).is_err() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//
|
|
||||||
let _filter = format!("(&(objectClass=*)(uid={}))", &self.username).to_owned();
|
|
||||||
let name = User::search_dn(&mut conn, &_filter).unwrap();
|
|
||||||
/*
|
|
||||||
let name = format!(
|
let name = format!(
|
||||||
"{}={},{}",
|
"{}={},{}",
|
||||||
ldap.user_name_attr, &self.username, ldap.base_dn
|
ldap.user_name_attr, &self.username, ldap.base_dn
|
||||||
);
|
);
|
||||||
*/
|
|
||||||
if let Ok(bind) = conn.simple_bind(&name, password) {
|
if let Ok(bind) = conn.simple_bind(&name, password) {
|
||||||
bind.success().is_ok()
|
bind.success().is_ok()
|
||||||
} else {
|
} else {
|
||||||
@ -494,7 +412,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
// if no user was found, and we were unable to auto-register from ldap
|
// if no user was found, and we were unable to auto-register from ldap
|
||||||
// fake-verify a password, and return an error.
|
// fake-verify a password, and return an error.
|
||||||
let other = User::get(&*conn, 1)
|
let other = User::get(conn, 1)
|
||||||
.expect("No user is registered")
|
.expect("No user is registered")
|
||||||
.hashed_password;
|
.hashed_password;
|
||||||
other.map(|pass| bcrypt::verify(password, &pass));
|
other.map(|pass| bcrypt::verify(password, &pass));
|
||||||
@ -502,7 +420,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ... ldap-non-anon PR https://git.joinplu.me/Plume/Plume/src/branch/ldap-non-anon ]].
|
|
||||||
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
|
pub fn reset_password(&self, conn: &Connection, pass: &str) -> Result<()> {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(users::hashed_password.eq(User::hash_pass(pass)?))
|
.set(users::hashed_password.eq(User::hash_pass(pass)?))
|
||||||
@ -513,7 +431,6 @@ impl User {
|
|||||||
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
.filter(users::role.ne(Role::Instance as i32))
|
|
||||||
.order(users::username.asc())
|
.order(users::username.asc())
|
||||||
.offset(min.into())
|
.offset(min.into())
|
||||||
.limit((max - min).into())
|
.limit((max - min).into())
|
||||||
@ -844,7 +761,7 @@ impl User {
|
|||||||
actor.set_url(ap_url.clone());
|
actor.set_url(ap_url.clone());
|
||||||
actor.set_inbox(self.inbox_url.parse()?);
|
actor.set_inbox(self.inbox_url.parse()?);
|
||||||
actor.set_outbox(self.outbox_url.parse()?);
|
actor.set_outbox(self.outbox_url.parse()?);
|
||||||
actor.set_preferred_username(self.username.clone());
|
actor.set_preferred_username(&self.fqn);
|
||||||
actor.set_followers(self.followers_endpoint.parse()?);
|
actor.set_followers(self.followers_endpoint.parse()?);
|
||||||
|
|
||||||
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
|
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
|
||||||
@ -1004,15 +921,15 @@ impl IntoId for User {
|
|||||||
|
|
||||||
impl Eq for User {}
|
impl Eq for User {}
|
||||||
|
|
||||||
impl FromId<Connection> for User {
|
impl FromId<DbConn> for User {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Object = CustomPerson;
|
type Object = CustomPerson;
|
||||||
|
|
||||||
fn from_db(conn: &Connection, 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: &Connection, acct: CustomPerson) -> Result<Self> {
|
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
|
||||||
let actor = acct.ap_actor_ref();
|
let actor = acct.ap_actor_ref();
|
||||||
let username = actor
|
let username = actor
|
||||||
.preferred_username()
|
.preferred_username()
|
||||||
@ -1113,7 +1030,7 @@ impl FromId<Connection> for User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsActor<&Connection> for User {
|
impl AsActor<&DbConn> for User {
|
||||||
fn get_inbox_url(&self) -> String {
|
fn get_inbox_url(&self) -> String {
|
||||||
self.inbox_url.clone()
|
self.inbox_url.clone()
|
||||||
}
|
}
|
||||||
@ -1129,11 +1046,11 @@ impl AsActor<&Connection> for User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsObject<User, Delete, &Connection> for User {
|
impl AsObject<User, Delete, &DbConn> for User {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn activity(self, conn: &Connection, actor: User, _id: &str) -> Result<()> {
|
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||||
if self.id == actor.id {
|
if self.id == actor.id {
|
||||||
self.delete(conn).map(|_| ())
|
self.delete(conn).map(|_| ())
|
||||||
} else {
|
} else {
|
||||||
|
1052
po/plume/af.po
1052
po/plume/af.po
File diff suppressed because it is too large
Load Diff
1036
po/plume/ar.po
1036
po/plume/ar.po
File diff suppressed because it is too large
Load Diff
1039
po/plume/bg.po
1039
po/plume/bg.po
File diff suppressed because it is too large
Load Diff
1150
po/plume/ca.po
1150
po/plume/ca.po
File diff suppressed because it is too large
Load Diff
1062
po/plume/cs.po
1062
po/plume/cs.po
File diff suppressed because it is too large
Load Diff
884
po/plume/cy.po
884
po/plume/cy.po
File diff suppressed because it is too large
Load Diff
1052
po/plume/da.po
1052
po/plume/da.po
File diff suppressed because it is too large
Load Diff
1006
po/plume/de.po
1006
po/plume/de.po
File diff suppressed because it is too large
Load Diff
1052
po/plume/el.po
1052
po/plume/el.po
File diff suppressed because it is too large
Load Diff
1052
po/plume/en.po
1052
po/plume/en.po
File diff suppressed because it is too large
Load Diff
1159
po/plume/eo.po
1159
po/plume/eo.po
File diff suppressed because it is too large
Load Diff
973
po/plume/es.po
973
po/plume/es.po
File diff suppressed because it is too large
Load Diff
1125
po/plume/eu.po
1125
po/plume/eu.po
File diff suppressed because it is too large
Load Diff
996
po/plume/fa.po
996
po/plume/fa.po
File diff suppressed because it is too large
Load Diff
1258
po/plume/fi.po
1258
po/plume/fi.po
File diff suppressed because it is too large
Load Diff
1143
po/plume/fr.po
1143
po/plume/fr.po
File diff suppressed because it is too large
Load Diff
1091
po/plume/gl.po
1091
po/plume/gl.po
File diff suppressed because it is too large
Load Diff
1063
po/plume/he.po
1063
po/plume/he.po
File diff suppressed because it is too large
Load Diff
1060
po/plume/hi.po
1060
po/plume/hi.po
File diff suppressed because it is too large
Load Diff
1135
po/plume/hr.po
1135
po/plume/hr.po
File diff suppressed because it is too large
Load Diff
1052
po/plume/hu.po
1052
po/plume/hu.po
File diff suppressed because it is too large
Load Diff
1041
po/plume/it.po
1041
po/plume/it.po
File diff suppressed because it is too large
Load Diff
1113
po/plume/ja.po
1113
po/plume/ja.po
File diff suppressed because it is too large
Load Diff
1048
po/plume/ko.po
1048
po/plume/ko.po
File diff suppressed because it is too large
Load Diff
1002
po/plume/nb.po
1002
po/plume/nb.po
File diff suppressed because it is too large
Load Diff
1142
po/plume/nl.po
1142
po/plume/nl.po
File diff suppressed because it is too large
Load Diff
1089
po/plume/no.po
1089
po/plume/no.po
File diff suppressed because it is too large
Load Diff
1161
po/plume/pl.po
1161
po/plume/pl.po
File diff suppressed because it is too large
Load Diff
@ -33,9 +33,6 @@ msgstr ""
|
|||||||
msgid "Your feed"
|
msgid "Your feed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "My feed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Local feed"
|
msgid "Local feed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -225,19 +222,139 @@ msgstr ""
|
|||||||
msgid "You can't delete someone else's account."
|
msgid "You can't delete someone else's account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create your account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create an account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Email confirmation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "An email will be sent to provided email. You can continue signing-up via the email."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Registration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Check your inbox!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "We sent a mail to the address you gave us, with a link for registration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password confirmation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Media upload"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Useful for visually impaired people, as well as licensing information"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Content warning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Leave it empty, if none is needed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "File"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Your media"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You don't have any media yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Content warning: {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Media details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Go back to the gallery"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Markdown syntax"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Copy it into your articles, to insert this media:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Use as an avatar"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Plume"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Menu"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Search"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "{0}'s subscriptions"
|
msgid "Log Out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Articles"
|
msgid "My account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Subscribers"
|
msgid "Log In"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Subscriptions"
|
msgid "Register"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "About this instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Privacy policy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Administration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Documentation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Source code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Matrix room"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Admin"
|
msgid "Admin"
|
||||||
@ -258,30 +375,30 @@ msgstr ""
|
|||||||
msgid "Subscribe"
|
msgid "Subscribe"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Create your account"
|
msgid "Follow {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Create an account"
|
msgid "Log in to follow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Username"
|
msgid "Enter your full username handle to follow"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Password confirmation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Apologies, but registrations are closed on this particular instance. You can, however, find a different one."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "{0}'s subscribers"
|
msgid "{0}'s subscribers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Articles"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Subscribers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Subscriptions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "{0}'s subscriptions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Your Dashboard"
|
msgid "Your Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -297,21 +414,9 @@ msgstr ""
|
|||||||
msgid "Your Drafts"
|
msgid "Your Drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Your media"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Go to your gallery"
|
msgid "Go to your gallery"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Follow {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log in to follow"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enter your full username handle to follow"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Edit your account"
|
msgid "Edit your account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -366,81 +471,10 @@ msgstr ""
|
|||||||
msgid "Recently boosted"
|
msgid "Recently boosted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Nothing to see here yet."
|
msgid "Articles tagged \"{0}\""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Edit"
|
msgid "There are currently no articles with such a tag"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "By {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Draft"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "One like"
|
|
||||||
msgid_plural "{0} likes"
|
|
||||||
msgstr[0] ""
|
|
||||||
|
|
||||||
msgid "One boost"
|
|
||||||
msgid_plural "{0} boosts"
|
|
||||||
msgstr[0] ""
|
|
||||||
|
|
||||||
msgid "What is Plume?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Plume is a decentralized blogging engine."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Authors can manage multiple blogs, each as its own website."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "About {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Home to <em>{0}</em> people"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Who wrote <em>{0}</em> articles"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Read the detailed rules"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Respond"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Are you sure?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete this comment"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "None"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You are not authorized."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Invalid CSRF token"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Page not found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "We couldn't find this page."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "The link that led you here may be broken."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "The content you sent can't be processed."
|
msgid "The content you sent can't be processed."
|
||||||
@ -458,172 +492,72 @@ msgstr ""
|
|||||||
msgid "Sorry about that. If you think this is a bug, please report it."
|
msgid "Sorry about that. If you think this is a bug, please report it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Articles tagged \"{0}\""
|
msgid "Invalid CSRF token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "There are currently no articles with such a tag"
|
msgid "Something is wrong with your CSRF token. Make sure cookies are enabled in you browser, and try reloading this page. If you continue to see this error message, please report it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "New Blog"
|
msgid "You are not authorized."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Create a blog"
|
msgid "Page not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Title"
|
msgid "We couldn't find this page."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Create blog"
|
msgid "The link that led you here may be broken."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Edit \"{}\""
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Markdown syntax is supported"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can upload images to your gallery, to use them as blog icons, or banners."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Upload images"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blog icon"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Blog banner"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Custom theme"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Update blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Be very careful, any action taken here can't be reversed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Are you sure that you want to permanently delete this blog?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Permanently delete this blog"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "{}'s icon"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "There's one author on this blog: "
|
|
||||||
msgid_plural "There are {0} authors on this blog: "
|
|
||||||
msgstr[0] ""
|
|
||||||
|
|
||||||
msgid "No posts to see here yet."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Media upload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Useful for visually impaired people, as well as licensing information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Content warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Leave it empty, if none is needed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "File"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Send"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Upload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You don't have any media yet."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Content warning: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Details"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Media details"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Go back to the gallery"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Markdown syntax"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Copy it into your articles, to insert this media:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use as an avatar"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "I'm from this instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Username, or email"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "I'm from another instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Continue to your instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Email confirmation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "An email will be sent to provided email. You can continue signing-up via the email."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Check your inbox!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "We sent a mail to the address you gave us, with a link for registration."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Administration of {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Instances"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Configuration"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Instances"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Email blocklist"
|
msgid "Email blocklist"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Grant admin rights"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Revoke admin rights"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Grant moderator rights"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Revoke moderator rights"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Ban"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Run on selected users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Moderator"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Moderation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Home"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Administration of {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Unblock"
|
msgid "Unblock"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Block"
|
msgid "Block"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -633,6 +567,9 @@ msgstr ""
|
|||||||
msgid "Short description"
|
msgid "Short description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Markdown syntax is supported"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Long description"
|
msgid "Long description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -642,22 +579,13 @@ msgstr ""
|
|||||||
msgid "Save these settings"
|
msgid "Save these settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Welcome to {}"
|
msgid "If you are browsing this site as a visitor, no data about you is collected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runs Plume {0}"
|
msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "And are connected to <em>{0}</em> other instances"
|
msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Administred by"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Moderation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Home"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Blocklisted Emails"
|
msgid "Blocklisted Emails"
|
||||||
@ -705,100 +633,37 @@ msgstr ""
|
|||||||
msgid "The user will be silently prevented from making an account"
|
msgid "The user will be silently prevented from making an account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search users"
|
msgid "Welcome to {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Grant admin rights"
|
msgid "Nothing to see here yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Revoke admin rights"
|
msgid "About {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Grant moderator rights"
|
msgid "Runs Plume {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Revoke moderator rights"
|
msgid "Home to <em>{0}</em> people"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Ban"
|
msgid "Who wrote <em>{0}</em> articles"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Run on selected users"
|
msgid "And are connected to <em>{0}</em> other instances"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Moderator"
|
msgid "Administred by"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Privacy policy"
|
msgid "Interact with {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "If you are browsing this site as a visitor, no data about you is collected."
|
msgid "Log in to interact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "As a registered user, you have to provide your username (which does not have to be your real name), your functional email address and a password, in order to be able to log in, write articles and comment. The content you submit is stored until you delete it."
|
msgid "Enter your full username to interact"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "When you log in, we store two cookies, one to keep your session open, the second to prevent other people to act on your behalf. We don't store any other cookies."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "This token has expired"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Please start the process again by clicking <a href=\"/password-reset\">here</a>."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reset your password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "New password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirmation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Update password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Send password reset link"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "We sent a mail to the address you gave us, with a link to reset your password."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Plume"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Menu"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Dashboard"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log Out"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "My account"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log In"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Register"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "About this instance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Documentation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Matrix room"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Publish"
|
msgid "Publish"
|
||||||
@ -807,6 +672,9 @@ msgstr ""
|
|||||||
msgid "Classic editor (any changes will be lost)"
|
msgid "Classic editor (any changes will be lost)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Title"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Subtitle"
|
msgid "Subtitle"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -840,15 +708,6 @@ msgstr ""
|
|||||||
msgid "Publish your post"
|
msgid "Publish your post"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Interact with {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Log in to interact"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enter your full username to interact"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Written by {0}"
|
msgid "Written by {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -858,12 +717,20 @@ msgstr ""
|
|||||||
msgid "This article is under the {0} license."
|
msgid "This article is under the {0} license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "One like"
|
||||||
|
msgid_plural "{0} likes"
|
||||||
|
msgstr[0] ""
|
||||||
|
|
||||||
msgid "I don't like this anymore"
|
msgid "I don't like this anymore"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Add yours"
|
msgid "Add yours"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "One boost"
|
||||||
|
msgid_plural "{0} boosts"
|
||||||
|
msgstr[0] ""
|
||||||
|
|
||||||
msgid "I don't want to boost this anymore"
|
msgid "I don't want to boost this anymore"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -885,12 +752,151 @@ msgstr ""
|
|||||||
msgid "No comments yet. Be the first to react!"
|
msgid "No comments yet. Be the first to react!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Are you sure?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "This article is still a draft. Only you and other authors can see it."
|
msgid "This article is still a draft. Only you and other authors can see it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Only you and other authors can edit this article."
|
msgid "Only you and other authors can edit this article."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "I'm from this instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Username, or email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "I'm from another instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Continue to your instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reset your password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "New password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Confirmation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Update password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "We sent a mail to the address you gave us, with a link to reset your password."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send password reset link"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This token has expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Please start the process again by clicking <a href=\"/password-reset\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "New Blog"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create a blog"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create blog"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit \"{}\""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You can upload images to your gallery, to use them as blog icons, or banners."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Upload images"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Blog icon"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Blog banner"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Custom theme"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Update blog"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Be very careful, any action taken here can't be reversed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Are you sure that you want to permanently delete this blog?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Permanently delete this blog"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "{}'s icon"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "There's one author on this blog: "
|
||||||
|
msgid_plural "There are {0} authors on this blog: "
|
||||||
|
msgstr[0] ""
|
||||||
|
|
||||||
|
msgid "No posts to see here yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "None"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Respond"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete this comment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "What is Plume?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Plume is a decentralized blogging engine."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Authors can manage multiple blogs, each as its own website."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Articles are also visible on other Plume instances, and you can interact with them directly from other platforms like Mastodon."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Read the detailed rules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "By {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Search result(s) for \"{0}\""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Search result(s)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No results for your query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No more results for your query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Advanced search"
|
msgid "Advanced search"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -947,15 +953,3 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "Article license"
|
msgid "Article license"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search result(s) for \"{0}\""
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search result(s)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No results for your query"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No more results for your query"
|
|
||||||
msgstr ""
|
|
||||||
|
1026
po/plume/pt.po
1026
po/plume/pt.po
File diff suppressed because it is too large
Load Diff
1291
po/plume/ro.po
1291
po/plume/ro.po
File diff suppressed because it is too large
Load Diff
1167
po/plume/ru.po
1167
po/plume/ru.po
File diff suppressed because it is too large
Load Diff
1017
po/plume/sat.po
1017
po/plume/sat.po
File diff suppressed because it is too large
Load Diff
1001
po/plume/si.po
1001
po/plume/si.po
File diff suppressed because it is too large
Load Diff
1060
po/plume/sk.po
1060
po/plume/sk.po
File diff suppressed because it is too large
Load Diff
1235
po/plume/sl.po
1235
po/plume/sl.po
File diff suppressed because it is too large
Load Diff
1129
po/plume/sr.po
1129
po/plume/sr.po
File diff suppressed because it is too large
Load Diff
1128
po/plume/sv.po
1128
po/plume/sv.po
File diff suppressed because it is too large
Load Diff
1074
po/plume/tr.po
1074
po/plume/tr.po
File diff suppressed because it is too large
Load Diff
1101
po/plume/uk.po
1101
po/plume/uk.po
File diff suppressed because it is too large
Load Diff
1048
po/plume/vi.po
1048
po/plume/vi.po
File diff suppressed because it is too large
Load Diff
1049
po/plume/zh.po
1049
po/plume/zh.po
File diff suppressed because it is too large
Load Diff
@ -1 +1 @@
|
|||||||
nightly-2023-11-10
|
nightly-2022-07-19
|
||||||
|
67
src/main.rs
67
src/main.rs
@ -16,7 +16,7 @@ use plume_models::{
|
|||||||
migrations::IMPORTED_MIGRATIONS,
|
migrations::IMPORTED_MIGRATIONS,
|
||||||
remote_fetch_actor::RemoteFetchActor,
|
remote_fetch_actor::RemoteFetchActor,
|
||||||
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
|
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
|
||||||
Connection, CONFIG,
|
Config, Connection, CONFIG,
|
||||||
};
|
};
|
||||||
use rocket_csrf::CsrfFairingBuilder;
|
use rocket_csrf::CsrfFairingBuilder;
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
@ -47,12 +47,12 @@ include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
|||||||
compile_i18n!();
|
compile_i18n!();
|
||||||
|
|
||||||
/// Initializes a database pool.
|
/// Initializes a database pool.
|
||||||
fn init_pool() -> Option<DbPool> {
|
fn init_pool(config: &Config) -> Option<DbPool> {
|
||||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||||
let mut builder = DbPool::builder()
|
let mut builder = DbPool::builder()
|
||||||
.connection_customizer(Box::new(PragmaForeignKey))
|
.connection_customizer(Box::new(PragmaForeignKey))
|
||||||
.min_idle(CONFIG.db_min_idle);
|
.min_idle(config.db_min_idle);
|
||||||
if let Some(max_size) = CONFIG.db_max_size {
|
if let Some(max_size) = config.db_max_size {
|
||||||
builder = builder.max_size(max_size);
|
builder = builder.max_size(max_size);
|
||||||
};
|
};
|
||||||
let pool = builder.build(manager).ok()?;
|
let pool = builder.build(manager).ok()?;
|
||||||
@ -63,28 +63,8 @@ fn init_pool() -> Option<DbPool> {
|
|||||||
Some(pool)
|
Some(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn init_rocket() -> rocket::Rocket {
|
pub(crate) fn init_rocket(config: &Config) -> rocket::Rocket {
|
||||||
match dotenv::dotenv() {
|
let dbpool = init_pool(config).expect("main: database pool initialization error");
|
||||||
Ok(path) => eprintln!("Configuration read from {}", path.display()),
|
|
||||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
|
||||||
e => e.map(|_| ()).unwrap(),
|
|
||||||
}
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
App::new("Plume")
|
|
||||||
.bin_name("plume")
|
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
|
||||||
.about("Plume backend server")
|
|
||||||
.after_help(
|
|
||||||
r#"
|
|
||||||
The plume command should be run inside the directory
|
|
||||||
containing the `.env` configuration file and `static` directory.
|
|
||||||
See https://docs.joinplu.me/installation/config
|
|
||||||
and https://docs.joinplu.me/installation/init for more info.
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
let dbpool = init_pool().expect("main: database pool initialization error");
|
|
||||||
if IMPORTED_MIGRATIONS
|
if IMPORTED_MIGRATIONS
|
||||||
.is_pending(&dbpool.get().unwrap())
|
.is_pending(&dbpool.get().unwrap())
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
@ -101,12 +81,11 @@ Then try to restart Plume.
|
|||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
//let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||||
let workpool = ScheduledThreadPool::new(num_cpus::get());
|
|
||||||
// we want a fast exit here, so
|
// we want a fast exit here, so
|
||||||
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
|
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
|
||||||
&CONFIG.search_index,
|
&config.search_index,
|
||||||
&CONFIG.search_tokenizers,
|
&config.search_tokenizers,
|
||||||
));
|
));
|
||||||
RemoteFetchActor::init(dbpool.clone());
|
RemoteFetchActor::init(dbpool.clone());
|
||||||
SearchActor::init(searcher.clone(), dbpool.clone());
|
SearchActor::init(searcher.clone(), dbpool.clone());
|
||||||
@ -126,12 +105,12 @@ Then try to restart Plume.
|
|||||||
.expect("Error setting Ctrl-c handler");
|
.expect("Error setting Ctrl-c handler");
|
||||||
|
|
||||||
let mail = mail::init();
|
let mail = mail::init();
|
||||||
if mail.is_none() && CONFIG.rocket.as_ref().unwrap().environment.is_prod() {
|
if mail.is_none() && config.rocket.as_ref().unwrap().environment.is_prod() {
|
||||||
warn!("Warning: the email server is not configured (or not completely).");
|
warn!("Warning: the email server is not configured (or not completely).");
|
||||||
warn!("Please refer to the documentation to see how to configure it.");
|
warn!("Please refer to the documentation to see how to configure it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
rocket::custom(CONFIG.rocket.clone().unwrap())
|
rocket::custom(config.rocket.clone().unwrap())
|
||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
routes![
|
routes![
|
||||||
@ -158,7 +137,6 @@ Then try to restart Plume.
|
|||||||
routes::instance::admin_mod,
|
routes::instance::admin_mod,
|
||||||
routes::instance::admin_instances,
|
routes::instance::admin_instances,
|
||||||
routes::instance::admin_users,
|
routes::instance::admin_users,
|
||||||
routes::instance::admin_search_users,
|
|
||||||
routes::instance::admin_email_blocklist,
|
routes::instance::admin_email_blocklist,
|
||||||
routes::instance::add_email_blocklist,
|
routes::instance::add_email_blocklist,
|
||||||
routes::instance::delete_email_blocklist,
|
routes::instance::delete_email_blocklist,
|
||||||
@ -282,7 +260,28 @@ Then try to restart Plume.
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let rocket = init_rocket();
|
match dotenv::dotenv() {
|
||||||
|
Ok(path) => eprintln!("Configuration read from {}", path.display()),
|
||||||
|
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||||
|
e => e.map(|_| ()).unwrap(),
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
App::new("Plume")
|
||||||
|
.bin_name("plume")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.about("Plume backend server")
|
||||||
|
.after_help(
|
||||||
|
r#"
|
||||||
|
The plume command should be run inside the directory
|
||||||
|
containing the `.env` configuration file and `static` directory.
|
||||||
|
See https://docs.joinplu.me/installation/config
|
||||||
|
and https://docs.joinplu.me/installation/init for more info.
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
let rocket = init_rocket(&CONFIG);
|
||||||
|
|
||||||
#[cfg(feature = "test")]
|
#[cfg(feature = "test")]
|
||||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use activitystreams::collection::{OrderedCollection, OrderedCollectionPage};
|
use activitystreams::{
|
||||||
|
collection::{OrderedCollection, OrderedCollectionPage},
|
||||||
|
iri_string::{spec::IriSpec, validate::iri_reference},
|
||||||
|
};
|
||||||
use diesel::SaveChangesDsl;
|
use diesel::SaveChangesDsl;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::ContentType,
|
http::ContentType,
|
||||||
@ -80,7 +83,7 @@ pub struct NewBlogForm {
|
|||||||
|
|
||||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||||
let slug = Blog::slug(title);
|
let slug = Blog::slug(title);
|
||||||
if slug.is_empty() {
|
if slug.is_empty() || iri_reference::<IriSpec>(&slug).is_err() {
|
||||||
Err(ValidationError::new("empty_slug"))
|
Err(ValidationError::new("empty_slug"))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -122,7 +125,7 @@ pub fn create(
|
|||||||
let blog = Blog::insert(
|
let blog = Blog::insert(
|
||||||
&conn,
|
&conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
slug.clone().into(),
|
slug.clone(),
|
||||||
form.title.to_string(),
|
form.title.to_string(),
|
||||||
String::from(""),
|
String::from(""),
|
||||||
Instance::get_local()
|
Instance::get_local()
|
||||||
@ -144,7 +147,7 @@ pub fn create(
|
|||||||
.expect("blog::create: author error");
|
.expect("blog::create: author error");
|
||||||
|
|
||||||
Flash::success(
|
Flash::success(
|
||||||
Redirect::to(uri!(details: name = slug, page = _)),
|
Redirect::to(uri!(details: name = &slug, page = _)),
|
||||||
&i18n!(intl, "Your blog was successfully created!"),
|
&i18n!(intl, "Your blog was successfully created!"),
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
@ -379,6 +382,8 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::env::var;
|
||||||
|
|
||||||
use super::valid_slug;
|
use super::valid_slug;
|
||||||
use crate::init_rocket;
|
use crate::init_rocket;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
@ -387,62 +392,107 @@ mod tests {
|
|||||||
blog_authors::{BlogAuthor, NewBlogAuthor},
|
blog_authors::{BlogAuthor, NewBlogAuthor},
|
||||||
blogs::{Blog, NewBlog},
|
blogs::{Blog, NewBlog},
|
||||||
db_conn::{DbConn, DbPool},
|
db_conn::{DbConn, DbPool},
|
||||||
|
get_rocket_config,
|
||||||
instance::{Instance, NewInstance},
|
instance::{Instance, NewInstance},
|
||||||
post_authors::{NewPostAuthor, PostAuthor},
|
post_authors::{NewPostAuthor, PostAuthor},
|
||||||
posts::{NewPost, Post},
|
posts::{NewPost, Post},
|
||||||
safe_string::SafeString,
|
safe_string::SafeString,
|
||||||
|
search::Searcher,
|
||||||
users::{NewUser, User, AUTH_COOKIE},
|
users::{NewUser, User, AUTH_COOKIE},
|
||||||
Connection as Conn, CONFIG,
|
Config, Fqn, SearchTokenizerConfig,
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{Cookie, Cookies, SameSite},
|
http::{ContentType, Cookie, Cookies, SameSite, Status},
|
||||||
local::{Client, LocalRequest},
|
local::{Client, LocalRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
type Models = (Instance, User, Blog, Post);
|
||||||
fn edit_link_within_post_card() {
|
|
||||||
let conn = Conn::establish(CONFIG.database_url.as_str()).unwrap();
|
fn setup() -> (Client, Models) {
|
||||||
Instance::insert(
|
dotenv::from_path(".env.test").unwrap();
|
||||||
&conn,
|
let config = Config {
|
||||||
NewInstance {
|
base_url: var("BASE_URL").unwrap(),
|
||||||
public_domain: "example.org".to_string(),
|
db_name: "plume",
|
||||||
name: "Plume".to_string(),
|
db_max_size: None,
|
||||||
local: true,
|
db_min_idle: None,
|
||||||
long_description: SafeString::new(""),
|
signup: Default::default(),
|
||||||
short_description: SafeString::new(""),
|
database_url: var("DATABASE_URL").unwrap(),
|
||||||
default_license: "CC-BY-SA".to_string(),
|
search_index: format!("/tmp/plume_test-{}", random_hex()),
|
||||||
open_registrations: true,
|
search_tokenizers: SearchTokenizerConfig::init(),
|
||||||
short_description_html: String::new(),
|
rocket: get_rocket_config(),
|
||||||
long_description_html: String::new(),
|
logo: Default::default(),
|
||||||
},
|
default_theme: Default::default(),
|
||||||
)
|
media_directory: format!("/tmp/plume_test-{}", random_hex()),
|
||||||
.unwrap();
|
mail: None,
|
||||||
let rocket = init_rocket();
|
ldap: None,
|
||||||
|
proxy: None,
|
||||||
|
};
|
||||||
|
let _ = Searcher::create(&config.search_index, &config.search_tokenizers).unwrap();
|
||||||
|
let rocket = init_rocket(&config);
|
||||||
let client = Client::new(rocket).expect("valid rocket instance");
|
let client = Client::new(rocket).expect("valid rocket instance");
|
||||||
let dbpool = client.rocket().state::<DbPool>().unwrap();
|
let dbpool = client.rocket().state::<DbPool>().unwrap();
|
||||||
let conn = &DbConn(dbpool.get().unwrap());
|
let conn = &DbConn(dbpool.get().unwrap());
|
||||||
|
|
||||||
let (_instance, user, blog, post) = create_models(conn);
|
(client, create_models(conn))
|
||||||
|
}
|
||||||
|
|
||||||
let blog_path = uri!(super::activity_details: name = &blog.fqn).to_string();
|
fn teardown((client, (instance, user, _blog, _post)): (&Client, Models)) {
|
||||||
|
let rocket = client.rocket();
|
||||||
|
|
||||||
|
let dbpool = rocket.state::<DbPool>().unwrap();
|
||||||
|
let conn = &DbConn(dbpool.get().unwrap());
|
||||||
|
|
||||||
|
user.delete(conn).unwrap();
|
||||||
|
let _ = diesel::delete(&instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edit_link_within_post_card() {
|
||||||
|
let (client, (instance, user, blog, post)) = setup();
|
||||||
|
|
||||||
|
let blog_path = uri!(super::activity_details: name = &blog.fqn.to_string()).to_string();
|
||||||
let edit_link = uri!(
|
let edit_link = uri!(
|
||||||
super::super::posts::edit: blog = &blog.fqn,
|
super::super::posts::edit: blog = &blog.fqn.to_string(),
|
||||||
slug = &post.slug
|
slug = &post.slug
|
||||||
)
|
)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let mut response = client.get(&blog_path).dispatch();
|
let mut response = client.get(&blog_path).dispatch();
|
||||||
let body = response.body_string().unwrap();
|
let body = response.body_string().unwrap();
|
||||||
assert!(!body.contains(&edit_link));
|
let body_not_contain_edit_link = !body.contains(&edit_link);
|
||||||
|
|
||||||
let request = client.get(&blog_path);
|
let request = client.get(&blog_path);
|
||||||
login(&request, &user);
|
login(&request, &user);
|
||||||
let mut response = request.dispatch();
|
let mut response = request.dispatch();
|
||||||
let body = response.body_string().unwrap();
|
let body = response.body_string().unwrap();
|
||||||
assert!(body.contains(&edit_link));
|
eprintln!("{:?}", &blog.fqn);
|
||||||
|
let body_contains_edit_lnk = body.contains(&edit_link);
|
||||||
|
|
||||||
|
teardown((&client, (instance, user, blog, post)));
|
||||||
|
|
||||||
|
assert!(body_not_contain_edit_link);
|
||||||
|
assert!(body_contains_edit_lnk);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_models(conn: &DbConn) -> (Instance, User, Blog, Post) {
|
fn create_models(conn: &DbConn) -> Models {
|
||||||
|
Instance::find_by_domain(conn, "example.org").unwrap_or_else(|_| {
|
||||||
|
Instance::insert(
|
||||||
|
conn,
|
||||||
|
NewInstance {
|
||||||
|
public_domain: "example.org".to_string(),
|
||||||
|
name: "Plume".to_string(),
|
||||||
|
local: true,
|
||||||
|
long_description: SafeString::new(""),
|
||||||
|
short_description: SafeString::new(""),
|
||||||
|
default_license: "CC-BY-SA".to_string(),
|
||||||
|
open_registrations: true,
|
||||||
|
short_description_html: String::new(),
|
||||||
|
long_description_html: String::new(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
conn.transaction::<(Instance, User, Blog, Post), diesel::result::Error, _>(|| {
|
conn.transaction::<(Instance, User, Blog, Post), diesel::result::Error, _>(|| {
|
||||||
let instance = Instance::get_local().unwrap_or_else(|_| {
|
let instance = Instance::get_local().unwrap_or_else(|_| {
|
||||||
let instance = Instance::insert(
|
let instance = Instance::insert(
|
||||||
@ -470,16 +520,26 @@ mod tests {
|
|||||||
inbox_url: random_hex(),
|
inbox_url: random_hex(),
|
||||||
outbox_url: random_hex(),
|
outbox_url: random_hex(),
|
||||||
followers_endpoint: random_hex(),
|
followers_endpoint: random_hex(),
|
||||||
|
fqn: random_hex(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let user = User::insert(conn, user).unwrap();
|
let user = User::insert(conn, user).unwrap();
|
||||||
|
let title = random_hex();
|
||||||
let blog = NewBlog {
|
let blog = NewBlog {
|
||||||
instance_id: instance.id,
|
instance_id: instance.id,
|
||||||
|
fqn: Fqn::make_local(&title).unwrap(),
|
||||||
|
title,
|
||||||
actor_id: random_hex(),
|
actor_id: random_hex(),
|
||||||
ap_url: random_hex(),
|
ap_url: random_hex(),
|
||||||
inbox_url: random_hex(),
|
inbox_url: random_hex(),
|
||||||
outbox_url: random_hex(),
|
outbox_url: random_hex(),
|
||||||
..Default::default()
|
summary: Default::default(),
|
||||||
|
summary_html: Default::default(),
|
||||||
|
public_key: Default::default(),
|
||||||
|
private_key: Default::default(),
|
||||||
|
icon_id: Default::default(),
|
||||||
|
banner_id: Default::default(),
|
||||||
|
theme: Default::default(),
|
||||||
};
|
};
|
||||||
let blog = Blog::insert(conn, blog).unwrap();
|
let blog = Blog::insert(conn, blog).unwrap();
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
@ -535,4 +595,38 @@ mod tests {
|
|||||||
assert!(valid_slug("Blog Title").is_ok());
|
assert!(valid_slug("Blog Title").is_ok());
|
||||||
assert!(valid_slug("ブログ タイトル").is_ok());
|
assert!(valid_slug("ブログ タイトル").is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_blog_with_same_title_twice() {
|
||||||
|
let (client, (instance, user, blog, post)) = setup();
|
||||||
|
|
||||||
|
let new_path = uri!(super::new).to_string();
|
||||||
|
let request = client.get(new_path);
|
||||||
|
login(&request, &user);
|
||||||
|
let mut response = request.dispatch();
|
||||||
|
let body = response.body_string().unwrap();
|
||||||
|
let prefix = r#"<input type="hidden" name="csrf-token" value=""#;
|
||||||
|
let pos = body.find(prefix).unwrap();
|
||||||
|
let token = body[pos + prefix.len()..pos + prefix.len() + 123].to_string();
|
||||||
|
|
||||||
|
let create_path = uri!(super::create).to_string();
|
||||||
|
let response = client
|
||||||
|
.post(&create_path)
|
||||||
|
.body(format!("title=My%20Blog&csrf-token={}", &token))
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.dispatch();
|
||||||
|
let first_attempt = response;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(&create_path)
|
||||||
|
.body(format!("title=My%20Blog&csrf-token={}", &token))
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.dispatch();
|
||||||
|
let second_attempt = response;
|
||||||
|
|
||||||
|
teardown((&client, (instance, user, blog, post)));
|
||||||
|
|
||||||
|
assert_eq!(first_attempt.status(), Status::SeeOther);
|
||||||
|
assert_eq!(second_attempt.status(), Status::SeeOther);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ pub fn index(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/admin")]
|
#[get("/admin")]
|
||||||
pub fn admin(_admin: InclusiveAdmin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
pub fn admin(_admin: Admin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
let local_inst = Instance::get_local()?;
|
let local_inst = Instance::get_local()?;
|
||||||
Ok(render!(instance::admin(
|
Ok(render!(instance::admin(
|
||||||
&(&conn, &rockets).to_context(),
|
&(&conn, &rockets).to_context(),
|
||||||
@ -160,7 +160,7 @@ pub fn toggle_block(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/admin/users?<page>", rank = 2)]
|
#[get("/admin/users?<page>")]
|
||||||
pub fn admin_users(
|
pub fn admin_users(
|
||||||
_mod: Moderator,
|
_mod: Moderator,
|
||||||
page: Option<Page>,
|
page: Option<Page>,
|
||||||
@ -171,30 +171,6 @@ pub fn admin_users(
|
|||||||
Ok(render!(instance::users(
|
Ok(render!(instance::users(
|
||||||
&(&conn, &rockets).to_context(),
|
&(&conn, &rockets).to_context(),
|
||||||
User::get_local_page(&conn, page.limits())?,
|
User::get_local_page(&conn, page.limits())?,
|
||||||
None,
|
|
||||||
page.0,
|
|
||||||
Page::total(User::count_local(&conn)? as i32)
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
#[get("/admin/users?<user>&<page>", rank = 1)]
|
|
||||||
pub fn admin_search_users(
|
|
||||||
_mod: Moderator,
|
|
||||||
user: String,
|
|
||||||
page: Option<Page>,
|
|
||||||
conn: DbConn,
|
|
||||||
rockets: PlumeRocket,
|
|
||||||
) -> Result<Ructe, ErrorPage> {
|
|
||||||
let page = page.unwrap_or_default();
|
|
||||||
let users = if user.is_empty() {
|
|
||||||
User::get_local_page(&conn, page.limits())?
|
|
||||||
} else {
|
|
||||||
User::search_local_by_name(&conn, &user, page.limits())?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(render!(instance::users(
|
|
||||||
&(&conn, &rockets).to_context(),
|
|
||||||
users,
|
|
||||||
Some(user.as_str()),
|
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(User::count_local(&conn)? as i32)
|
Page::total(User::count_local(&conn)? as i32)
|
||||||
)))
|
)))
|
||||||
@ -431,7 +407,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
|
|||||||
|
|
||||||
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
|
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
|
||||||
return Some(Redirect::to(uri!(
|
return Some(Redirect::to(uri!(
|
||||||
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
|
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn.to_string(),
|
||||||
slug = &post.slug,
|
slug = &post.slug,
|
||||||
responding_to = _
|
responding_to = _
|
||||||
)));
|
)));
|
||||||
@ -442,7 +418,7 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
|
|||||||
let post = comment.get_post(&conn).expect("Can't retrieve post");
|
let post = comment.get_post(&conn).expect("Can't retrieve post");
|
||||||
return Some(Redirect::to(uri!(
|
return Some(Redirect::to(uri!(
|
||||||
super::posts::details: blog =
|
super::posts::details: blog =
|
||||||
post.get_blog(&conn).expect("Can't retrieve blog").fqn,
|
post.get_blog(&conn).expect("Can't retrieve blog").fqn.to_string(),
|
||||||
slug = &post.slug,
|
slug = &post.slug,
|
||||||
responding_to = comment.id
|
responding_to = comment.id
|
||||||
)));
|
)));
|
||||||
|
@ -2,7 +2,7 @@ use crate::routes::{errors::ErrorPage, Page};
|
|||||||
use crate::template_utils::{IntoContext, Ructe};
|
use crate::template_utils::{IntoContext, Ructe};
|
||||||
use guid_create::GUID;
|
use guid_create::GUID;
|
||||||
use multipart::server::{
|
use multipart::server::{
|
||||||
save::{SaveResult, SavedField, SavedData},
|
save::{SaveResult, SavedData},
|
||||||
Multipart,
|
Multipart,
|
||||||
};
|
};
|
||||||
use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket, CONFIG};
|
use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket, CONFIG};
|
||||||
@ -55,16 +55,41 @@ pub fn upload(
|
|||||||
if let SaveResult::Full(entries) = Multipart::with_body(data.open(), boundary).save().temp() {
|
if let SaveResult::Full(entries) = Multipart::with_body(data.open(), boundary).save().temp() {
|
||||||
let fields = entries.fields;
|
let fields = entries.fields;
|
||||||
|
|
||||||
let file = fields
|
let filename = fields
|
||||||
.get("file")
|
.get("file")
|
||||||
.and_then(|v| v.iter().next())
|
.and_then(|v| v.iter().next())
|
||||||
.ok_or(status::BadRequest(Some("No file uploaded")))?;
|
.ok_or(status::BadRequest(Some("No file uploaded")))?
|
||||||
|
.headers
|
||||||
|
.filename
|
||||||
|
.clone();
|
||||||
|
// Remove extension if it contains something else than just letters and numbers
|
||||||
|
let ext = filename
|
||||||
|
.and_then(|f| {
|
||||||
|
f.rsplit('.')
|
||||||
|
.next()
|
||||||
|
.and_then(|ext| {
|
||||||
|
if ext.chars().any(|c| !c.is_alphanumeric()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ext.to_lowercase())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|ext| format!(".{}", ext))
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let dest = format!("{}/{}{}", CONFIG.media_directory, GUID::rand(), ext);
|
||||||
|
|
||||||
let file_path = match save_uploaded_file(file) {
|
match fields["file"][0].data {
|
||||||
Ok(Some(file_path)) => file_path,
|
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
|
||||||
Ok(None) => return Ok(Redirect::to(uri!(new))),
|
.map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
|
||||||
Err(_) => return Err(status::BadRequest(Some("Couldn't save uploaded media: {}"))),
|
SavedData::File(ref path, _) => {
|
||||||
};
|
fs::copy(path, &dest)
|
||||||
|
.map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Ok(Redirect::to(uri!(new)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let has_cw = !read(&fields["cw"][0].data)
|
let has_cw = !read(&fields["cw"][0].data)
|
||||||
.map(|cw| cw.is_empty())
|
.map(|cw| cw.is_empty())
|
||||||
@ -72,7 +97,7 @@ pub fn upload(
|
|||||||
let media = Media::insert(
|
let media = Media::insert(
|
||||||
&conn,
|
&conn,
|
||||||
NewMedia {
|
NewMedia {
|
||||||
file_path,
|
file_path: dest,
|
||||||
alt_text: read(&fields["alt"][0].data)?,
|
alt_text: read(&fields["alt"][0].data)?,
|
||||||
is_remote: false,
|
is_remote: false,
|
||||||
remote_url: None,
|
remote_url: None,
|
||||||
@ -92,74 +117,6 @@ pub fn upload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_uploaded_file(file: &SavedField) -> Result<Option<String>, plume_models::Error> {
|
|
||||||
// Remove extension if it contains something else than just letters and numbers
|
|
||||||
let ext = file
|
|
||||||
.headers
|
|
||||||
.filename
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|f| {
|
|
||||||
f.rsplit('.')
|
|
||||||
.next()
|
|
||||||
.and_then(|ext| {
|
|
||||||
if ext.chars().any(|c| !c.is_alphanumeric()) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ext.to_lowercase())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if CONFIG.s3.is_some() {
|
|
||||||
#[cfg(not(feature="s3"))]
|
|
||||||
unreachable!();
|
|
||||||
|
|
||||||
#[cfg(feature="s3")]
|
|
||||||
{
|
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
let dest = format!("static/media/{}.{}", GUID::rand(), ext);
|
|
||||||
|
|
||||||
let bytes = match file.data {
|
|
||||||
SavedData::Bytes(ref bytes) => Cow::from(bytes),
|
|
||||||
SavedData::File(ref path, _) => Cow::from(fs::read(path)?),
|
|
||||||
_ => {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let bucket = CONFIG.s3.as_ref().unwrap().get_bucket();
|
|
||||||
let content_type = match &file.headers.content_type {
|
|
||||||
Some(ct) => ct.to_string(),
|
|
||||||
None => ContentType::from_extension(&ext)
|
|
||||||
.unwrap_or(ContentType::Binary)
|
|
||||||
.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
bucket.put_object_with_content_type_blocking(&dest, &bytes, &content_type)?;
|
|
||||||
|
|
||||||
Ok(Some(dest))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let dest = format!("{}/{}.{}", CONFIG.media_directory, GUID::rand(), ext);
|
|
||||||
|
|
||||||
match file.data {
|
|
||||||
SavedData::Bytes(ref bytes) => {
|
|
||||||
fs::write(&dest, bytes)?;
|
|
||||||
}
|
|
||||||
SavedData::File(ref path, _) => {
|
|
||||||
fs::copy(path, &dest)?;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(dest))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read(data: &SavedData) -> Result<String, status::BadRequest<&'static str>> {
|
fn read(data: &SavedData) -> Result<String, status::BadRequest<&'static str>> {
|
||||||
if let SavedData::Text(s) = data {
|
if let SavedData::Text(s) = data {
|
||||||
Ok(s.clone())
|
Ok(s.clone())
|
||||||
|
@ -21,9 +21,6 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
use rocket::http::ContentType;
|
|
||||||
|
|
||||||
/// Special return type used for routes that "cannot fail", and instead
|
/// Special return type used for routes that "cannot fail", and instead
|
||||||
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
|
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
@ -143,7 +140,7 @@ pub fn build_atom_feed(
|
|||||||
FeedBuilder::default()
|
FeedBuilder::default()
|
||||||
.title(title)
|
.title(title)
|
||||||
.id(uri)
|
.id(uri)
|
||||||
.updated(DateTime::<Utc>::from_naive_utc_and_offset(*updated, Utc))
|
.updated(DateTime::<Utc>::from_utc(*updated, Utc))
|
||||||
.entries(
|
.entries(
|
||||||
entries
|
entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -182,9 +179,9 @@ fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
|||||||
// Using RFC 4287 format, see https://tools.ietf.org/html/rfc4287#section-3.3 for dates
|
// Using RFC 4287 format, see https://tools.ietf.org/html/rfc4287#section-3.3 for dates
|
||||||
// eg: 2003-12-13T18:30:02Z (Z is here because there is no timezone support with the NaiveDateTime crate)
|
// eg: 2003-12-13T18:30:02Z (Z is here because there is no timezone support with the NaiveDateTime crate)
|
||||||
.published(Some(
|
.published(Some(
|
||||||
DateTime::<Utc>::from_naive_utc_and_offset(post.creation_date, Utc).into(),
|
DateTime::<Utc>::from_utc(post.creation_date, Utc).into(),
|
||||||
))
|
))
|
||||||
.updated(DateTime::<Utc>::from_naive_utc_and_offset(post.creation_date, Utc))
|
.updated(DateTime::<Utc>::from_utc(post.creation_date, Utc))
|
||||||
.id(post.ap_url.clone())
|
.id(post.ap_url.clone())
|
||||||
.links(vec![LinkBuilder::default().href(post.ap_url).build()])
|
.links(vec![LinkBuilder::default().href(post.ap_url).build()])
|
||||||
.build()
|
.build()
|
||||||
@ -207,17 +204,10 @@ pub mod timelines;
|
|||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod well_known;
|
pub mod well_known;
|
||||||
|
|
||||||
#[derive(Responder)]
|
|
||||||
enum FileKind {
|
|
||||||
Local(NamedFile),
|
|
||||||
#[cfg(feature = "s3")]
|
|
||||||
S3(Vec<u8>, ContentType),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Responder)]
|
#[derive(Responder)]
|
||||||
#[response()]
|
#[response()]
|
||||||
pub struct CachedFile {
|
pub struct CachedFile {
|
||||||
inner: FileKind,
|
inner: NamedFile,
|
||||||
cache_control: CacheControl,
|
cache_control: CacheControl,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,41 +253,19 @@ pub fn plume_static_files(file: PathBuf, build_id: &RawStr) -> Option<CachedFile
|
|||||||
}
|
}
|
||||||
#[get("/static/media/<file..>")]
|
#[get("/static/media/<file..>")]
|
||||||
pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
|
pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
|
||||||
if CONFIG.s3.is_some() {
|
NamedFile::open(Path::new(&CONFIG.media_directory).join(file))
|
||||||
#[cfg(not(feature="s3"))]
|
.ok()
|
||||||
unreachable!();
|
.map(|f| CachedFile {
|
||||||
|
inner: f,
|
||||||
#[cfg(feature="s3")]
|
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
||||||
{
|
})
|
||||||
let data = CONFIG.s3.as_ref().unwrap().get_bucket()
|
|
||||||
.get_object_blocking(format!("static/media/{}", file.to_string_lossy())).ok()?;
|
|
||||||
|
|
||||||
let ct = data.headers().get("content-type")
|
|
||||||
.and_then(|x| ContentType::parse_flexible(&x))
|
|
||||||
.or_else(|| file.extension()
|
|
||||||
.and_then(|ext| ContentType::from_extension(&ext.to_string_lossy())))
|
|
||||||
.unwrap_or(ContentType::Binary);
|
|
||||||
|
|
||||||
Some(CachedFile {
|
|
||||||
inner: FileKind::S3(data.to_vec(), ct),
|
|
||||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NamedFile::open(Path::new(&CONFIG.media_directory).join(file))
|
|
||||||
.ok()
|
|
||||||
.map(|f| CachedFile {
|
|
||||||
inner: FileKind::Local(f),
|
|
||||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[get("/static/<file..>", rank = 3)]
|
#[get("/static/<file..>", rank = 3)]
|
||||||
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
|
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
|
||||||
NamedFile::open(Path::new("static/").join(file))
|
NamedFile::open(Path::new("static/").join(file))
|
||||||
.ok()
|
.ok()
|
||||||
.map(|f| CachedFile {
|
.map(|f| CachedFile {
|
||||||
inner: FileKind::Local(f),
|
inner: f,
|
||||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -87,8 +87,6 @@
|
|||||||
<a href="@uri!(instance::privacy)">@i18n!(ctx.1, "Privacy policy")</a>
|
<a href="@uri!(instance::privacy)">@i18n!(ctx.1, "Privacy policy")</a>
|
||||||
@if ctx.2.clone().map(|u| u.is_admin()).unwrap_or(false) {
|
@if ctx.2.clone().map(|u| u.is_admin()).unwrap_or(false) {
|
||||||
<a href="@uri!(instance::admin)">@i18n!(ctx.1, "Administration")</a>
|
<a href="@uri!(instance::admin)">@i18n!(ctx.1, "Administration")</a>
|
||||||
} else if ctx.2.clone().map(|u| u.is_moderator()).unwrap_or(false) {
|
|
||||||
<a href="@uri!(instance::admin_mod)">@i18n!(ctx.1, "Moderation")</a>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<meta content="@blog.summary_html" property="og:description" />
|
<meta content="@blog.summary_html" property="og:description" />
|
||||||
<meta content="@blog.icon_url(ctx.0)" property="og:image" />
|
<meta content="@blog.icon_url(ctx.0)" property="og:image" />
|
||||||
|
|
||||||
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn, "atom.xml")' rel='alternate' type='application/atom+xml'>
|
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn.to_string(), "atom.xml")' rel='alternate' type='application/atom+xml'>
|
||||||
<link href='@blog.ap_url' rel='alternate' type='application/activity+json'>
|
<link href='@blog.ap_url' rel='alternate' type='application/activity+json'>
|
||||||
<link href='@blog.ap_url' rel='canonical'>
|
<link href='@blog.ap_url' rel='canonical'>
|
||||||
@if !ctx.2.clone().map(|u| u.hide_custom_css).unwrap_or(false) {
|
@if !ctx.2.clone().map(|u| u.hide_custom_css).unwrap_or(false) {
|
||||||
@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)" dir="auto">@blog.title</a>
|
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)" dir="auto">@blog.title</a>
|
||||||
}, {
|
}, {
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
@for author in authors {
|
@for author in authors {
|
||||||
@ -58,8 +58,8 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@if ctx.2.clone().and_then(|u| u.is_author_in(ctx.0, &blog).ok()).unwrap_or(false) {
|
@if ctx.2.clone().and_then(|u| u.is_author_in(ctx.0, &blog).ok()).unwrap_or(false) {
|
||||||
<a href="@uri!(posts::new: blog = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "New article")</a>
|
<a href="@uri!(posts::new: blog = &blog.fqn.to_string())" class="button" dir="auto">@i18n!(ctx.1, "New article")</a>
|
||||||
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
|
<a href="@uri!(blogs::edit: name = &blog.fqn.to_string())" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -76,7 +76,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<h2 dir="auto">
|
<h2 dir="auto">
|
||||||
@i18n!(ctx.1, "Latest articles")
|
@i18n!(ctx.1, "Latest articles")
|
||||||
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn)" title="Atom feed">@icon!("rss")</a></small>
|
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn.to_string())" title="Atom feed">@icon!("rss")</a></small>
|
||||||
</h2>
|
</h2>
|
||||||
@if posts.is_empty() {
|
@if posts.is_empty() {
|
||||||
<p dir="auto">@i18n!(ctx.1, "No posts to see here yet.")</p>
|
<p dir="auto">@i18n!(ctx.1, "No posts to see here yet.")</p>
|
||||||
|
@ -12,10 +12,10 @@
|
|||||||
@(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors)
|
@(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors)
|
||||||
|
|
||||||
@:base(ctx, i18n!(ctx.1, "Edit \"{}\""; &blog.title), {}, {
|
@:base(ctx, i18n!(ctx.1, "Edit \"{}\""; &blog.title), {}, {
|
||||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)">@blog.title</a>
|
||||||
}, {
|
}, {
|
||||||
<h1>@i18n!(ctx.1, "Edit \"{}\""; &blog.title)</h1>
|
<h1>@i18n!(ctx.1, "Edit \"{}\""; &blog.title)</h1>
|
||||||
<form method="post" action="@uri!(blogs::update: name = &blog.fqn)">
|
<form method="post" action="@uri!(blogs::update: name = &blog.fqn.to_string())">
|
||||||
<!-- Rocket hack to use various HTTP methods -->
|
<!-- Rocket hack to use various HTTP methods -->
|
||||||
<input type=hidden name="_method" value="put">
|
<input type=hidden name="_method" value="put">
|
||||||
|
|
||||||
@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
<h2>@i18n!(ctx.1, "Danger zone")</h2>
|
<h2>@i18n!(ctx.1, "Danger zone")</h2>
|
||||||
<p>@i18n!(ctx.1, "Be very careful, any action taken here can't be reversed.")</p>
|
<p>@i18n!(ctx.1, "Be very careful, any action taken here can't be reversed.")</p>
|
||||||
<form method="post" action="@uri!(blogs::delete: name = &blog.fqn)" onsubmit="return confirm('@i18n!(ctx.1, "Are you sure that you want to permanently delete this blog?")')">
|
<form method="post" action="@uri!(blogs::delete: name = &blog.fqn.to_string())" onsubmit="return confirm('@i18n!(ctx.1, "Are you sure that you want to permanently delete this blog?")')">
|
||||||
<input type="submit" class="inline-block button destructive" value="@i18n!(ctx.1, "Permanently delete this blog")">
|
<input type="submit" class="inline-block button destructive" value="@i18n!(ctx.1, "Permanently delete this blog")">
|
||||||
</form>
|
</form>
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@use plume_models::instance::Instance;
|
@use plume_models::instance::Instance;
|
||||||
@use validator::ValidationErrors;
|
@use validator::ValidationErrors;
|
||||||
@use crate::templates::{base, instance::admin_header};
|
@use crate::templates::base;
|
||||||
@use crate::template_utils::*;
|
@use crate::template_utils::*;
|
||||||
@use crate::routes::instance::InstanceSettingsForm;
|
@use crate::routes::instance::InstanceSettingsForm;
|
||||||
@use crate::routes::*;
|
@use crate::routes::*;
|
||||||
@ -8,7 +8,15 @@
|
|||||||
@(ctx: BaseContext, instance: Instance, form: InstanceSettingsForm, errors: ValidationErrors)
|
@(ctx: BaseContext, instance: Instance, form: InstanceSettingsForm, errors: ValidationErrors)
|
||||||
|
|
||||||
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name.clone()), {}, {}, {
|
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name.clone()), {}, {}, {
|
||||||
@:admin_header(ctx, "Administration", 1)
|
<h1>@i18n!(ctx.1, "Administration")</h1>
|
||||||
|
|
||||||
|
@tabs(&[
|
||||||
|
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), true),
|
||||||
|
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false),
|
||||||
|
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false),
|
||||||
|
(&uri!(instance::admin_email_blocklist: page=_).to_string(), i18n!(ctx.1, "Email blocklist"), false)
|
||||||
|
])
|
||||||
|
|
||||||
<form method="post" action="@uri!(instance::update_settings)">
|
<form method="post" action="@uri!(instance::update_settings)">
|
||||||
@(Input::new("name", i18n!(ctx.1, "Name"))
|
@(Input::new("name", i18n!(ctx.1, "Name"))
|
||||||
.default(&form.name)
|
.default(&form.name)
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
@use crate::template_utils::*;
|
|
||||||
@use crate::routes::*;
|
|
||||||
|
|
||||||
@(ctx: BaseContext, title: &str, selected_tab: u8)
|
|
||||||
|
|
||||||
<h1>@i18n!(ctx.1, title)</h1>
|
|
||||||
|
|
||||||
@if ctx.2.clone().map(|u| u.is_admin()).unwrap_or(false) {
|
|
||||||
@tabs(&[
|
|
||||||
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), selected_tab == 1),
|
|
||||||
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), selected_tab == 2),
|
|
||||||
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), selected_tab == 3),
|
|
||||||
(&uri!(instance::admin_email_blocklist: page=_).to_string(), i18n!(ctx.1, "Email blocklist"), selected_tab == 4)
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
@tabs(&[
|
|
||||||
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), selected_tab == 2),
|
|
||||||
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), selected_tab == 3),
|
|
||||||
(&uri!(instance::admin_email_blocklist: page=_).to_string(), i18n!(ctx.1, "Email blocklist"), selected_tab == 4)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
|||||||
@use crate::templates::{base, instance::admin_header};
|
@use crate::templates::base;
|
||||||
@use crate::template_utils::*;
|
@use crate::template_utils::*;
|
||||||
|
@use crate::routes::*;
|
||||||
|
|
||||||
@(ctx: BaseContext)
|
@(ctx: BaseContext)
|
||||||
|
|
||||||
@:base(ctx, i18n!(ctx.1, "Moderation"), {}, {}, {
|
@:base(ctx, i18n!(ctx.1, "Moderation"), {}, {}, {
|
||||||
@:admin_header(ctx, "Moderation", 0)
|
<h1>@i18n!(ctx.1, "Moderation")</h1>
|
||||||
|
|
||||||
|
@tabs(&[
|
||||||
|
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Home"), true),
|
||||||
|
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false),
|
||||||
|
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false),
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
@use plume_models::blocklisted_emails::BlocklistedEmail;
|
@use plume_models::blocklisted_emails::BlocklistedEmail;
|
||||||
@use crate::templates::{base, instance::admin_header};
|
@use crate::templates::base;
|
||||||
@use crate::template_utils::*;
|
@use crate::template_utils::*;
|
||||||
@use crate::routes::*;
|
@use crate::routes::*;
|
||||||
|
|
||||||
@(ctx:BaseContext, emails: Vec<BlocklistedEmail>, page:i32, n_pages:i32)
|
@(ctx:BaseContext, emails: Vec<BlocklistedEmail>, page:i32, n_pages:i32)
|
||||||
@:base(ctx, i18n!(ctx.1, "Blocklisted Emails"), {}, {}, {
|
@:base(ctx, i18n!(ctx.1, "Blocklisted Emails"), {}, {}, {
|
||||||
@:admin_header(ctx, "Blocklisted Emails", 4)
|
<h1>@i18n!(ctx.1,"Blocklisted Emails")</h1>
|
||||||
|
@tabs(&[
|
||||||
|
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false),
|
||||||
|
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false),
|
||||||
|
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false),
|
||||||
|
(&uri!(instance::admin_email_blocklist:page=_).to_string(), i18n!(ctx.1, "Email blocklist"), true),
|
||||||
|
])
|
||||||
<form method="post" action="@uri!(instance::add_email_blocklist)">
|
<form method="post" action="@uri!(instance::add_email_blocklist)">
|
||||||
@(Input::new("email_address", i18n!(ctx.1, "Email address"))
|
@(Input::new("email_address", i18n!(ctx.1, "Email address"))
|
||||||
.details(i18n!(ctx.1, "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com"))
|
.details(i18n!(ctx.1, "The email address you wish to block. In order to block domains, you can use globbing syntax, for example '*@example.com' blocks all addresses from example.com"))
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
@use plume_models::instance::Instance;
|
@use plume_models::instance::Instance;
|
||||||
@use crate::templates::{base, instance::admin_header};
|
@use crate::templates::base;
|
||||||
@use crate::template_utils::*;
|
@use crate::template_utils::*;
|
||||||
@use crate::routes::*;
|
@use crate::routes::*;
|
||||||
|
|
||||||
@(ctx: BaseContext, instance: Instance, instances: Vec<Instance>, page: i32, n_pages: i32)
|
@(ctx: BaseContext, instance: Instance, instances: Vec<Instance>, page: i32, n_pages: i32)
|
||||||
|
|
||||||
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name), {}, {}, {
|
@:base(ctx, i18n!(ctx.1, "Administration of {0}"; instance.name), {}, {}, {
|
||||||
@:admin_header(ctx, "Instances", 2))
|
<h1>@i18n!(ctx.1, "Instances")</h1>
|
||||||
|
|
||||||
|
@tabs(&[
|
||||||
|
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false),
|
||||||
|
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), true),
|
||||||
|
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), false),
|
||||||
|
(&uri!(instance::admin_email_blocklist:page=_).to_string(), i18n!(ctx.1, "Email blocklist"), false),
|
||||||
|
])
|
||||||
|
|
||||||
<div class="list">
|
<div class="list">
|
||||||
@for instance in instances {
|
@for instance in instances {
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
@use plume_models::users::User;
|
@use plume_models::users::User;
|
||||||
@use crate::templates::{base, instance::admin_header};
|
@use crate::templates::base;
|
||||||
@use crate::template_utils::*;
|
@use crate::template_utils::*;
|
||||||
@use crate::routes::*;
|
@use crate::routes::*;
|
||||||
|
|
||||||
@(ctx: BaseContext, users: Vec<User>, user: Option<&str>, page: i32, n_pages: i32)
|
@(ctx: BaseContext, users: Vec<User>, page: i32, n_pages: i32)
|
||||||
|
|
||||||
@:base(ctx, i18n!(ctx.1, "Users"), {}, {}, {
|
@:base(ctx, i18n!(ctx.1, "Users"), {}, {}, {
|
||||||
@:admin_header(ctx, "Users", 3))
|
<h1>@i18n!(ctx.1, "Users")</h1>
|
||||||
|
|
||||||
<form method="get" action="@uri!(instance::admin_search_users: page = _, user = user.unwrap_or_default())">
|
@tabs(&[
|
||||||
<header>
|
(&uri!(instance::admin).to_string(), i18n!(ctx.1, "Configuration"), false),
|
||||||
<input type="search" name="user" value="@user.unwrap_or_default()">
|
(&uri!(instance::admin_instances: page = _).to_string(), i18n!(ctx.1, "Instances"), false),
|
||||||
<input type="submit" value="@i18n!(ctx.1, "Search users")">
|
(&uri!(instance::admin_users: page = _).to_string(), i18n!(ctx.1, "Users"), true),
|
||||||
</header>
|
(&uri!(instance::admin_email_blocklist: page=_).to_string(), i18n!(ctx.1, "Email blocklist"), false)
|
||||||
</form>
|
])
|
||||||
|
|
||||||
<form method="post" action="@uri!(instance::edit_users)">
|
<form method="post" action="@uri!(instance::edit_users)">
|
||||||
<header>
|
<header>
|
||||||
@ -46,9 +46,5 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@if user.is_some() {
|
@paginate(ctx.1, page, n_pages)
|
||||||
@paginate_param(ctx.1, page, n_pages, Some(format!("user={}", encode_query_param(user.unwrap_or_default()))))
|
|
||||||
} else {
|
|
||||||
@paginate(ctx.1, page, n_pages)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
@ -6,19 +6,19 @@
|
|||||||
|
|
||||||
<div class="card h-entry">
|
<div class="card h-entry">
|
||||||
@if article.cover_id.is_some() {
|
@if article.cover_id.is_some() {
|
||||||
<a class="cover-link" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
|
<a class="cover-link" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug, responding_to = _)">
|
||||||
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
<header dir="auto">
|
<header dir="auto">
|
||||||
<h3 class="p-name">
|
<h3 class="p-name">
|
||||||
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
|
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug, responding_to = _)">
|
||||||
@article.title
|
@article.title
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
|
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
<a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0).to_string(), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
@if article.published {
|
@if article.published {
|
||||||
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
||||||
}
|
}
|
||||||
⋅ <a href="@uri!(blogs::details: name = &article.get_blog_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
⋅ <a href="@uri!(blogs::details: name = &article.get_blog_fqn(ctx.0).to_string(), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
||||||
⋅
|
⋅
|
||||||
</div>
|
</div>
|
||||||
@if !article.published {
|
@if !article.published {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
@if article.cover_id.is_some() {
|
@if article.cover_id.is_some() {
|
||||||
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
|
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
|
||||||
}
|
}
|
||||||
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn, slug = &article.slug, responding_to = _)"/>
|
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn.to_string(), slug = &article.slug, responding_to = _)"/>
|
||||||
<meta property="og:description" content="@article.subtitle"/>
|
<meta property="og:description" content="@article.subtitle"/>
|
||||||
<link rel="canonical" href="@article.ap_url"/>
|
<link rel="canonical" href="@article.ap_url"/>
|
||||||
|
|
||||||
@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
<a href="@uri!(blogs::details: name = &blog.fqn.to_string(), page = _)">@blog.title</a>
|
||||||
}, {
|
}, {
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<header
|
<header
|
||||||
@ -78,7 +78,7 @@
|
|||||||
</section>
|
</section>
|
||||||
@if ctx.2.is_some() {
|
@if ctx.2.is_some() {
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
<form id="likes" class="likes" action="@uri!(likes::create: blog = &blog.fqn, slug = &article.slug)#likes" method="POST">
|
<form id="likes" class="likes" action="@uri!(likes::create: blog = &blog.fqn.to_string(), slug = &article.slug)#likes" method="POST">
|
||||||
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
|
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
|
||||||
@n_likes
|
@n_likes
|
||||||
</p>
|
</p>
|
||||||
@ -89,7 +89,7 @@
|
|||||||
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
|
<button type="submit" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</button>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
<form id="reshares" class="reshares" action="@uri!(reshares::create: blog = &blog.fqn, slug = &article.slug)#reshares" method="POST">
|
<form id="reshares" class="reshares" action="@uri!(reshares::create: blog = &blog.fqn.to_string(), slug = &article.slug)#reshares" method="POST">
|
||||||
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
|
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
|
||||||
@n_reshares
|
@n_reshares
|
||||||
</p>
|
</p>
|
||||||
@ -104,7 +104,7 @@
|
|||||||
} else {
|
} else {
|
||||||
<p class="center">@Html(i18n!(ctx.1, "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article";
|
<p class="center">@Html(i18n!(ctx.1, "{0}Log in{1}, or {2}use your Fediverse account{3} to interact with this article";
|
||||||
format!("<a href='{}'>", escape(&uri!(session::new: m = _).to_string())), "</a>",
|
format!("<a href='{}'>", escape(&uri!(session::new: m = _).to_string())), "</a>",
|
||||||
format!("<a href='{}'>", escape(&uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug).to_string())), "</a>"
|
format!("<a href='{}'>", escape(&uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug).to_string())), "</a>"
|
||||||
))
|
))
|
||||||
</p>
|
</p>
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
@ -112,19 +112,19 @@
|
|||||||
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
|
<p aria-label="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)" title="@i18n!(ctx.1, "One like", "{0} likes"; n_likes)">
|
||||||
@n_likes
|
@n_likes
|
||||||
</p>
|
</p>
|
||||||
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug)" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
|
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug)" class="action">@icon!("heart") @i18n!(ctx.1, "Add yours")</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="reshares" class="reshares">
|
<div id="reshares" class="reshares">
|
||||||
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
|
<p aria-label="@i18n!(ctx.1, "One boost", "{0} boost"; n_reshares)" title="@i18n!(ctx.1, "One boost", "{0} boosts"; n_reshares)">
|
||||||
@n_reshares
|
@n_reshares
|
||||||
</p>
|
</p>
|
||||||
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn, slug = &article.slug)" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
|
<a href="@uri!(posts::remote_interact: blog_name = &blog.fqn.to_string(), slug = &article.slug)" class="action">@icon!("repeat") @i18n!(ctx.1, "Boost")</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
<section class="banner">
|
<section class="banner">
|
||||||
<div class="flex wrap p-author h-card user" dir="auto">
|
<div class="flex p-author h-card user" dir="auto">
|
||||||
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
|
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<h2 class="p-name">
|
<h2 class="p-name">
|
||||||
@ -144,7 +144,7 @@
|
|||||||
<h2>@i18n!(ctx.1, "Comments")</h2>
|
<h2>@i18n!(ctx.1, "Comments")</h2>
|
||||||
|
|
||||||
@if ctx.2.is_some() {
|
@if ctx.2.is_some() {
|
||||||
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn, slug = &article.slug)#comments">
|
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn.to_string(), slug = &article.slug)#comments">
|
||||||
@(Input::new("warning", i18n!(ctx.1, "Content warning"))
|
@(Input::new("warning", i18n!(ctx.1, "Content warning"))
|
||||||
.default(&comment_form.warning)
|
.default(&comment_form.warning)
|
||||||
.error(&comment_errors)
|
.error(&comment_errors)
|
||||||
@ -162,7 +162,7 @@
|
|||||||
|
|
||||||
@if !comments.is_empty() {
|
@if !comments.is_empty() {
|
||||||
@for comm in comments {
|
@for comm in comments {
|
||||||
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn, &article.slug)
|
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn.to_string(), &article.slug)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
<p class="center" dir="auto">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
<p class="center" dir="auto">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
||||||
@ -173,7 +173,7 @@
|
|||||||
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
|
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
|
||||||
<aside class="bottom-bar">
|
<aside class="bottom-bar">
|
||||||
<div>
|
<div>
|
||||||
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = &blog.fqn, slug = &article.slug)">
|
<form class="inline" method="post" action="@uri!(posts::delete: blog_name = &blog.fqn.to_string(), slug = &article.slug)">
|
||||||
<input class="button destructive" onclick="return confirm('@i18n!(ctx.1, "Are you sure?")')" type="submit" value="@i18n!(ctx.1, "Delete")">
|
<input class="button destructive" onclick="return confirm('@i18n!(ctx.1, "Are you sure?")')" type="submit" value="@i18n!(ctx.1, "Delete")">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -186,9 +186,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@if !article.published {
|
@if !article.published {
|
||||||
<a class="button secondary" href="@uri!(posts::edit: blog = &blog.fqn, slug = &article.slug)">@i18n!(ctx.1, "Publish")</a>
|
<a class="button secondary" href="@uri!(posts::edit: blog = &blog.fqn.to_string(), slug = &article.slug)">@i18n!(ctx.1, "Publish")</a>
|
||||||
}
|
}
|
||||||
<a class="button" href="@uri!(posts::edit: blog = &blog.fqn, slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
<a class="button" href="@uri!(posts::edit: blog = &blog.fqn.to_string(), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user