Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

83 changed files with 1340 additions and 3165 deletions

View file

@ -10,7 +10,7 @@ executors:
type: boolean
default: false
docker:
- image: plumeorg/plume-buildenv:v0.8.0
- image: plumeorg/plume-buildenv:v0.7.0
- image: <<#parameters.postgres>>cimg/postgres:14.2<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
environment:
POSTGRES_USER: postgres
@ -63,7 +63,7 @@ commands:
type: boolean
default: false
steps:
- run: rustup component add clippy --toolchain nightly-2022-07-19-x86_64-unknown-linux-gnu
- run: rustup component add clippy --toolchain nightly-2022-01-27-x86_64-unknown-linux-gnu
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
run_with_coverage:
@ -112,7 +112,7 @@ jobs:
name: default
steps:
- restore_env
- run: rustup component add rustfmt --toolchain nightly-2022-07-19-x86_64-unknown-linux-gnu
- run: rustup component add rustfmt --toolchain nightly-2022-01-27-x86_64-unknown-linux-gnu
- run: cargo fmt --all -- --check
clippy:

View file

@ -1,4 +1,4 @@
FROM rust:1
FROM rust:1-buster
ENV PATH="/root/.cargo/bin:${PATH}"
#install native/circleci/build dependancies
@ -14,7 +14,6 @@ RUN apt update &&\
#stick rust environment
COPY rust-toolchain ./
RUN rustup component add rustfmt clippy
#compile some deps
RUN cargo install wasm-pack &&\

View file

@ -1 +1 @@
nightly-2022-07-19
nightly-2022-01-27

View file

@ -3,5 +3,3 @@ data
Dockerfile
docker-compose.yml
.env
target
data

1
.envrc
View file

@ -1 +0,0 @@
use flake

View file

@ -11,20 +11,20 @@ jobs:
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v3
uses: docker/build-push-action@v2
with:
push: true
tags: plumeorg/plume:latest

View file

@ -11,10 +11,10 @@ jobs:
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v1
-
name: Docker meta
id: meta
@ -23,14 +23,14 @@ jobs:
images: plumeorg/plume
-
name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v3
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}

1
.gitignore vendored
View file

@ -20,4 +20,3 @@ search_index
__pycache__
.vscode/
*-journal
.direnv/

View file

@ -4,25 +4,6 @@
## [Unreleased] - ReleaseDate
### Added
- Add 'My feed' to i18n timeline name (#1084)
- Bidirectional support for user page header (#1092)
### Changed
- Use blog title as slug (#1094, #1126, #1127)
- Bump Rust to nightly 2022-07-19 (#1119)
### Fixed
- Malfunction while creating a blog post in Persian (#1116)
- Email block list is ignored when email sign-up (#1122)
- Bug that some Activity Sytreams properties are not parsed properly (#1129)
- Allow empty avatar for remote users (#1129)
- Percent encode blog FQN for federation interoperability (#1129)
- The same to `preferredUsername` (#1129)
## [[0.7.2]] - 2022-05-11
### Added

1763
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ repository = "https://github.com/Plume-org/Plume"
edition = "2018"
[dependencies]
atom_syndication = "0.12.0"
atom_syndication = "0.11.0"
clap = "2.33"
dotenv = "0.15.0"
gettext = "0.4.0"
@ -19,7 +19,7 @@ rocket = "0.4.11"
rocket_contrib = { version = "0.4.11", features = ["json"] }
rocket_i18n = "0.4.1"
scheduled-thread-pool = "0.2.6"
serde = "1.0.137"
serde = "1.0"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
validator = { version = "0.15", features = ["derive"] }
@ -27,7 +27,7 @@ webfinger = "0.4.1"
tracing = "0.1.35"
tracing-subscriber = "0.3.10"
riker = "0.4.2"
activitystreams = "=0.7.0-alpha.20"
activitystreams = "0.7.0-alpha.18"
[[bin]]
name = "plume"
@ -60,21 +60,20 @@ path = "plume-common"
path = "plume-models"
[dependencies.rocket_csrf]
git = "https://git.joinplu.me/plume/rocket_csrf"
rev = "0.1.2"
git = "https://github.com/fdb-hiroshima/rocket_csrf"
rev = "29910f2829e7e590a540da3804336577b48c7b31"
[build-dependencies]
ructe = "0.15.0"
rsass = "0.26"
ructe = "0.14.0"
rsass = "0.25"
[features]
default = ["postgres", "s3"]
default = ["postgres"]
postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
debug-mailer = []
test = []
search-lindera = ["plume-models/search-lindera"]
s3 = ["plume-models/s3"]
[workspace]
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]

View file

@ -1,4 +1,4 @@
FROM rust:1 as builder
FROM rust:1-buster as builder
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
@ -18,15 +18,17 @@ COPY script/wasm-deps.sh .
RUN chmod a+x ./wasm-deps.sh && sleep 1 && ./wasm-deps.sh
WORKDIR /app
COPY Cargo.toml Cargo.lock rust-toolchain ./
RUN cargo install wasm-pack
COPY . .
RUN cargo install wasm-pack
RUN chmod a+x ./script/plume-front.sh && sleep 1 && ./script/plume-front.sh
RUN cargo install --path ./ --force --no-default-features --features postgres
RUN cargo install --path plume-cli --force --no-default-features --features postgres
RUN cargo clean
FROM debian:stable-slim
FROM debian:buster-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \

View file

@ -98,7 +98,7 @@ main article {
}
blockquote {
border-inline-start: 5px solid $lightpurple;
border-inline-start: 5px solid $gray;
margin: 1em auto;
padding: 0em 2em;
}
@ -228,7 +228,7 @@ main .article-meta {
fill: currentColor;
}
.action.liked:hover svg.feather {
background: transparentize($red, 0.75);
background: transparentize($red, 0.75)
color: $red;
}
}
@ -252,7 +252,7 @@ main .article-meta {
background: $primary;
}
.action.reshared:hover svg.feather {
background: transparentize($primary, 0.75);
background: transparentize($primary, 0.75)
color: $primary;
}
}

View file

@ -37,10 +37,6 @@ body > header {
flex-direction: row;
align-items: center;
&.right-nav {
overflow-x: hidden;
}
hr {
height: 100%;
width: 0.2em;

View file

@ -1,10 +0,0 @@
/* tables */
table, td, th, tr {
border: 1px dotted $lightpurple;
}
table {
border-collapse: collapse;
}
tr:nth-child(even){background-color: $gray;}

View file

@ -12,4 +12,3 @@
@import "header";
@import "article";
@import "forms";
@import "tables";

View file

@ -12,4 +12,3 @@
@import "header";
@import "article";
@import "forms";
@import "tables";

116
flake.lock generated
View file

@ -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
}

View file

@ -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;
};
});
}

View file

@ -5,5 +5,5 @@ authors = ["Plume contributors"]
edition = "2018"
[dependencies]
serde = "1.0.137"
serde = "1.0"
serde_derive = "1.0"

View file

@ -24,4 +24,3 @@ path = "../plume-models"
postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
search-lindera = ["plume-models/search-lindera"]
s3 = ["plume-models/s3"]

View file

@ -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();
}
}
}
}

View file

@ -4,10 +4,8 @@ use plume_models::{instance::Instance, Connection as Conn, CONFIG};
use std::io::{self, prelude::*};
mod instance;
mod list;
mod migration;
mod search;
mod timeline;
mod users;
fn main() {
@ -18,8 +16,6 @@ fn main() {
.subcommand(instance::command())
.subcommand(migration::command())
.subcommand(search::command())
.subcommand(timeline::command())
.subcommand(list::command())
.subcommand(users::command());
let matches = app.clone().get_matches();
@ -41,10 +37,6 @@ fn main() {
("search", Some(args)) => {
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::run(args, &conn.expect("Couldn't connect to the database."))
}

View file

@ -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);
}

View file

@ -11,7 +11,7 @@ hex = "0.4"
openssl = "0.10.40"
rocket = "0.4.11"
reqwest = { version = "0.11.11", features = ["blocking", "json", "socks"] }
serde = "1.0.137"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0.81"
shrinkwraprs = "0.3.0"
@ -19,12 +19,12 @@ syntect = "4.5.0"
regex-syntax = { version = "0.6.26", default-features = false, features = ["unicode-perl"] }
tracing = "0.1.35"
askama_escape = "0.10.3"
activitystreams = "=0.7.0-alpha.20"
activitystreams = "0.7.0-alpha.18"
activitystreams-ext = "0.1.0-alpha.2"
url = "2.2.2"
flume = "0.10.13"
tokio = { version = "1.19.2", features = ["full"] }
futures = "0.3.25"
futures = "0.3.21"
[dependencies.chrono]
features = ["serde"]

View file

@ -561,7 +561,7 @@ mod tests {
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
static MY_SIGNER: Lazy<MySigner> = Lazy::new(MySigner::new);
static MY_SIGNER: Lazy<MySigner> = Lazy::new(|| MySigner::new());
struct MySigner {
public_key: String,
@ -596,7 +596,7 @@ mod tests {
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(signature).map_err(|_| SignError())
verifier.verify(&signature).map_err(|_| SignError())
}
}
@ -782,7 +782,7 @@ mod tests {
.done();
assert!(res.is_err());
let res: Result<(), ()> = Inbox::handle(&(), act)
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
.with::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None)
.done();

View file

@ -518,8 +518,7 @@ mod tests {
use super::*;
use activitystreams::{
activity::{ActorAndObjectRef, Create},
object::{kind::ArticleType, Image},
prelude::{ApActorExt, BaseExt, ExtendsExt, ObjectExt},
object::kind::ArticleType,
};
use assert_json_diff::assert_json_eq;
use serde_json::{from_str, json, to_value};
@ -593,7 +592,7 @@ mod tests {
}
#[test]
fn se_custom_group() {
fn de_custom_group() {
let group = CustomGroup::new(
ApActor::new("https://example.com/inbox".parse().unwrap(), Group::new()),
ApSignature {
@ -626,71 +625,6 @@ mod tests {
assert_eq!(to_value(group).unwrap(), expected);
}
#[test]
fn de_custom_group() {
let value: CustomGroup = from_str(
r#"
{
"icon": {
"type": "Image"
},
"id": "https://plume01.localhost/~/Plume01%20Blog%202/",
"image": {
"type": "Image"
},
"inbox": "https://plume01.localhost/~/Plume01%20Blog%202/inbox",
"name": "Plume01 Blog 2",
"outbox": "https://plume01.localhost/~/Plume01%20Blog%202/outbox",
"preferredUsername": "Plume01 Blog 2",
"publicKey": {
"id": "https://plume01.localhost/~/Plume01%20Blog%202/#main-key",
"owner": "https://plume01.localhost/~/Plume01%20Blog%202/",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n"
},
"source": {
"content": "",
"mediaType": "text/markdown"
},
"summary": "",
"type": "Group"
}
"#
).unwrap();
let mut expected = CustomGroup::new(
ApActor::new("https://plume01.localhost/~/Plume01%20Blog%202/inbox".parse().unwrap(), Group::new()),
ApSignature {
public_key: PublicKey {
id: "https://plume01.localhost/~/Plume01%20Blog%202/#main-key".parse().unwrap(),
owner: "https://plume01.localhost/~/Plume01%20Blog%202/".parse().unwrap(),
public_key_pem: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPGtKkl/iMsNAyeVaJGz\noEz5PoNkjRnKK7G97MFvb4zw9zs5SpzWW7b/pKHa4dODcGDJXmkCJ1H5JWyguzN8\n2GNoFjtEOJHxEGwBHSYDsTmhuLNB0DKxMU2iu55g8iIiXhZiIW1FBNGs/Geaymvr\nh/TEtzdReN8wzloRR55kOVcU49xBkqx8cfDSk/lrrDLlpveHdqgaFnIvuw2vycK0\nxFzS3xlEUpzJk9kHxoR1uEAfZ+gCv26Sgo/HqOAhqSD5IU3QZC3kdkr/hwVqtr8U\nXGkGG6Mo1rgzhkYiCFkWrV2WoKkcEHD4nEzbgoZZ5MyuSoloxnyF3NiScqmqW+Yx\nkQIDAQAB\n-----END PUBLIC KEY-----\n".into(),
}
},
SourceProperty {
source: Source {
content: String::from(""),
media_type: String::from("text/markdown")
}
}
);
expected.set_icon(Image::new().into_any_base().unwrap());
expected.set_id(
"https://plume01.localhost/~/Plume01%20Blog%202/"
.parse()
.unwrap(),
);
expected.set_image(Image::new().into_any_base().unwrap());
expected.set_name("Plume01 Blog 2");
expected.set_outbox(
"https://plume01.localhost/~/Plume01%20Blog%202/outbox"
.parse()
.unwrap(),
);
expected.set_preferred_username("Plume01 Blog 2");
expected.set_summary("");
assert_json_eq!(value, expected);
}
#[test]
fn se_licensed_article() {
let object = ApObject::new(Article::new());

View file

@ -253,7 +253,7 @@ mod tests {
.unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap();
verifier.verify(signature).map_err(|_| Error())
verifier.verify(&signature).map_err(|_| Error())
}
}
@ -262,7 +262,7 @@ mod tests {
let signer = MySigner::new();
let headers = HeaderMap::new();
let result = signature(&signer, &headers, ("post", "/inbox", None)).unwrap();
let fields: Vec<&str> = result.to_str().unwrap().split(',').collect();
let fields: Vec<&str> = result.to_str().unwrap().split(",").collect();
assert_eq!(r#"headers="(request-target)""#, fields[2]);
let sign = &fields[3][11..(fields[3].len() - 1)];
assert!(signer.verify("post /inbox", sign.as_bytes()).is_ok());

View file

@ -119,7 +119,7 @@ impl Signable for serde_json::Value {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum SignatureValidity {
Invalid,
ValidNoDigest,

View file

@ -4,9 +4,6 @@ version = "0.7.2"
authors = ["Plume contributors"]
edition = "2018"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
[lib]
crate-type = ["cdylib"]
@ -15,7 +12,7 @@ gettext = "0.4.0"
gettext-macros = "0.6.1"
gettext-utils = "0.1.0"
lazy_static = "1.3"
serde = "1.0.137"
serde = "1.0"
serde_json = "1.0"
wasm-bindgen = "0.2.81"
js-sys = "0.3.58"

View file

@ -10,32 +10,31 @@ bcrypt = "0.12.1"
guid-create = "0.2"
itertools = "0.10.3"
lazy_static = "1.0"
ldap3 = "0.11.1"
ldap3 = "0.10.5"
migrations_internals= "1.4.0"
openssl = "0.10.40"
rocket = "0.4.11"
rocket_i18n = "0.4.1"
reqwest = "0.11.11"
scheduled-thread-pool = "0.2.6"
serde = "1.0.137"
rust-s3 = { version = "0.33.0", optional = true, features = ["blocking"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0.81"
tantivy = "0.13.3"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
whatlang = "0.16.2"
whatlang = "0.16.0"
shrinkwraprs = "0.3.0"
diesel-derive-newtype = "1.0.0"
glob = "0.3.1"
glob = "0.3.0"
lindera-tantivy = { version = "0.7.1", optional = true }
tracing = "0.1.35"
riker = "0.4.2"
once_cell = "1.12.0"
lettre = "0.9.6"
native-tls = "0.2.10"
activitystreams = "=0.7.0-alpha.20"
activitystreams = "0.7.0-alpha.18"
[dependencies.chrono]
features = ["serde"]
@ -62,4 +61,3 @@ diesel_migrations = "1.3.0"
postgres = ["diesel/postgres", "plume-macro/postgres" ]
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
search-lindera = ["lindera-tantivy"]
s3 = ["rust-s3"]

View file

@ -5,7 +5,7 @@ use rocket::{
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);
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.
pub struct Moderator(pub User);

View file

@ -103,7 +103,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
let conn = request
.guard::<DbConn>()
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
if let Ok(token) = ApiToken::find_by_value(&conn, val) {
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
return Outcome::Success(token);
}
}

View file

@ -126,9 +126,12 @@ pub(crate) mod tests {
.id,
various[1].id
);
assert!(BlocklistedEmail::matches_blocklist(&conn, no_match)
.unwrap()
.is_none());
assert_eq!(
BlocklistedEmail::matches_blocklist(&conn, no_match)
.unwrap()
.is_none(),
true
);
Ok(())
});
}

View file

@ -1,6 +1,6 @@
use crate::{
instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs, users::User,
Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
db_conn::DbConn, instance::*, medias::Media, posts::Post, safe_string::SafeString,
schema::blogs, users::User, Connection, Error, PlumeRocket, Result, CONFIG, ITEMS_PER_PAGE,
};
use activitystreams::{
actor::{ApActor, ApActorExt, AsApActor, Group},
@ -18,13 +18,10 @@ use openssl::{
rsa::Rsa,
sign::{Signer, Verifier},
};
use plume_common::{
activity_pub::{
inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source,
SourceProperty, ToAsString, ToAsUri,
},
utils::iri_percent_encode_seg,
use plume_common::activity_pub::{
inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source, SourceProperty,
ToAsString, ToAsUri,
};
use webfinger::*;
@ -86,13 +83,9 @@ impl Blog {
if inserted.fqn.is_empty() {
if instance.local {
inserted.fqn = iri_percent_encode_seg(&inserted.actor_id);
inserted.fqn = inserted.actor_id.clone();
} else {
inserted.fqn = format!(
"{}@{}",
iri_percent_encode_seg(&inserted.actor_id),
instance.public_domain
);
inserted.fqn = format!("{}@{}", inserted.actor_id, instance.public_domain);
}
}
@ -142,10 +135,10 @@ impl Blog {
.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
.filter(blogs::fqn.eq(fqn))
.first(conn)
.first(&**conn)
.optional()?;
if let Some(from_db) = from_db {
Ok(from_db)
@ -154,7 +147,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)?
.links
.into_iter()
@ -173,7 +166,7 @@ impl Blog {
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
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.actor_id.clone());
blog.set_name(self.title.clone());
blog.set_outbox(self.outbox_url.parse()?);
blog.set_summary(self.summary_html.to_string());
@ -372,15 +365,15 @@ impl IntoId for Blog {
}
}
impl FromId<Connection> for Blog {
impl FromId<DbConn> for Blog {
type Error = Error;
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)
}
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 actor = acct.ap_actor_ref();
let name = actor
@ -388,7 +381,6 @@ impl FromId<Connection> for Blog {
.ok_or(Error::MissingApProperty)?
.to_string();
if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
tracing::error!("preferredUsername includes invalid character(s): {}", &name);
return Err(Error::InvalidValue);
}
(
@ -668,7 +660,7 @@ pub(crate) mod tests {
.unwrap()
.id,
);
let _: Blog = blog1.save_changes(conn).unwrap();
let _: Blog = blog1.save_changes(&*conn).unwrap();
(users, vec![blog1, blog2, blog3])
}
@ -677,10 +669,10 @@ pub(crate) mod tests {
fn get_instance() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let blog = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@ -692,7 +684,7 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(
blog.get_instance(conn).unwrap().id,
blog.get_instance(&conn).unwrap().id,
Instance::get_local().unwrap().id
);
// TODO add tests for remote instance
@ -704,10 +696,10 @@ pub(crate) mod tests {
fn authors() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(conn);
let (user, _) = fill_database(&conn);
let b1 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@ -718,7 +710,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@ -731,7 +723,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2];
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
@ -741,7 +733,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@ -751,7 +743,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@ -761,39 +753,39 @@ pub(crate) mod tests {
.unwrap();
assert!(blog[0]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[0].id));
assert!(blog[0]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[1].id));
assert!(blog[1]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[0].id));
assert!(!blog[1]
.list_authors(conn)
.list_authors(&conn)
.unwrap()
.iter()
.any(|a| a.id == user[1].id));
assert!(Blog::find_for_author(conn, &user[0])
assert!(Blog::find_for_author(&conn, &user[0])
.unwrap()
.iter()
.any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(conn, &user[1])
assert!(Blog::find_for_author(&conn, &user[1])
.unwrap()
.iter()
.any(|b| b.id == blog[0].id));
assert!(Blog::find_for_author(conn, &user[0])
assert!(Blog::find_for_author(&conn, &user[0])
.unwrap()
.iter()
.any(|b| b.id == blog[1].id));
assert!(!Blog::find_for_author(conn, &user[1])
assert!(!Blog::find_for_author(&conn, &user[1])
.unwrap()
.iter()
.any(|b| b.id == blog[1].id));
@ -805,10 +797,10 @@ pub(crate) mod tests {
fn find_local() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let blog = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@ -819,7 +811,7 @@ pub(crate) mod tests {
)
.unwrap();
assert_eq!(Blog::find_by_fqn(conn, "SomeName").unwrap().id, blog.id);
assert_eq!(Blog::find_by_fqn(&conn, "SomeName").unwrap().id, blog.id);
Ok(())
})
}
@ -828,10 +820,10 @@ pub(crate) mod tests {
fn get_fqn() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let blog = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@ -851,10 +843,10 @@ pub(crate) mod tests {
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_, blogs) = fill_database(conn);
let (_, blogs) = fill_database(&conn);
blogs[0].delete(conn).unwrap();
assert!(Blog::get(conn, blogs[0].id).is_err());
blogs[0].delete(&conn).unwrap();
assert!(Blog::get(&conn, blogs[0].id).is_err());
Ok(())
})
}
@ -863,10 +855,10 @@ pub(crate) mod tests {
fn delete_via_user() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (user, _) = fill_database(conn);
let (user, _) = fill_database(&conn);
let b1 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"SomeName".to_owned(),
"Some name".to_owned(),
@ -877,7 +869,7 @@ pub(crate) mod tests {
)
.unwrap();
let b2 = Blog::insert(
conn,
&conn,
NewBlog::new_local(
"Blog".to_owned(),
"Blog".to_owned(),
@ -890,7 +882,7 @@ pub(crate) mod tests {
let blog = vec![b1, b2];
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[0].id,
@ -900,7 +892,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[0].id,
author_id: user[1].id,
@ -910,7 +902,7 @@ pub(crate) mod tests {
.unwrap();
BlogAuthor::insert(
conn,
&conn,
NewBlogAuthor {
blog_id: blog[1].id,
author_id: user[0].id,
@ -919,11 +911,11 @@ pub(crate) mod tests {
)
.unwrap();
user[0].delete(conn).unwrap();
assert!(Blog::get(conn, blog[0].id).is_ok());
assert!(Blog::get(conn, blog[1].id).is_err());
user[1].delete(conn).unwrap();
assert!(Blog::get(conn, blog[0].id).is_err());
user[0].delete(&conn).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_ok());
assert!(Blog::get(&conn, blog[1].id).is_err());
user[1].delete(&conn).unwrap();
assert!(Blog::get(&conn, blog[0].id).is_err());
Ok(())
})
}
@ -932,10 +924,10 @@ pub(crate) mod tests {
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, mut blogs) = fill_database(conn);
let (users, mut blogs) = fill_database(&conn);
blogs[0].icon_id = Some(
Media::insert(
conn,
&conn,
NewMedia {
file_path: "aaa.png".into(),
alt_text: String::new(),
@ -951,7 +943,7 @@ pub(crate) mod tests {
);
blogs[0].banner_id = Some(
Media::insert(
conn,
&conn,
NewMedia {
file_path: "bbb.png".into(),
alt_text: String::new(),
@ -966,9 +958,9 @@ pub(crate) mod tests {
.id,
);
let _: Blog = blogs[0].save_changes(&**conn).unwrap();
let ap_repr = blogs[0].to_activity(conn).unwrap();
blogs[0].delete(conn).unwrap();
let blog = Blog::from_activity(conn, ap_repr).unwrap();
let ap_repr = blogs[0].to_activity(&conn).unwrap();
blogs[0].delete(&conn).unwrap();
let blog = Blog::from_activity(&conn, ap_repr).unwrap();
assert_eq!(blog.actor_id, blogs[0].actor_id);
assert_eq!(blog.title, blogs[0].title);
@ -980,8 +972,8 @@ pub(crate) mod tests {
assert_eq!(blog.public_key, blogs[0].public_key);
assert_eq!(blog.fqn, blogs[0].fqn);
assert_eq!(blog.summary_html, blogs[0].summary_html);
assert_eq!(blog.icon_url(conn), blogs[0].icon_url(conn));
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn));
assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn));
Ok(())
})
@ -991,7 +983,7 @@ pub(crate) mod tests {
fn to_activity() {
let conn = &db();
conn.test_transaction::<_, Error, _>(|| {
let (_users, blogs) = fill_database(conn);
let (_users, blogs) = fill_database(&conn);
let blog = &blogs[0];
let act = blog.to_activity(conn)?;

View file

@ -1,5 +1,6 @@
use crate::{
comment_seers::{CommentSeers, NewCommentSeers},
db_conn::DbConn,
instance::Instance,
medias::Media,
mentions::Mention,
@ -73,7 +74,6 @@ impl Comment {
});
get!(comments);
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);
pub fn get_author(&self, conn: &Connection) -> Result<User> {
@ -111,7 +111,7 @@ impl Comment {
.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 (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(),
@ -149,7 +149,7 @@ impl Comment {
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 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 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)
}
fn from_activity(conn: &Connection, note: Note) -> Result<Self> {
fn from_activity(conn: &DbConn, note: Note) -> Result<Self> {
let comm = {
let previous_url = note
.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 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
Ok(self)
}
}
impl AsObject<User, Delete, &Connection> for Comment {
impl AsObject<User, Delete, &DbConn> for Comment {
type Error = Error;
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 {
return Err(Error::Unauthorized);
}
@ -381,14 +381,14 @@ impl AsObject<User, Delete, &Connection> for Comment {
}
for n in Notification::find_for_comment(conn, &self)? {
n.delete(conn)?;
n.delete(&**conn)?;
}
diesel::update(comments::table)
.filter(comments::in_response_to_id.eq(self.id))
.set(comments::in_response_to_id.eq(self.in_response_to_id))
.execute(conn)?;
diesel::delete(&self).execute(conn)?;
.execute(&**conn)?;
diesel::delete(&self).execute(&**conn)?;
Ok(())
}
}
@ -423,7 +423,6 @@ impl CommentTree {
mod tests {
use super::*;
use crate::blogs::Blog;
use crate::db_conn::DbConn;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::safe_string::SafeString;
use crate::tests::{db, format_datetime};
@ -432,7 +431,7 @@ mod tests {
use serde_json::{json, to_value};
fn prepare_activity(conn: &DbConn) -> (Comment, Vec<Post>, Vec<User>, Vec<Blog>) {
let (posts, users, blogs) = fill_database(conn);
let (posts, users, blogs) = fill_database(&conn);
let comment = Comment::insert(
conn,
@ -458,8 +457,8 @@ mod tests {
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (original_comm, posts, users, _blogs) = prepare_activity(conn);
let act = original_comm.create_activity(conn).unwrap();
let (original_comm, posts, users, _blogs) = prepare_activity(&conn);
let act = original_comm.create_activity(&conn).unwrap();
assert_json_eq!(to_value(&act).unwrap(), json!({
"actor": "https://plu.me/@/admin/",
@ -501,7 +500,7 @@ mod tests {
},
)
.unwrap();
let reply_act = reply.create_activity(conn).unwrap();
let reply_act = reply.create_activity(&conn).unwrap();
assert_json_eq!(to_value(&reply_act).unwrap(), json!({
"actor": "https://plu.me/@/user/",
@ -523,12 +522,12 @@ mod tests {
}));
inbox(
conn,
serde_json::to_value(original_comm.build_delete(conn).unwrap()).unwrap(),
&conn,
serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(),
)
.unwrap();
match inbox(conn, to_value(act).unwrap()).unwrap() {
match inbox(&conn, to_value(act).unwrap()).unwrap() {
InboxResult::Commented(c) => {
// TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content);
assert_eq!(c.in_response_to_id, original_comm.in_response_to_id);

View file

@ -6,9 +6,6 @@ use rocket::Config as RocketConfig;
use std::collections::HashSet;
use std::env::{self, var};
#[cfg(feature = "s3")]
use s3::{Bucket, Region, creds::Credentials};
#[cfg(not(test))]
const DB_NAME: &str = "plume";
#[cfg(test)]
@ -30,23 +27,13 @@ pub struct Config {
pub mail: Option<MailConfig>,
pub ldap: Option<LdapConfig>,
pub proxy: Option<ProxyConfig>,
pub s3: Option<S3Config>,
}
impl Config {
pub fn proxy(&self) -> Option<&reqwest::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)]
pub enum InvalidRocketConfig {
Env,
@ -301,7 +288,11 @@ fn get_ldap_config() -> Option<LdapConfig> {
match (addr, base_dn) {
(Some(addr), Some(base_dn)) => {
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 mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
Some(LdapConfig {
@ -358,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! {
pub static ref CONFIG: Config = Config {
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
@ -487,6 +380,5 @@ lazy_static! {
mail: get_mail_config(),
ldap: get_ldap_config(),
proxy: get_proxy_config(),
s3: get_s3_config(),
};
}

View file

@ -69,8 +69,7 @@ pub(crate) mod tests {
impl CustomizeConnection<Connection, ConnError> for TestConnectionCustomizer {
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
PragmaForeignKey.on_acquire(conn)?;
conn.begin_test_transaction().unwrap();
Ok(())
Ok(conn.begin_test_transaction().unwrap())
}
}
}

View file

@ -1,5 +1,4 @@
use crate::{
blocklisted_emails::BlocklistedEmail,
db_conn::DbConn,
schema::email_signups,
users::{NewUser, Role, User},
@ -61,8 +60,6 @@ pub struct NewEmailSignup<'a> {
impl EmailSignup {
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
Self::ensure_email_not_blocked(conn, email)?;
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, email)?;
let _rows = Self::delete_existings_by_email(conn, email)?;
@ -93,8 +90,6 @@ impl EmailSignup {
}
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
Self::ensure_email_not_blocked(conn, &self.email)?;
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
if self.expired() {
@ -106,8 +101,6 @@ impl EmailSignup {
}
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
Self::ensure_email_not_blocked(conn, &self.email)?;
conn.transaction(|| {
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
let user = NewUser::new_local(
@ -129,14 +122,6 @@ impl EmailSignup {
Ok(())
}
fn ensure_email_not_blocked(conn: &DbConn, email: &str) -> Result<()> {
if let Some(x) = BlocklistedEmail::matches_blocklist(conn, email)? {
Err(Error::Blocklisted(x.notify_user, x.notification_text))
} else {
Ok(())
}
}
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
if User::email_used(conn, email)? {
let _rows = Self::delete_existings_by_email(conn, email)?;

View file

@ -1,6 +1,6 @@
use crate::{
ap_url, instance::Instance, notifications::*, schema::follows, users::User, Connection, Error,
Result, CONFIG,
ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Connection, Error, Result, CONFIG,
};
use activitystreams::{
activity::{Accept, ActorAndObjectRef, Follow as FollowAct, Undo},
@ -107,7 +107,12 @@ impl Follow {
res.notify(conn)?;
let accept = res.build_accept(from, target, follow)?;
broadcast(target, accept, vec![from.clone()], CONFIG.proxy().cloned());
broadcast(
&*target,
accept,
vec![from.clone()],
CONFIG.proxy().cloned(),
);
Ok(res)
}
@ -150,11 +155,11 @@ impl Follow {
}
}
impl AsObject<User, FollowAct, &Connection> for User {
impl AsObject<User, FollowAct, &DbConn> for User {
type Error = Error;
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,
// so we rebuilt it here
let follow = FollowAct::new(actor.ap_url.parse::<IriString>()?, id.parse::<IriString>()?);
@ -162,15 +167,15 @@ impl AsObject<User, FollowAct, &Connection> for User {
}
}
impl FromId<Connection> for Follow {
impl FromId<DbConn> for Follow {
type Error = Error;
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)
}
fn from_activity(conn: &Connection, follow: FollowAct) -> Result<Self> {
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id(
conn,
follow
@ -202,18 +207,18 @@ impl FromId<Connection> for Follow {
}
}
impl AsObject<User, Undo, &Connection> for Follow {
impl AsObject<User, Undo, &DbConn> for Follow {
type Error = Error;
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;
if self.follower_id == actor.id {
diesel::delete(&self).execute(conn)?;
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif).execute(conn)?;
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
@ -232,9 +237,7 @@ impl IntoId for Follow {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
db_conn::DbConn, tests::db, users::tests as user_tests, users::tests::fill_database,
};
use crate::{tests::db, users::tests as user_tests, users::tests::fill_database};
use assert_json_diff::assert_json_eq;
use diesel::Connection;
use serde_json::{json, to_value};

View file

@ -2,11 +2,12 @@ use activitystreams::activity::{Announce, Create, Delete, Follow, Like, Undo, Up
use crate::{
comments::Comment,
db_conn::DbConn,
follows, likes,
posts::{Post, PostUpdate},
reshares::Reshare,
users::User,
Connection, Error, CONFIG,
Error, CONFIG,
};
use plume_common::activity_pub::inbox::Inbox;
@ -45,7 +46,7 @@ impl_into_inbox_result! {
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)
.with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy())
@ -81,9 +82,9 @@ pub(crate) mod tests {
use crate::post_authors::*;
use crate::posts::*;
let (users, blogs) = blog_fill_db(conn);
let (users, blogs) = blog_fill_db(&conn);
let post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "testing".to_owned(),
@ -101,7 +102,7 @@ pub(crate) mod tests {
.unwrap();
PostAuthor::insert(
conn,
&conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
@ -189,7 +190,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@ -220,7 +221,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@ -248,7 +249,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@ -323,7 +324,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@ -361,7 +362,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))
@ -396,7 +397,7 @@ pub(crate) mod tests {
});
assert!(matches!(
super::inbox(&conn, act),
super::inbox(&conn, act.clone()),
Err(super::Error::Inbox(
box plume_common::activity_pub::inbox::InboxError::InvalidObject(_),
))

View file

@ -9,7 +9,7 @@ use crate::{
use chrono::NaiveDateTime;
use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl};
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;
#[derive(Clone, Identifiable, Queryable)]
@ -173,8 +173,8 @@ impl Instance {
"{instance}/{prefix}/{name}/{box_name}",
instance = self.public_domain,
prefix = prefix,
name = iri_percent_encode_seg(name),
box_name = iri_percent_encode_seg(box_name)
name = name,
box_name = box_name
))
}
@ -523,7 +523,7 @@ pub(crate) mod tests {
.unwrap();
let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.name, "NewName".to_owned());
assert!(!inst.open_registrations);
assert_eq!(inst.open_registrations, false);
assert_eq!(
inst.long_description.get(),
"[long_description](/with_link)"

View file

@ -69,8 +69,6 @@ pub enum Error {
Webfinger,
Expired,
UserAlreadyExists,
#[cfg(feature = "s3")]
S3(s3::error::S3Error),
}
impl From<bcrypt::BcryptError> for Error {
@ -172,13 +170,6 @@ impl From<request::Error> for Error {
}
}
#[cfg(feature = "s3")]
impl From<s3::error::S3Error> for Error {
fn from(err: s3::error::S3Error) -> Error {
Error::S3(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Adds a function to a model, that returns the first
@ -186,7 +177,7 @@ pub type Result<T> = std::result::Result<T, Error>;
///
/// Usage:
///
/// ```ignore
/// ```rust
/// impl Model {
/// find_by!(model_table, name_of_the_function, field1 as String, field2 as i32);
/// }
@ -210,7 +201,7 @@ macro_rules! find_by {
///
/// Usage:
///
/// ```ignore
/// ```rust
/// impl Model {
/// list_by!(model_table, name_of_the_function, field1 as String);
/// }
@ -234,7 +225,7 @@ macro_rules! list_by {
///
/// # Usage
///
/// ```ignore
/// ```rust
/// impl Model {
/// get!(model_table);
/// }
@ -257,7 +248,7 @@ macro_rules! get {
///
/// # Usage
///
/// ```ignore
/// ```rust
/// impl Model {
/// insert!(model_table, NewModelType);
/// }
@ -289,7 +280,7 @@ macro_rules! insert {
///
/// # Usage
///
/// ```ignore
/// ```rust
/// impl Model {
/// last!(model_table);
/// }
@ -363,7 +354,7 @@ mod tests {
};
}
pub fn db() -> db_conn::DbConn {
pub fn db<'a>() -> db_conn::DbConn {
db_conn::DbConn((*DB_POOL).get().unwrap())
}

View file

@ -1,6 +1,6 @@
use crate::{
instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, users::User,
Connection, Error, Result, CONFIG,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
users::User, Connection, Error, Result, CONFIG,
};
use activitystreams::{
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 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(
conn,
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 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)
}
fn from_activity(conn: &Connection, act: LikeAct) -> Result<Self> {
fn from_activity(conn: &DbConn, act: LikeAct) -> Result<Self> {
let res = Like::insert(
conn,
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 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 {
diesel::delete(&self).execute(conn)?;
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif).execute(conn)?;
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
} else {
@ -199,7 +199,7 @@ mod tests {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let like = Like::insert(&conn, NewLike::new(post, user))?;
let like = Like::insert(&*conn, NewLike::new(post, user))?;
let act = like.to_activity(&conn).unwrap();
let expected = json!({
@ -223,8 +223,8 @@ mod tests {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let like = Like::insert(&conn, NewLike::new(post, user))?;
let act = like.build_undo(&conn)?;
let like = Like::insert(&*conn, NewLike::new(post, user))?;
let act = like.build_undo(&*conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",

View file

@ -297,28 +297,6 @@ impl List {
.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_blogs, Blog, add_blogs}
func! {set: set_words, Word, add_words}
@ -435,7 +413,7 @@ mod tests {
&List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(),
);
l_eq(
&l1u,
&&l1u,
&List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(),
);
Ok(())

View file

@ -1,6 +1,6 @@
use crate::{
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
Error, Result, CONFIG,
ap_url, db_conn::DbConn, instance::Instance, safe_string::SafeString, schema::medias,
users::User, Connection, Error, Result, CONFIG,
};
use activitystreams::{object::Image, prelude::*};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
@ -16,9 +16,6 @@ use std::{
use tracing::warn;
use url::Url;
#[cfg(feature = "s3")]
use crate::config::S3Config;
const REMOTE_MEDIA_DIRECTORY: &str = "remote";
#[derive(Clone, Identifiable, Queryable, AsChangeset)]
@ -45,7 +42,7 @@ pub struct NewMedia {
pub owner_id: i32,
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq)]
pub enum MediaCategory {
Image,
Audio,
@ -108,7 +105,7 @@ impl Media {
.file_path
.rsplit_once('.')
.map(|x| x.1)
.unwrap_or("")
.expect("Media::category: extension error")
.to_lowercase()
{
"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> {
if self.is_remote {
Ok(self.remote_url.clone().unwrap_or_default())
} else {
let relative_url = self.relative_url().unwrap_or_default();
#[cfg(feature="s3")]
if CONFIG.s3.as_ref().map(|x| x.direct_download).unwrap_or(false) {
let s3_url = match CONFIG.s3.as_ref().unwrap() {
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);
}
let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen(
&CONFIG.media_directory,
"static/media",
1,
); // "static/media" from plume::routs::plume_media_files()
Ok(ap_url(&format!(
"{}/{}",
Instance::get_local()?.public_domain,
relative_url
&file_path
)))
}
}
pub fn delete(&self, conn: &Connection) -> Result<()> {
if !self.is_remote {
if CONFIG.s3.is_some() {
#[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)?)?;
}
fs::remove_file(self.file_path.as_str())?;
}
diesel::delete(self)
.execute(conn)
@ -282,65 +206,27 @@ impl Media {
}
// 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
.url()
.and_then(|url| url.to_as_uri())
.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() {
#[cfg(not(feature="s3"))]
unreachable!();
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)?;
#[cfg(feature = "s3")]
{
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)
Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?)
.and_then(|mut media| {
let mut updated = false;
@ -372,7 +258,7 @@ impl Media {
updated = true;
}
if updated {
diesel::update(&media).set(&media).execute(conn)?;
diesel::update(&media).set(&media).execute(&**conn)?;
}
Ok(media)
})
@ -381,7 +267,7 @@ impl Media {
Media::insert(
conn,
NewMedia {
file_path,
file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(),
alt_text: image
.content()
.and_then(|content| content.to_as_string())
@ -421,10 +307,12 @@ impl Media {
}
fn determine_mirror_file_path(url: &str) -> PathBuf {
let mut file_path = Path::new(&CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
match Url::parse(url) {
Ok(url) if url.has_host() => {
let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY);
Url::parse(url)
.map(|url| {
if !url.has_host() {
return;
}
file_path.push(url.host_str().unwrap());
for segment in url.path_segments().expect("FIXME") {
file_path.push(segment);
@ -432,54 +320,19 @@ fn determine_mirror_file_path(url: &str) -> PathBuf {
// TODO: handle query
// HINT: Use characters which must be percent-encoded in path as separator between path and query
// HINT: handle extension
}
other => {
if let Err(err) = other {
warn!("Failed to parse url: {} {}", &url, err);
} else {
warn!("Error without a host: {}", &url);
}
})
.unwrap_or_else(|err| {
warn!("Failed to parse url: {} {}", &url, err);
let ext = url
.rsplit('.')
.next()
.map(ToOwned::to_owned)
.unwrap_or_else(|| String::from("png"));
file_path.push(format!("{}.{}", GUID::rand(), ext));
}
}
});
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)]
pub(crate) mod tests {
use super::*;
@ -490,7 +343,7 @@ pub(crate) mod tests {
use std::path::Path;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
let mut wd = current_dir().unwrap();
let mut wd = current_dir().unwrap().to_path_buf();
while wd.pop() {
if wd.join(".git").exists() {
set_current_dir(wd).unwrap();
@ -603,7 +456,7 @@ pub(crate) mod tests {
let media = Media::insert(
conn,
NewMedia {
file_path: path,
file_path: path.clone(),
alt_text: "alt message".to_owned(),
is_remote: false,
remote_url: None,

View file

@ -1,6 +1,6 @@
use crate::{
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
Error, Result,
comments::Comment, db_conn::DbConn, notifications::*, posts::Post, schema::mentions,
users::User, Connection, Error, Result,
};
use activitystreams::{
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 mut mention = link::Mention::new();
mention.set_href(user.ap_url.parse::<IriString>()?);

View file

@ -89,7 +89,7 @@ mod tests {
let request = PasswordResetRequest::find_by_token(&conn, &token)
.expect("couldn't retrieve request");
assert!(token.len() > 32);
assert!(&token.len() > &32);
assert_eq!(&request.email, &admin_email);
Ok(())
@ -103,8 +103,8 @@ mod tests {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
PasswordResetRequest::insert(&conn, admin_email).expect("couldn't insert new request");
PasswordResetRequest::insert(&conn, admin_email)
PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
PasswordResetRequest::insert(&conn, &admin_email)
.expect("couldn't insert second request");
let count = password_reset_requests::table.count().get_result(&*conn);
@ -132,7 +132,7 @@ mod tests {
.execute(&*conn)
.expect("could not insert request");
match PasswordResetRequest::find_by_token(&conn, token) {
match PasswordResetRequest::find_by_token(&conn, &token) {
Err(Error::Expired) => (),
_ => panic!("Received unexpected result finding expired token"),
}
@ -148,7 +148,7 @@ mod tests {
user_tests::fill_database(&conn);
let admin_email = "admin@example.com";
let token = PasswordResetRequest::insert(&conn, admin_email)
let token = PasswordResetRequest::insert(&conn, &admin_email)
.expect("couldn't insert new request");
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
.expect("couldn't find and delete request");

View file

@ -1,7 +1,7 @@
use crate::{
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, Connection, Error,
PostEvent::*, Result, CONFIG, POST_CHAN,
ap_url, blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::Mention,
post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User,
Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN,
};
use activitystreams::{
activity::{Create, Delete, Update},
@ -134,7 +134,7 @@ impl Post {
.filter(posts::published.eq(true))
.count()
.load(conn)?
.first()
.get(0)
.cloned()
.ok_or(Error::NotFound)
}
@ -255,7 +255,7 @@ impl Post {
ap_url(&format!(
"{}/~/{}/{}/",
CONFIG.base_url,
iri_percent_encode_seg(&blog.fqn),
blog.fqn,
iri_percent_encode_seg(slug)
))
}
@ -465,7 +465,7 @@ impl Post {
.collect::<HashSet<_>>();
for (m, id) in &mentions {
if !old_user_mentioned.contains(id) {
Mention::from_activity(conn, m, self.id, true, true)?;
Mention::from_activity(&*conn, m, self.id, true, true)?;
}
}
@ -488,7 +488,7 @@ impl Post {
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(conn, self.id)?;
let old_tags = Tag::for_post(&*conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
@ -525,7 +525,7 @@ impl Post {
.filter_map(|t| t.name.as_ref().map(|name| name.as_str().to_string()))
.collect::<HashSet<_>>();
let old_tags = Tag::for_post(conn, self.id)?;
let old_tags = Tag::for_post(&*conn, self.id)?;
let old_tags_name = old_tags
.iter()
.filter_map(|tag| {
@ -615,15 +615,15 @@ impl Post {
}
}
impl FromId<Connection> for Post {
impl FromId<DbConn> for Post {
type Error = Error;
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)
}
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 article = article.inner;
@ -756,11 +756,7 @@ impl FromId<Connection> for Post {
let timestamp_secs = published.unix_timestamp();
let timestamp_nanos = published.unix_timestamp_nanos()
- (timestamp_secs as i128) * 1000i128 * 1000i128 * 1000i128;
NaiveDateTime::from_timestamp_opt(
timestamp_secs,
timestamp_nanos as u32,
)
.unwrap()
NaiveDateTime::from_timestamp(timestamp_secs, timestamp_nanos as u32)
}),
subtitle: article
.summary()
@ -821,21 +817,21 @@ impl FromId<Connection> for Post {
}
}
impl AsObject<User, Create, &Connection> for Post {
impl AsObject<User, Create, &DbConn> for Post {
type Error = Error;
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?
Ok(self)
}
}
impl AsObject<User, Delete, &Connection> for Post {
impl AsObject<User, Delete, &DbConn> for Post {
type Error = Error;
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
.get_authors(conn)?
.into_iter()
@ -859,16 +855,16 @@ pub struct PostUpdate {
pub tags: Option<serde_json::Value>,
}
impl FromId<Connection> for PostUpdate {
impl FromId<DbConn> for PostUpdate {
type Error = Error;
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
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 {
ap_url: updated
.ap_object_ref()
@ -923,11 +919,11 @@ impl FromId<Connection> for PostUpdate {
}
}
impl AsObject<User, Update, &Connection> for PostUpdate {
impl AsObject<User, Update, &DbConn> for PostUpdate {
type Error = Error;
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 =
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
@ -1027,7 +1023,6 @@ impl From<PostEvent> for Arc<Post> {
#[cfg(test)]
mod tests {
use super::*;
use crate::db_conn::DbConn;
use crate::inbox::{inbox, tests::fill_database, InboxResult};
use crate::mentions::{Mention, NewMention};
use crate::safe_string::SafeString;
@ -1041,7 +1036,7 @@ mod tests {
let post = &posts[0];
let mentioned = &users[1];
let mention = Mention::insert(
conn,
&conn,
NewMention {
mentioned_id: mentioned.id,
post_id: Some(post.id),
@ -1049,7 +1044,7 @@ mod tests {
},
)
.unwrap();
(post.to_owned(), mention, posts, users, blogs)
(post.to_owned(), mention.to_owned(), posts, users, blogs)
}
// creates a post, get it's Create activity, delete the post,
@ -1058,9 +1053,9 @@ mod tests {
fn self_federation() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (_, users, blogs) = fill_database(conn);
let (_, users, blogs) = fill_database(&conn);
let post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "yo".into(),
@ -1077,19 +1072,19 @@ mod tests {
)
.unwrap();
PostAuthor::insert(
conn,
&conn,
NewPostAuthor {
post_id: post.id,
author_id: users[0].id,
},
)
.unwrap();
let create = post.create_activity(conn).unwrap();
post.delete(conn).unwrap();
let create = post.create_activity(&conn).unwrap();
post.delete(&conn).unwrap();
match inbox(conn, serde_json::to_value(create).unwrap()).unwrap() {
match inbox(&conn, serde_json::to_value(create).unwrap()).unwrap() {
InboxResult::Post(p) => {
assert!(p.is_author(conn, users[0].id).unwrap());
assert!(p.is_author(&conn, users[0].id).unwrap());
assert_eq!(p.source, "Hello".to_owned());
assert_eq!(p.blog_id, blogs[0].id);
assert_eq!(p.content, SafeString::new("Hello"));
@ -1226,7 +1221,7 @@ mod tests {
let actual = to_value(act)?;
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!(
id_pre,

View file

@ -45,12 +45,6 @@ impl Actor for RemoteFetchActor {
RemoteUserFound(user) => match self.conn.get() {
Ok(conn) => {
let conn = DbConn(conn);
if user
.get_instance(&conn)
.map_or(false, |instance| instance.blocked)
{
return;
}
// Don't call these functions in parallel
// for the case database connections limit is too small
fetch_and_cache_articles(&user, &conn);

View file

@ -1,6 +1,6 @@
use crate::{
instance::Instance, notifications::*, posts::Post, schema::reshares, timeline::*, users::User,
Connection, Error, Result, CONFIG,
db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
timeline::*, users::User, Connection, Error, Result, CONFIG,
};
use activitystreams::{
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 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 reshare = Reshare::insert(
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 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)
}
fn from_activity(conn: &Connection, act: Announce) -> Result<Self> {
fn from_activity(conn: &DbConn, act: Announce) -> Result<Self> {
let res = Reshare::insert(
conn,
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 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 {
diesel::delete(&self).execute(conn)?;
diesel::delete(&self).execute(&**conn)?;
// delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif).execute(conn)?;
diesel::delete(&notif).execute(&**conn)?;
}
Ok(())
@ -229,7 +229,7 @@ mod test {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
let act = reshare.to_activity(&conn).unwrap();
let expected = json!({
@ -253,8 +253,8 @@ mod test {
let (posts, _users, _blogs) = fill_database(&conn);
let post = &posts[0];
let user = &post.get_authors(&conn)?[0];
let reshare = Reshare::insert(&conn, NewReshare::new(post, user))?;
let act = reshare.build_undo(&conn)?;
let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?;
let act = reshare.build_undo(&*conn)?;
let expected = json!({
"actor": "https://plu.me/@/admin/",

View file

@ -93,7 +93,7 @@ fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
}
}
#[derive(Debug, Clone, PartialEq, Eq, AsExpression, FromSqlRow, Default)]
#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)]
#[sql_type = "Text"]
pub struct SafeString {
value: String,

View file

@ -108,7 +108,7 @@ mod tests {
let searcher = Arc::new(get_searcher(&CONFIG.search_tokenizers));
SearchActor::init(searcher.clone(), db_pool.clone());
let conn = db_pool.get().unwrap();
let conn = db_pool.clone().get().unwrap();
let title = random_hex()[..8].to_owned();
let (_instance, _user, blog) = fill_database(&conn);
@ -161,43 +161,41 @@ mod tests {
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex(),
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex(),
public_domain: random_hex().to_string(),
},
)
.unwrap();
let user = User::insert(
conn,
NewUser {
username: random_hex(),
display_name: random_hex(),
outbox_url: random_hex(),
inbox_url: random_hex(),
username: random_hex().to_string(),
display_name: random_hex().to_string(),
outbox_url: random_hex().to_string(),
inbox_url: random_hex().to_string(),
summary: "".to_string(),
email: None,
hashed_password: None,
instance_id: instance.id,
ap_url: random_hex(),
ap_url: random_hex().to_string(),
private_key: None,
public_key: "".to_string(),
shared_inbox_url: None,
followers_endpoint: random_hex(),
followers_endpoint: random_hex().to_string(),
avatar_id: None,
summary_html: SafeString::new(""),
role: 0,
fqn: random_hex(),
fqn: random_hex().to_string(),
},
)
.unwrap();
let blog = NewBlog {
instance_id: instance.id,
actor_id: random_hex(),
ap_url: random_hex(),
inbox_url: random_hex(),
outbox_url: random_hex(),
..Default::default()
};
let mut blog = NewBlog::default();
blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string();
blog.ap_url = random_hex().to_string();
blog.inbox_url = random_hex().to_string();
blog.outbox_url = random_hex().to_string();
let blog = Blog::insert(conn, blog).unwrap();
BlogAuthor::insert(
conn,

View file

@ -154,7 +154,7 @@ pub(crate) mod tests {
},
)
.unwrap();
searcher.add_document(conn, &post).unwrap();
searcher.add_document(&conn, &post).unwrap();
searcher.commit();
assert_eq!(
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,

View file

@ -94,7 +94,7 @@ macro_rules! gen_to_string {
)*
$(
for val in &$self.$date {
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce_opt(*val as i32).unwrap().format("%Y-%m-%d")));
$result.push_str(&format!("{}:{} ", stringify!($date), NaiveDate::from_num_days_from_ce(*val as i32).format("%Y-%m-%d")));
}
)*
}
@ -180,16 +180,12 @@ impl PlumeQuery {
if self.before.is_some() || self.after.is_some() {
// if at least one range bound is provided
let after = self.after.unwrap_or_else(|| {
i64::from(
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.num_days_from_ce(),
)
});
let after = self
.after
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
let before = self
.before
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
let field = Searcher::schema().get_field("creation_date").unwrap();
let range =
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
@ -206,20 +202,16 @@ impl PlumeQuery {
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
let before = self
.before
.unwrap_or_else(|| i64::from(Utc::now().date_naive().num_days_from_ce()));
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
self
}
// documents older than the provided date will be ignored
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
let after = self.after.unwrap_or_else(|| {
i64::from(
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.num_days_from_ce(),
)
});
let after = self
.after
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
self
}

View file

@ -7,7 +7,6 @@ use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use itertools::Itertools;
use std::fs;
use std::{cmp, fs::create_dir_all, io, path::Path, sync::Mutex};
use std::collections::HashMap;
use tantivy::{
collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
ReloadPolicy, TantivyError, Term,
@ -290,59 +289,13 @@ Then try to restart Plume
}
pub fn fill(&self, conn: &Connection) -> Result<()> {
let mut writer = self.writer.lock().unwrap();
let writer = writer.as_mut().unwrap();
writer.delete_all_documents().unwrap();
let schema = self.index.schema();
let post_id = schema.get_field("post_id").unwrap();
let creation_date = schema.get_field("creation_date").unwrap();
let instance = schema.get_field("instance").unwrap();
let author = schema.get_field("author").unwrap();
let tag = schema.get_field("tag").unwrap();
let blog_name = schema.get_field("blog").unwrap();
let content = schema.get_field("content").unwrap();
let subtitle = schema.get_field("subtitle").unwrap();
let title = schema.get_field("title").unwrap();
let lang = schema.get_field("lang").unwrap();
let license = schema.get_field("license").unwrap();
let mut instance_cache = HashMap::new();
const PAGE_SIZE: i64 = 8192;
let mut cursor = -1;
loop {
let posts = posts::table
.filter(posts::published.eq(true))
.filter(posts::id.gt(cursor))
.order(posts::id.asc())
.limit(PAGE_SIZE)
.load::<Post>(conn)?;
for post in posts.iter() {
// TODO we could joins to reduce per-post sql calls even more
let blog = post.get_blog(conn)?;
if !instance_cache.contains_key(&blog.instance_id) {
instance_cache.insert(blog.instance_id, Instance::get(conn, blog.instance_id)?.public_domain);
}
writer.add_document(doc!(
post_id => i64::from(post.id),
author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "),
creation_date => i64::from(post.creation_date.num_days_from_ce()),
instance => instance_cache[&blog.instance_id].clone(),
tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "),
blog_name => blog.title,
content => post.content.get().clone(),
subtitle => post.subtitle.clone(),
title => post.title.clone(),
lang => detect_lang(post.content.get()).and_then(|i| if i.is_reliable() { Some(i.lang()) } else {None} ).unwrap_or(Lang::Eng).name(),
license => post.license.clone(),
));
cursor = post.id;
}
if posts.len() < PAGE_SIZE as usize {
break Ok(())
}
for post in posts::table
.filter(posts::published.eq(true))
.load::<Post>(conn)?
{
self.update_document(conn, &post)?
}
Ok(())
}
pub fn commit(&self) {

View file

@ -92,7 +92,7 @@ mod tests {
let (posts, _users, _blogs) = fill_database(conn);
let post_id = posts[0].id;
let mut ht = Hashtag::new();
ht.set_href(ap_url("https://plu.me/tag/a_tag").parse::<IriString>()?);
ht.set_href(ap_url(&format!("https://plu.me/tag/a_tag")).parse::<IriString>()?);
ht.set_name("a_tag".to_string());
let tag = Tag::from_activity(conn, &ht, post_id, true)?;

View file

@ -1,4 +1,5 @@
use crate::{
db_conn::DbConn,
lists::List,
posts::Post,
schema::{posts, timeline, timeline_definition},
@ -11,9 +12,9 @@ use std::ops::Deref;
pub(crate) mod query;
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, Queryable, Identifiable, AsChangeset)]
#[table_name = "timeline_definition"]
pub struct Timeline {
pub id: i32,
@ -219,7 +220,7 @@ impl Timeline {
.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
.load::<Self>(conn.deref())
.map_err(Error::from)?;
@ -245,26 +246,7 @@ impl Timeline {
Ok(())
}
pub fn remove_post(&self, conn: &Connection, post: &Post) -> 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> {
pub fn matches(&self, conn: &DbConn, post: &Post, kind: Kind<'_>) -> Result<bool> {
let query = TimelineQuery::parse(&self.query)?;
query.matches(conn, self, post, kind)
}
@ -300,63 +282,73 @@ mod tests {
fn test_timeline() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(conn);
let users = userTests::fill_database(&conn);
let mut tl1_u1 = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"all".to_owned(),
)
.unwrap();
List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
List::new(
&conn,
"languages I speak",
Some(&users[1]),
ListType::Prefix,
)
.unwrap();
let tl2_u1 = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"another timeline".to_owned(),
"followed".to_owned(),
)
.unwrap();
let tl1_u2 = Timeline::new_for_user(
conn,
&conn,
users[1].id,
"english posts".to_owned(),
"lang in \"languages I speak\"".to_owned(),
)
.unwrap();
let tl1_instance = Timeline::new_for_instance(
conn,
&conn,
"english posts".to_owned(),
"license in [cc]".to_owned(),
)
.unwrap();
assert_eq!(tl1_u1, Timeline::get(conn, tl1_u1.id).unwrap());
assert_eq!(tl1_u1, Timeline::get(&conn, tl1_u1.id).unwrap());
assert_eq!(
tl2_u1,
Timeline::find_for_user_by_name(conn, Some(users[0].id), "another timeline")
Timeline::find_for_user_by_name(&conn, Some(users[0].id), "another timeline")
.unwrap()
);
assert_eq!(
tl1_instance,
Timeline::find_for_user_by_name(conn, None, "english posts").unwrap()
Timeline::find_for_user_by_name(&conn, None, "english posts").unwrap()
);
let tl_u1 = Timeline::list_for_user(conn, Some(users[0].id)).unwrap();
let tl_u1 = Timeline::list_for_user(&conn, Some(users[0].id)).unwrap();
assert_eq!(3, tl_u1.len()); // it is not 2 because there is a "Your feed" tl created for each user automatically
assert!(tl_u1.iter().any(|tl| *tl == tl1_u1));
assert!(tl_u1.iter().any(|tl| *tl == tl2_u1));
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl1_u1 }));
assert!(tl_u1.iter().fold(false, |res, tl| { res || *tl == tl2_u1 }));
let tl_instance = Timeline::list_for_user(conn, None).unwrap();
let tl_instance = Timeline::list_for_user(&conn, None).unwrap();
assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
assert!(tl_instance.iter().any(|tl| *tl == tl1_instance));
assert!(tl_instance
.iter()
.fold(false, |res, tl| { res || *tl == tl1_instance }));
tl1_u1.name = "My Super TL".to_owned();
let new_tl1_u2 = tl1_u2.update(conn).unwrap();
let new_tl1_u2 = tl1_u2.update(&conn).unwrap();
let tl_u2 = Timeline::list_for_user(conn, Some(users[1].id)).unwrap();
let tl_u2 = Timeline::list_for_user(&conn, Some(users[1].id)).unwrap();
assert_eq!(2, tl_u2.len()); // same here
assert!(tl_u2.iter().any(|tl| *tl == new_tl1_u2));
assert!(tl_u2
.iter()
.fold(false, |res, tl| { res || *tl == new_tl1_u2 }));
Ok(())
});
@ -366,48 +358,48 @@ mod tests {
fn test_timeline_creation_error() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let users = userTests::fill_database(conn);
let users = userTests::fill_database(&conn);
assert!(Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"invalid keyword".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
conn,
&conn,
"my timeline".to_owned(),
"invalid keyword".to_owned(),
)
.is_err());
assert!(Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"author in non_existant_list".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
conn,
&conn,
"my timeline".to_owned(),
"lang in dont-exist".to_owned(),
)
.is_err());
List::new(conn, "friends", Some(&users[0]), ListType::User).unwrap();
List::new(conn, "idk", None, ListType::Blog).unwrap();
List::new(&conn, "friends", Some(&users[0]), ListType::User).unwrap();
List::new(&conn, "idk", None, ListType::Blog).unwrap();
assert!(Timeline::new_for_user(
conn,
&conn,
users[0].id,
"my timeline".to_owned(),
"blog in friends".to_owned(),
)
.is_err());
assert!(Timeline::new_for_instance(
conn,
&conn,
"my timeline".to_owned(),
"not author in idk".to_owned(),
)
@ -421,10 +413,10 @@ mod tests {
fn test_simple_match() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(),
@ -432,7 +424,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@ -448,10 +440,10 @@ mod tests {
},
)
.unwrap();
assert!(gnu_tl.matches(conn, &gnu_post, Kind::Original).unwrap());
assert!(gnu_tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
let non_free_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug2".to_string(),
@ -468,7 +460,7 @@ mod tests {
)
.unwrap();
assert!(!gnu_tl
.matches(conn, &non_free_post, Kind::Original)
.matches(&conn, &non_free_post, Kind::Original)
.unwrap());
Ok(())
@ -479,9 +471,9 @@ mod tests {
fn test_complex_match() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
Follow::insert(
conn,
&conn,
NewFollow {
follower_id: users[0].id,
following_id: users[1].id,
@ -491,11 +483,11 @@ mod tests {
.unwrap();
let fav_blogs_list =
List::new(conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
fav_blogs_list.add_blogs(conn, &[blogs[0].id]).unwrap();
List::new(&conn, "fav_blogs", Some(&users[0]), ListType::Blog).unwrap();
fav_blogs_list.add_blogs(&conn, &[blogs[0].id]).unwrap();
let my_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"My timeline".to_owned(),
"blog in fav_blogs and not has_cover or local and followed exclude likes"
@ -504,7 +496,7 @@ mod tests {
.unwrap();
let post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "about-linux".to_string(),
@ -520,10 +512,10 @@ mod tests {
},
)
.unwrap();
assert!(my_tl.matches(conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
assert!(my_tl.matches(&conn, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
let post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[1].id,
slug: "about-linux-2".to_string(),
@ -541,7 +533,7 @@ mod tests {
},
)
.unwrap();
assert!(!my_tl.matches(conn, &post, Kind::Like(&users[1])).unwrap());
assert!(!my_tl.matches(&conn, &post, Kind::Like(&users[1])).unwrap());
Ok(())
});
@ -551,17 +543,17 @@ mod tests {
fn test_add_to_all_timelines() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"GNU timeline".to_owned(),
"license in [AGPL, LGPL, GPL]".to_owned(),
)
.unwrap();
let non_gnu_tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Stallman disapproved timeline".to_owned(),
"not license in [AGPL, LGPL, GPL]".to_owned(),
@ -569,7 +561,7 @@ mod tests {
.unwrap();
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@ -587,7 +579,7 @@ mod tests {
.unwrap();
let non_free_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug2".to_string(),
@ -604,13 +596,13 @@ mod tests {
)
.unwrap();
Timeline::add_to_all_timelines(conn, &gnu_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(conn, &non_free_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(&conn, &gnu_post, Kind::Original).unwrap();
Timeline::add_to_all_timelines(&conn, &non_free_post, Kind::Original).unwrap();
let res = gnu_tl.get_latest(conn, 2).unwrap();
let res = gnu_tl.get_latest(&conn, 2).unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].id, gnu_post.id);
let res = non_gnu_tl.get_latest(conn, 2).unwrap();
let res = non_gnu_tl.get_latest(&conn, 2).unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].id, non_free_post.id);
@ -622,10 +614,10 @@ mod tests {
fn test_matches_lists_direct() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@ -642,63 +634,63 @@ mod tests {
)
.unwrap();
gnu_post
.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
.update_tags(&conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
.unwrap();
PostAuthor::insert(
conn,
&conn,
NewPostAuthor {
post_id: gnu_post.id,
author_id: blogs[0].list_authors(conn).unwrap()[0].id,
author_id: blogs[0].list_authors(&conn).unwrap()[0].id,
},
)
.unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"blog timeline".to_owned(),
format!("blog in [{}]", blogs[0].fqn),
)
.unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"blog timeline".to_owned(),
"blog in [no_one@nowhere]".to_owned(),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"author timeline".to_owned(),
format!(
"author in [{}]",
blogs[0].list_authors(conn).unwrap()[0].fqn
blogs[0].list_authors(&conn).unwrap()[0].fqn
),
)
.unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"author timeline".to_owned(),
format!("author in [{}]", users[2].fqn),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
assert!(tl
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
.unwrap());
assert!(!tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"author timeline".to_owned(),
format!(
@ -707,50 +699,50 @@ mod tests {
),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
assert!(!tl
.matches(conn, &gnu_post, Kind::Reshare(&users[2]))
.matches(&conn, &gnu_post, Kind::Reshare(&users[2]))
.unwrap());
assert!(tl.matches(conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Like(&users[2])).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"tag timeline".to_owned(),
"tags in [free]".to_owned(),
)
.unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"tag timeline".to_owned(),
"tags in [private]".to_owned(),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"english timeline".to_owned(),
"lang in [en]".to_owned(),
)
.unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"franco-italian timeline".to_owned(),
"lang in [fr, it]".to_owned(),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
Ok(())
});
@ -794,10 +786,10 @@ mod tests {
fn test_matches_keyword() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let (users, blogs) = blogTests::fill_database(conn);
let (users, blogs) = blogTests::fill_database(&conn);
let gnu_post = Post::insert(
conn,
&conn,
NewPost {
blog_id: blogs[0].id,
slug: "slug".to_string(),
@ -815,61 +807,61 @@ mod tests {
.unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Linux title".to_owned(),
"title contains Linux".to_owned(),
)
.unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Microsoft title".to_owned(),
"title contains Microsoft".to_owned(),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Linux subtitle".to_owned(),
"subtitle contains Stallman".to_owned(),
)
.unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Microsoft subtitle".to_owned(),
"subtitle contains Nadella".to_owned(),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Linux content".to_owned(),
"content contains Linux".to_owned(),
)
.unwrap();
assert!(tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
let tl = Timeline::new_for_user(
conn,
&conn,
users[0].id,
"Microsoft content".to_owned(),
"subtitle contains Windows".to_owned(),
)
.unwrap();
assert!(!tl.matches(conn, &gnu_post, Kind::Original).unwrap());
tl.delete(conn).unwrap();
assert!(!tl.matches(&conn, &gnu_post, Kind::Original).unwrap());
tl.delete(&conn).unwrap();
Ok(())
});

View file

@ -1,16 +1,17 @@
use crate::{
blogs::Blog,
db_conn::DbConn,
lists::{self, ListType},
posts::Post,
tags::Tag,
timeline::Timeline,
users::User,
Connection, Result,
Result,
};
use plume_common::activity_pub::inbox::AsActor;
use whatlang::{self, Lang};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub enum QueryError {
SyntaxError(usize, usize, String),
UnexpectedEndOfQuery,
@ -19,7 +20,7 @@ pub enum QueryError {
pub type QueryResult<T> = std::result::Result<T, QueryError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Kind<'a> {
Original,
Reshare(&'a User),
@ -154,7 +155,7 @@ enum TQ<'a> {
impl<'a> TQ<'a> {
fn matches(
&self,
conn: &Connection,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
@ -199,7 +200,7 @@ enum Arg<'a> {
impl<'a> Arg<'a> {
pub fn matches(
&self,
conn: &Connection,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
@ -224,7 +225,7 @@ enum WithList {
impl WithList {
pub fn matches(
&self,
conn: &Connection,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
list: &List<'_>,
@ -291,7 +292,7 @@ impl WithList {
WithList::Author { boosts, likes } => match kind {
Kind::Original => Ok(list
.iter()
.filter_map(|a| User::find_by_fqn(conn, a).ok())
.filter_map(|a| User::find_by_fqn(&*conn, a).ok())
.any(|a| post.is_author(conn, a.id).unwrap_or(false))),
Kind::Reshare(u) => {
if *boosts {
@ -360,7 +361,7 @@ enum Bool {
impl Bool {
pub fn matches(
&self,
conn: &Connection,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,
@ -653,7 +654,7 @@ impl<'a> TimelineQuery<'a> {
pub fn matches(
&self,
conn: &Connection,
conn: &DbConn,
timeline: &Timeline,
post: &Post,
kind: Kind<'_>,

View file

@ -1,8 +1,8 @@
use crate::{
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, comments::Comment, db_conn::DbConn,
follows::Follow, instance::*, medias::Media, notifications::Notification,
post_authors::PostAuthor, posts::Post, safe_string::SafeString, schema::users,
timeline::Timeline, Connection, Error, Result, UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
ap_url, blocklisted_emails::BlocklistedEmail, blogs::Blog, db_conn::DbConn, follows::Follow,
instance::*, medias::Media, notifications::Notification, post_authors::PostAuthor, posts::Post,
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
};
use activitystreams::{
activity::Delete,
@ -15,10 +15,7 @@ use activitystreams::{
prelude::*,
};
use chrono::{NaiveDateTime, Utc};
use diesel::{
self, BelongingToDsl, BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl,
RunQueryDsl, TextExpressionMethods,
};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use ldap3::{LdapConn, Scope, SearchEntry};
use openssl::{
hash::MessageDigest,
@ -168,14 +165,6 @@ impl User {
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)
.execute(conn)
.map(|_| ())
@ -197,16 +186,15 @@ impl User {
pub fn count_local(conn: &Connection) -> Result<i64> {
users::table
.filter(users::instance_id.eq(Instance::get_local()?.id))
.filter(users::role.ne(Role::Instance as i32))
.count()
.get_result(conn)
.get_result(&*conn)
.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
.filter(users::fqn.eq(fqn))
.first(conn)
.first(&**conn)
.optional()?;
if let Some(from_db) = 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
*/
@ -252,7 +219,7 @@ impl User {
.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)?
.links
.into_iter()
@ -279,7 +246,20 @@ impl User {
fn fetch(url: &str) -> Result<CustomPerson> {
let res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
let text = &res.text()?;
let json = serde_json::from_str::<CustomPerson>(text)?;
// without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let person = serde_json::from_str::<Person>(text)?;
let json = CustomPerson::new(
ApActor::new(
person
.clone()
.id_unchecked()
.ok_or(Error::MissingApProperty)?
.to_owned(),
person,
),
ap_sign,
); // FIXME: Don't clone()
Ok(json)
}
@ -289,13 +269,23 @@ impl User {
pub fn refetch(&self, conn: &Connection) -> Result<()> {
User::fetch(&self.ap_url.clone()).and_then(|json| {
let avatar = json
.icon()
.and_then(|icon| icon.iter().next())
.and_then(|i| i.clone().extend::<Image, ImageType>().ok())
.and_then(|image| image)
.and_then(|image| image.id_unchecked().map(|url| url.to_string()))
.and_then(|url| Media::save_remote(conn, url, self).ok());
let avatar = Media::save_remote(
conn,
json.ap_actor_ref()
.icon()
.ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set
.iter()
.next()
.and_then(|i| {
i.clone()
.extend::<Image, ImageType>() // FIXME: Don't clone()
.ok()?
.and_then(|url| Some(url.id_unchecked()?.to_string()))
})
.ok_or(Error::MissingApProperty)?,
self,
)
.ok();
let pub_key = &json.ext_one.public_key.public_key_pem;
diesel::update(self)
@ -445,7 +435,7 @@ impl User {
}
// if no user was found, and we were unable to auto-register from ldap
// 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")
.hashed_password;
other.map(|pass| bcrypt::verify(password, &pass));
@ -464,7 +454,6 @@ impl User {
pub fn get_local_page(conn: &Connection, (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))
.order(users::username.asc())
.offset(min.into())
.limit((max - min).into())
@ -942,7 +931,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
.cookies()
.get_private(AUTH_COOKIE)
.and_then(|cookie| cookie.value().parse().ok())
.and_then(|id| User::get(&conn, id).ok())
.and_then(|id| User::get(&*conn, id).ok())
.or_forward(())
}
}
@ -955,15 +944,15 @@ impl IntoId for User {
impl Eq for User {}
impl FromId<Connection> for User {
impl FromId<DbConn> for User {
type Error = Error;
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)
}
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 username = actor
.preferred_username()
@ -971,10 +960,6 @@ impl FromId<Connection> for User {
.to_string();
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
tracing::error!(
"preferredUsername includes invalid character(s): {}",
&username
);
return Err(Error::InvalidValue);
}
@ -1064,7 +1049,7 @@ impl FromId<Connection> for User {
}
}
impl AsActor<&Connection> for User {
impl AsActor<&DbConn> for User {
fn get_inbox_url(&self) -> String {
self.inbox_url.clone()
}
@ -1080,11 +1065,11 @@ impl AsActor<&Connection> for User {
}
}
impl AsObject<User, Delete, &Connection> for User {
impl AsObject<User, Delete, &DbConn> for User {
type Error = Error;
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 {
self.delete(conn).map(|_| ())
} else {
@ -1247,7 +1232,7 @@ pub(crate) mod tests {
)
.unwrap();
other.avatar_id = Some(avatar.id);
let other = other.save_changes::<User>(conn).unwrap();
let other = other.save_changes::<User>(&*conn).unwrap();
vec![admin, user, other]
}
@ -1350,11 +1335,11 @@ pub(crate) mod tests {
fn delete() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn);
let inserted = fill_database(&conn);
assert!(User::get(conn, inserted[0].id).is_ok());
inserted[0].delete(conn).unwrap();
assert!(User::get(conn, inserted[0].id).is_err());
assert!(User::get(&conn, inserted[0].id).is_ok());
inserted[0].delete(&conn).unwrap();
assert!(User::get(&conn, inserted[0].id).is_err());
Ok(())
});
}
@ -1363,20 +1348,20 @@ pub(crate) mod tests {
fn admin() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn);
let inserted = fill_database(&conn);
let local_inst = Instance::get_local().unwrap();
let mut i = 0;
while local_inst.has_admin(conn).unwrap() {
while local_inst.has_admin(&conn).unwrap() {
assert!(i < 100); //prevent from looping indefinitelly
local_inst
.main_admin(conn)
.main_admin(&conn)
.unwrap()
.set_role(conn, Role::Normal)
.set_role(&conn, Role::Normal)
.unwrap();
i += 1;
}
inserted[0].set_role(conn, Role::Admin).unwrap();
assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
inserted[0].set_role(&conn, Role::Admin).unwrap();
assert_eq!(inserted[0].id, local_inst.main_admin(&conn).unwrap().id);
Ok(())
});
}
@ -1385,9 +1370,9 @@ pub(crate) mod tests {
fn auth() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let test_user = NewUser::new_local(
conn,
&conn,
"test".to_owned(),
"test user".to_owned(),
Role::Normal,
@ -1398,10 +1383,10 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(
User::login(conn, "test", "test_password").unwrap().id,
User::login(&conn, "test", "test_password").unwrap().id,
test_user.id
);
assert!(User::login(conn, "test", "other_password").is_err());
assert!(User::login(&conn, "test", "other_password").is_err());
Ok(())
});
}
@ -1410,26 +1395,26 @@ pub(crate) mod tests {
fn get_local_page() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
fill_database(conn);
fill_database(&conn);
let page = User::get_local_page(conn, (0, 2)).unwrap();
let page = User::get_local_page(&conn, (0, 2)).unwrap();
assert_eq!(page.len(), 2);
assert!(page[0].username <= page[1].username);
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
let mut last_username = User::get_local_page(&conn, (0, 1)).unwrap()[0]
.username
.clone();
for i in 1..User::count_local(conn).unwrap() as i32 {
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
for i in 1..User::count_local(&conn).unwrap() as i32 {
let page = User::get_local_page(&conn, (i, i + 1)).unwrap();
assert_eq!(page.len(), 1);
assert!(last_username <= page[0].username);
last_username = page[0].username.clone();
}
assert_eq!(
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
User::get_local_page(&conn, (0, User::count_local(&conn).unwrap() as i32 + 10))
.unwrap()
.len() as i64,
User::count_local(conn).unwrap()
User::count_local(&conn).unwrap()
);
Ok(())
});

View file

@ -1 +1 @@
nightly-2022-07-19
nightly-2022-01-27

View file

@ -3,4 +3,4 @@ set -euo pipefail
version="$1"
docker run --rm -v $PWD:/repo -v $PWD/pkg:/pkg -v $PWD/script/prebuild.sh:/prebuild.sh plumeorg/plume-buildenv:v0.7.0 /prebuild.sh "$version" /repo /prebuild /pkg
docker run --rm -v $PWD:/repo -v $PWD/pkg:/pkg -v $PWD/script/prebuild.sh:/prebuild.sh plumeorg/plume-buildenv:v0.4.0 /prebuild.sh "$version" /repo /prebuild /pkg

View file

@ -10,7 +10,7 @@ plm instance new -d plume-test.local -n plume-test
plm users new -n admin -N 'Admin' -e 'email@exemple.com' -p 'password'
plume &
caddy run --config /Caddyfile &
caddy run -config /Caddyfile &
until curl http://localhost:7878/test/health -f; do sleep 1; done 2>/dev/null >/dev/null

View file

@ -10,7 +10,7 @@ pub fn create(conn: DbConn, data: Json<NewAppData>) -> Api<App> {
let client_id = random_hex();
let client_secret = random_hex();
let app = App::insert(
&conn,
&*conn,
NewApp {
name: data.name.clone(),
client_id,

View file

@ -157,7 +157,6 @@ Then try to restart Plume.
routes::instance::admin_mod,
routes::instance::admin_instances,
routes::instance::admin_users,
routes::instance::admin_search_users,
routes::instance::admin_email_blocklist,
routes::instance::add_email_blocklist,
routes::instance::delete_email_blocklist,

View file

@ -160,7 +160,7 @@ pub fn delete(name: String, conn: DbConn, rockets: PlumeRocket) -> RespondOrRedi
.and_then(|u| u.is_author_in(&conn, &blog).ok())
.unwrap_or(false)
{
blog.delete(&conn).expect("blog::expect: deletion error");
blog.delete(&*conn).expect("blog::expect: deletion error");
Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your blog was deleted."),
@ -364,7 +364,7 @@ pub fn outbox_page(
#[get("/~/<name>/atom.xml")]
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
let entries = Post::get_recents_for_blog(&conn, &blog, 15).ok()?;
let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?;
let uri = Instance::get_local()
.ok()?
.compute_box("~", &name, "atom.xml");
@ -454,33 +454,29 @@ mod tests {
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex(),
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex(),
public_domain: random_hex().to_string(),
},
)
.unwrap();
Instance::cache_local(conn);
instance
});
let user = NewUser {
instance_id: instance.id,
username: random_hex(),
ap_url: random_hex(),
inbox_url: random_hex(),
outbox_url: random_hex(),
followers_endpoint: random_hex(),
..Default::default()
};
let mut user = NewUser::default();
user.instance_id = instance.id;
user.username = random_hex().to_string();
user.ap_url = random_hex().to_string();
user.inbox_url = random_hex().to_string();
user.outbox_url = random_hex().to_string();
user.followers_endpoint = random_hex().to_string();
let user = User::insert(conn, user).unwrap();
let blog = NewBlog {
instance_id: instance.id,
actor_id: random_hex(),
ap_url: random_hex(),
inbox_url: random_hex(),
outbox_url: random_hex(),
..Default::default()
};
let mut blog = NewBlog::default();
blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string();
blog.ap_url = random_hex().to_string();
blog.inbox_url = random_hex().to_string();
blog.outbox_url = random_hex().to_string();
let blog = Blog::insert(conn, blog).unwrap();
BlogAuthor::insert(
conn,

View file

@ -125,7 +125,7 @@ pub fn create(
user.has_reshared(&conn, &post)
.expect("comments::create: reshared error"),
user.is_following(
&conn,
&*conn,
post.get_authors(&conn)
.expect("comments::create: authors error")[0]
.id

View file

@ -3,7 +3,6 @@ use crate::{
routes::{errors::ErrorPage, RespondOrRedirect},
template_utils::{IntoContext, Ructe},
};
use plume_models::{
db_conn::DbConn, email_signups::EmailSignup, instance::Instance, lettre::Transport, signups,
Error, PlumeRocket, CONFIG,
@ -14,11 +13,7 @@ use rocket::{
response::{Flash, Redirect},
State,
};
use std::{
borrow::Cow,
collections::HashMap,
sync::{Arc, Mutex},
};
use std::sync::{Arc, Mutex};
use tracing::warn;
use validator::{Validate, ValidationError, ValidationErrors};
@ -110,26 +105,6 @@ pub fn create(
render!(email_signups::create(&(&conn, &rockets).to_context())).into()
}
Error::NotFound => render!(errors::not_found(&(&conn, &rockets).to_context())).into(),
Error::Blocklisted(show, msg) => {
let mut errors = ValidationErrors::new();
if *show {
errors.add(
"email",
ValidationError {
code: Cow::from("blocklisted"),
message: Some(Cow::from(msg.clone())),
params: HashMap::new(),
},
);
}
render!(email_signups::new(
&(&conn, &rockets).to_context(),
registration_open,
&form,
errors
))
.into()
}
_ => render!(errors::not_found(&(&conn, &rockets).to_context())).into(), // FIXME
});
}
@ -178,28 +153,6 @@ pub fn show(
)))
} // TODO: Flash and redirect
Error::NotFound => return Err(Error::NotFound.into()),
Error::Blocklisted(show, msg) => {
let mut errors = ValidationErrors::new();
if show {
errors.add(
"email",
ValidationError {
code: Cow::from("blocklisted"),
message: Some(Cow::from(msg)),
params: HashMap::new(),
},
);
}
return Ok(render!(email_signups::new(
&(&conn, &rockets).to_context(),
Instance::get_local()?.open_registrations,
&EmailSignupForm {
email: signup.email.clone(),
email_confirmation: signup.email
},
errors
)));
}
_ => return Err(Error::NotFound.into()), // FIXME
}
}
@ -254,38 +207,12 @@ pub fn signup(
err
))));
}
let user = signup.complete(&conn, form.username.clone(), form.password.clone());
match user {
Err(Error::Blocklisted(show, msg)) => {
let instance = Instance::get_local().map_err(|_| Status::UnprocessableEntity)?;
let mut errors = ValidationErrors::new();
if show {
errors.add(
"email",
ValidationError {
code: Cow::from("blocklisted"),
message: Some(Cow::from(msg)),
params: HashMap::new(),
},
);
}
return Ok(render!(email_signups::new(
&(&conn, &rockets).to_context(),
instance.open_registrations,
&EmailSignupForm {
email: signup.email.clone(),
email_confirmation: signup.email
},
errors
))
.into());
}
Err(e) => {
let _user = signup
.complete(&conn, form.username.clone(), form.password.clone())
.map_err(|e| {
warn!("{:?}", e);
return Err(Status::UnprocessableEntity);
}
_ => {}
}
Status::UnprocessableEntity
})?;
Ok(FlashRedirect(Flash::success(
Redirect::to(uri!(super::session::new: m = _)),
i18n!(

View file

@ -51,7 +51,7 @@ pub fn index(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
}
#[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()?;
Ok(render!(instance::admin(
&(&conn, &rockets).to_context(),
@ -105,7 +105,7 @@ pub fn update_settings(
Instance::get_local().expect("instance::update_settings: local instance error");
instance
.update(
&conn,
&*conn,
form.name.clone(),
form.open_registrations,
form.short_description.clone(),
@ -160,7 +160,7 @@ pub fn toggle_block(
))
}
#[get("/admin/users?<page>", rank = 2)]
#[get("/admin/users?<page>")]
pub fn admin_users(
_mod: Moderator,
page: Option<Page>,
@ -171,30 +171,6 @@ pub fn admin_users(
Ok(render!(instance::users(
&(&conn, &rockets).to_context(),
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::total(User::count_local(&conn)? as i32)
)))
@ -390,8 +366,8 @@ pub fn edit_users(
}
fn ban(id: i32, conn: &Connection, worker: &ScheduledThreadPool) -> Result<(), ErrorPage> {
let u = User::get(conn, id)?;
u.delete(conn)?;
let u = User::get(&*conn, id)?;
u.delete(&*conn)?;
if Instance::get_local()
.map(|i| u.instance_id == i.id)
.unwrap_or(false)
@ -406,8 +382,8 @@ fn ban(id: i32, conn: &Connection, worker: &ScheduledThreadPool) -> Result<(), E
},
)
.unwrap();
let target = User::one_by_instance(conn)?;
let delete_act = u.delete_activity(conn)?;
let target = User::one_by_instance(&*conn)?;
let delete_act = u.delete_activity(&*conn)?;
worker.execute(move || broadcast(&u, delete_act, target, CONFIG.proxy().cloned()));
}

View file

@ -20,14 +20,14 @@ pub fn create(
let b = Blog::find_by_fqn(&conn, &blog)?;
let post = Post::find_by_slug(&conn, &slug, b.id)?;
if !user.has_liked(&conn, &post)? {
let like = likes::Like::insert(&conn, likes::NewLike::new(&post, &user))?;
like.notify(&conn)?;
if !user.has_liked(&*conn, &post)? {
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post, &user))?;
like.notify(&*conn)?;
Timeline::add_to_all_timelines(&conn, &post, Kind::Like(&user))?;
let dest = User::one_by_instance(&conn)?;
let act = like.to_activity(&conn)?;
let dest = User::one_by_instance(&*conn)?;
let act = like.to_activity(&*conn)?;
rockets
.worker
.execute(move || broadcast(&user, act, dest, CONFIG.proxy().cloned()));

View file

@ -2,7 +2,7 @@ use crate::routes::{errors::ErrorPage, Page};
use crate::template_utils::{IntoContext, Ructe};
use guid_create::GUID;
use multipart::server::{
save::{SaveResult, SavedField, SavedData},
save::{SaveResult, SavedData},
Multipart,
};
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() {
let fields = entries.fields;
let file = fields
let filename = fields
.get("file")
.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) {
Ok(Some(file_path)) => file_path,
Ok(None) => return Ok(Redirect::to(uri!(new))),
Err(_) => return Err(status::BadRequest(Some("Couldn't save uploaded media: {}"))),
};
match fields["file"][0].data {
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
.map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
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)
.map(|cw| cw.is_empty())
@ -72,7 +97,7 @@ pub fn upload(
let media = Media::insert(
&conn,
NewMedia {
file_path,
file_path: dest,
alt_text: read(&fields["alt"][0].data)?,
is_remote: false,
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>> {
if let SavedData::Text(s) = data {
Ok(s.clone())
@ -188,9 +145,9 @@ pub fn details(
#[post("/medias/<id>/delete")]
pub fn delete(id: i32, user: User, conn: DbConn, intl: I18n) -> Result<Flash<Redirect>, ErrorPage> {
let media = Media::get(&conn, id)?;
let media = Media::get(&*conn, id)?;
if media.owner_id == user.id {
media.delete(&conn)?;
media.delete(&*conn)?;
Ok(Flash::success(
Redirect::to(uri!(list: page = _)),
i18n!(intl.catalog, "Your media have been deleted."),
@ -210,9 +167,9 @@ pub fn set_avatar(
conn: DbConn,
intl: I18n,
) -> Result<Flash<Redirect>, ErrorPage> {
let media = Media::get(&conn, id)?;
let media = Media::get(&*conn, id)?;
if media.owner_id == user.id {
user.set_avatar(&conn, media.id)?;
user.set_avatar(&*conn, media.id)?;
Ok(Flash::success(
Redirect::to(uri!(details: id = id)),
i18n!(intl.catalog, "Your avatar has been updated."),

View file

@ -21,9 +21,6 @@ use std::{
path::{Path, PathBuf},
};
#[cfg(feature = "s3")]
use rocket::http::ContentType;
/// Special return type used for routes that "cannot fail", and instead
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
#[allow(clippy::large_enum_variant)]
@ -168,7 +165,7 @@ fn post_to_atom(post: Post, conn: &Connection) -> Entry {
.build(),
)
.authors(
post.get_authors(conn)
post.get_authors(&*conn)
.expect("Atom feed: author error")
.into_iter()
.map(|a| {
@ -207,17 +204,10 @@ pub mod timelines;
pub mod user;
pub mod well_known;
#[derive(Responder)]
enum FileKind {
Local(NamedFile),
#[cfg(feature = "s3")]
S3(Vec<u8>, ContentType),
}
#[derive(Responder)]
#[response()]
pub struct CachedFile {
inner: FileKind,
inner: NamedFile,
cache_control: CacheControl,
}
@ -263,41 +253,19 @@ pub fn plume_static_files(file: PathBuf, build_id: &RawStr) -> Option<CachedFile
}
#[get("/static/media/<file..>")]
pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
if CONFIG.s3.is_some() {
#[cfg(not(feature="s3"))]
unreachable!();
#[cfg(feature="s3")]
{
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)]),
})
}
NamedFile::open(Path::new(&CONFIG.media_directory).join(file))
.ok()
.map(|f| CachedFile {
inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
})
}
#[get("/static/<file..>", rank = 3)]
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
NamedFile::open(Path::new("static/").join(file))
.ok()
.map(|f| CachedFile {
inner: FileKind::Local(f),
inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
})
}

View file

@ -65,7 +65,7 @@ pub fn search(query: Option<Form<SearchQuery>>, conn: DbConn, rockets: PlumeRock
if str_query.is_empty() {
render!(search::index(
&(&conn, &rockets).to_context(),
&format!("{}", Utc::now().date_naive().format("%Y-%m-d"))
&format!("{}", Utc::today().format("%Y-%m-d"))
))
} else {
let res = rockets

View file

@ -48,10 +48,10 @@ pub fn me(user: Option<User>) -> RespondOrRedirect {
#[get("/@/<name>", rank = 2)]
pub fn details(name: String, rockets: PlumeRocket, conn: DbConn) -> Result<Ructe, ErrorPage> {
let user = User::find_by_fqn(&conn, &name)?;
let recents = Post::get_recents_for_author(&conn, &user, 6)?;
let reshares = Reshare::get_recents_for_author(&conn, &user, 6)?;
let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
if !user.get_instance(&conn)?.local {
if !user.get_instance(&*conn)?.local {
tracing::trace!("remote user found");
user.remote_user_found(); // Doesn't block
}
@ -62,14 +62,14 @@ pub fn details(name: String, rockets: PlumeRocket, conn: DbConn) -> Result<Ructe
rockets
.user
.clone()
.and_then(|x| x.is_following(&conn, user.id).ok())
.and_then(|x| x.is_following(&*conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local()?.id,
user.get_instance(&conn)?.public_domain,
user.get_instance(&*conn)?.public_domain,
recents,
reshares
.into_iter()
.filter_map(|r| r.get_post(&conn).ok())
.filter_map(|r| r.get_post(&*conn).ok())
.collect()
)))
}

View file

@ -50,10 +50,10 @@ impl Resolver<DbConn> for WebfingerResolver {
fn find(prefix: Prefix, acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
match prefix {
Prefix::Acct => User::find_by_fqn(&conn, &acct)
.and_then(|usr| usr.webfinger(&conn))
.and_then(|usr| usr.webfinger(&*conn))
.or(Err(ResolverError::NotFound)),
Prefix::Group => Blog::find_by_fqn(&conn, &acct)
.and_then(|blog| blog.webfinger(&conn))
.and_then(|blog| blog.webfinger(&*conn))
.or(Err(ResolverError::NotFound)),
Prefix::Custom(_) => Err(ResolverError::NotFound),
}

View file

@ -85,7 +85,7 @@ impl<'r> Responder<'r> for Ructe {
macro_rules! render {
($group:tt :: $page:tt ( $( $param:expr ),* ) ) => {
{
use $crate::templates;
use crate::templates;
let mut res = vec![];
templates::$group::$page(

View file

@ -32,7 +32,7 @@
<hr/>
@:header()
</nav>
<nav class="right-nav">
<nav>
@if ctx.2.is_some() {
<a href="@uri!(search::search: _)">
<i class="icon icon-search"></i>
@ -87,8 +87,6 @@
<a href="@uri!(instance::privacy)">@i18n!(ctx.1, "Privacy policy")</a>
@if ctx.2.clone().map(|u| u.is_admin()).unwrap_or(false) {
<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>

View file

@ -1,6 +1,6 @@
@use plume_models::instance::Instance;
@use validator::ValidationErrors;
@use crate::templates::{base, instance::admin_header};
@use crate::templates::base;
@use crate::template_utils::*;
@use crate::routes::instance::InstanceSettingsForm;
@use crate::routes::*;
@ -8,7 +8,14 @@
@(ctx: BaseContext, instance: Instance, form: InstanceSettingsForm, errors: ValidationErrors)
@: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)">
@(Input::new("name", i18n!(ctx.1, "Name"))

View file

@ -1,21 +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)
])
}

View file

@ -1,8 +1,15 @@
@use crate::templates::{base, instance::admin_header};
@use crate::templates::base;
@use crate::template_utils::*;
@use crate::routes::*;
@(ctx: BaseContext)
@: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),
])
})

View file

@ -1,11 +1,17 @@
@use plume_models::blocklisted_emails::BlocklistedEmail;
@use crate::templates::{base, instance::admin_header};
@use crate::templates::base;
@use crate::template_utils::*;
@use crate::routes::*;
@(ctx:BaseContext, emails: Vec<BlocklistedEmail>, page:i32, n_pages:i32)
@:base(ctx, i18n!(ctx.1, "Blocklisted Emails"), {}, {}, {
@:admin_header(ctx, "Blocklisted Emails", 4)
@:base(ctx, i18n!(ctx.1, "Blocklisted Emails"), {}, {}, {
<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)">
@(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"))

View file

@ -1,12 +1,19 @@
@use plume_models::instance::Instance;
@use crate::templates::{base, instance::admin_header};
@use crate::templates::base;
@use crate::template_utils::*;
@use crate::routes::*;
@(ctx: BaseContext, instance: Instance, instances: Vec<Instance>, page: i32, n_pages: i32)
@: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">
@for instance in instances {

View file

@ -1,19 +1,19 @@
@use plume_models::users::User;
@use crate::templates::{base, instance::admin_header};
@use crate::templates::base;
@use crate::template_utils::*;
@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"), {}, {}, {
@: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())">
<header>
<input type="search" name="user" value="@user.unwrap_or_default()">
<input type="submit" value="@i18n!(ctx.1, "Search users")">
</header>
</form>
@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"), true),
(&uri!(instance::admin_email_blocklist: page=_).to_string(), i18n!(ctx.1, "Email blocklist"), false)
])
<form method="post" action="@uri!(instance::edit_users)">
<header>
@ -46,9 +46,5 @@
}
</div>
</form>
@if user.is_some() {
@paginate_param(ctx.1, page, n_pages, Some(format!("user={}", encode_query_param(user.unwrap_or_default()))))
} else {
@paginate(ctx.1, page, n_pages)
}
@paginate(ctx.1, page, n_pages)
})