Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39edca5edc | |||
| bce806ac63 | |||
| 3669a0097d | |||
| cc998e7c61 | |||
| 4142e73018 | |||
| 5d03331f0c | |||
| 3198f30515 |
@@ -10,7 +10,7 @@ executors:
|
|||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
docker:
|
docker:
|
||||||
- image: plumeorg/plume-buildenv:v0.0.9
|
- image: plumeorg/plume-buildenv:v0.0.7
|
||||||
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
- image: <<#parameters.postgres>>circleci/postgres:9.6-alpine<</parameters.postgres>><<^parameters.postgres>>alpine:latest<</parameters.postgres>>
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
@@ -19,7 +19,7 @@ executors:
|
|||||||
working_directory: ~/projects/Plume
|
working_directory: ~/projects/Plume
|
||||||
environment:
|
environment:
|
||||||
RUST_TEST_THREADS: 1
|
RUST_TEST_THREADS: 1
|
||||||
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>,ci
|
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
|
||||||
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
|
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ commands:
|
|||||||
type: string
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- run: zip -0 ccov.zip `find . -name 'plume*.gc*' -o -name 'plm*.gc*'`
|
- run: zip -0 ccov.zip `find . -name 'plume*.gc*' -o -name 'plm*.gc*'`
|
||||||
- run: grcov ccov.zip -s . -t lcov --llvm --branch --ignore-not-existing --ignore '/*' -o lcov.info
|
- run: grcov ccov.zip -s . -t lcov --llvm --branch --ignore-not-existing --ignore-dir '/*' -o lcov.info
|
||||||
- run: bash <(curl -s https://codecov.io/bash) -f lcov.info -F <<parameters.type>>
|
- run: bash <(curl -s https://codecov.io/bash) -f lcov.info -F <<parameters.type>>
|
||||||
- run: find . -name 'plume*.gc*' -delete -o -name 'plm*.gc*' -delete
|
- run: find . -name 'plume*.gc*' -delete -o -name 'plm*.gc*' -delete
|
||||||
- run: rm ccov.zip lcov.info
|
- run: rm ccov.zip lcov.info
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ ENV PATH="/root/.cargo/bin:${PATH}"
|
|||||||
#install native/circleci/build dependancies
|
#install native/circleci/build dependancies
|
||||||
RUN apt update &&\
|
RUN apt update &&\
|
||||||
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
|
apt install -y --no-install-recommends git ssh tar gzip ca-certificates default-jre&&\
|
||||||
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip libclang-dev&&\
|
apt install -y --no-install-recommends binutils-dev build-essential cmake curl gcc gettext git libcurl4-openssl-dev libdw-dev libelf-dev libiberty-dev libpq-dev libsqlite3-dev libssl-dev make openssl pkg-config postgresql postgresql-contrib python zlib1g-dev python3-pip zip unzip &&\
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
#install and configure rust
|
#install and configure rust
|
||||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-01-15 -y &&\
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-03-23 -y &&\
|
||||||
rustup component add rustfmt clippy &&\
|
rustup component add rustfmt clippy &&\
|
||||||
rustup component add rust-std --target wasm32-unknown-unknown
|
rustup component add rust-std --target wasm32-unknown-unknown
|
||||||
|
|
||||||
|
|||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
# This file contains your instance configuration
|
|
||||||
# Some documentation about these variables is available here:
|
|
||||||
# https://docs.joinplu.me/environment/
|
|
||||||
|
|
||||||
## GENERAL SETTINGS ##
|
|
||||||
|
|
||||||
# The directory containing database migrations
|
|
||||||
# For Postgres: migrations/postgres
|
|
||||||
# For SQlite: migrations/sqlite
|
|
||||||
MIGRATION_DIRECTORY=migrations/postgres
|
|
||||||
|
|
||||||
# The URL of your database (or its path for SQlite databases)
|
|
||||||
DATABASE_URL=postgres://plume:plume@localhost/plume
|
|
||||||
|
|
||||||
# The domain of your instance
|
|
||||||
BASE_URL=plu.me
|
|
||||||
|
|
||||||
# The secret key for private cookies and CSRF protection
|
|
||||||
# You can generate one with `openssl rand -base64 32`
|
|
||||||
ROCKET_SECRET_KEY=
|
|
||||||
|
|
||||||
# Port and address which Plume will use
|
|
||||||
ROCKET_PORT=7878
|
|
||||||
ROCKET_ADDRESS=127.0.0.1
|
|
||||||
|
|
||||||
## MAIL CONFIG ##
|
|
||||||
#MAIL_SERVER=smtp.plu.me
|
|
||||||
#MAIL_ADDRESS=no-reply@plu.me
|
|
||||||
#MAIL_USER=plume
|
|
||||||
#MAIL_PASSWORD=
|
|
||||||
#MAIL_HELO_NAME=no-reply@plu.me
|
|
||||||
|
|
||||||
## ADVANCED OPTIONS ##
|
|
||||||
#MEDIA_UPLOAD_DIRECTORY=static/media
|
|
||||||
#SEARCH_INDEX=search_index
|
|
||||||
|
|
||||||
# Sample logo configuration
|
|
||||||
#PLUME_LOGO=icons/trwnh/paragraphs/plumeParagraphs.svg
|
|
||||||
#PLUME_LOGO_FAVICON=icons/trwnh/paragraphs/plumeParagraphs32.png
|
|
||||||
#PLUME_LOGO_48=icons/trwnh/paragraphs/plumeParagraphs48.png
|
|
||||||
#PLUME_LOGO_72=icons/trwnh/paragraphs/plumeParagraphs72.png
|
|
||||||
#PLUME_LOGO_96=icons/trwnh/paragraphs/plumeParagraphs96.png
|
|
||||||
#PLUME_LOGO_144=icons/trwnh/paragraphs/plumeParagraphs144.png
|
|
||||||
#PLUME_LOGO_160=icons/trwnh/paragraphs/plumeParagraphs160.png
|
|
||||||
#PLUME_LOGO_192=icons/trwnh/paragraphs/plumeParagraphs192.png
|
|
||||||
#PLUME_LOGO_256=icons/trwnh/paragraphs/plumeParagraphs256.png
|
|
||||||
#PLUME_LOGO_512=icons/trwnh/paragraphs/plumeParagraphs512.png
|
|
||||||
+4
-1
@@ -7,7 +7,7 @@ translations
|
|||||||
.env
|
.env
|
||||||
Rocket.toml
|
Rocket.toml
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
static
|
static/media
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
@@ -16,5 +16,8 @@ docker-compose.yml
|
|||||||
tags.*
|
tags.*
|
||||||
!tags.rs
|
!tags.rs
|
||||||
search_index
|
search_index
|
||||||
|
main.css
|
||||||
|
*.wasm
|
||||||
|
*.js
|
||||||
.buildconfig
|
.buildconfig
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|||||||
Generated
+252
-740
File diff suppressed because it is too large
Load Diff
+13
-16
@@ -1,9 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
name = "plume"
|
name = "plume"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
repository = "https://github.com/Plume-org/Plume"
|
repository = "https://github.com/Plume-org/Plume"
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
activitypub = "0.1.3"
|
activitypub = "0.1.3"
|
||||||
@@ -17,20 +16,19 @@ gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "
|
|||||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
guid-create = "0.1"
|
guid-create = "0.1"
|
||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
lettre = "0.9.2"
|
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||||
lettre_email = "0.9.2"
|
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||||
num_cpus = "1.10"
|
num_cpus = "1.0"
|
||||||
rocket = "0.4.2"
|
rocket = "0.4.0"
|
||||||
rocket_contrib = { version = "0.4.2", features = ["json"] }
|
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
rpassword = "4.0"
|
rpassword = "3.0"
|
||||||
runtime-fmt = "0.4.0"
|
runtime-fmt = "0.3.0"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_qs = "0.5"
|
serde_qs = "0.4"
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
syntect = "3.3"
|
|
||||||
validator = "0.8"
|
validator = "0.8"
|
||||||
validator_derive = "0.8"
|
validator_derive = "0.8"
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
@@ -66,11 +64,11 @@ path = "plume-common"
|
|||||||
path = "plume-models"
|
path = "plume-models"
|
||||||
|
|
||||||
[dependencies.rocket_csrf]
|
[dependencies.rocket_csrf]
|
||||||
git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
git = "https://github.com/Plume-org/rocket_csrf"
|
||||||
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
rev = "89ecb380266234f858c651354216bf5bf3cc09b2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = "0.9.0"
|
ructe = "0.6.2"
|
||||||
rsass = "0.9"
|
rsass = "0.9"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
@@ -79,7 +77,6 @@ postgres = ["plume-models/postgres", "diesel/postgres"]
|
|||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
debug-mailer = []
|
debug-mailer = []
|
||||||
test = []
|
test = []
|
||||||
ci = ["plume-models/ci", "plume-api/ci", "plume-common/ci"]
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
|
||||||
|
|||||||
+1
-2
@@ -10,8 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
gcc \
|
gcc \
|
||||||
make \
|
make \
|
||||||
openssl \
|
openssl \
|
||||||
libssl-dev \
|
libssl-dev
|
||||||
clang
|
|
||||||
|
|
||||||
WORKDIR /scratch
|
WORKDIR /scratch
|
||||||
COPY script/wasm-deps.sh .
|
COPY script/wasm-deps.sh .
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="https://raw.githubusercontent.com/Plume-org/Plume/master/assets/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
|
<img src="https://raw.githubusercontent.com/Plume-org/Plume/master/static/icons/trwnh/feather/plumeFeather64.png" alt="Plume's logo">
|
||||||
Plume
|
Plume
|
||||||
</h1>
|
</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
* {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
@import '_variables';
|
|
||||||
|
|
||||||
/* Color Scheme */
|
|
||||||
$gray: #1a3854;
|
|
||||||
$black: #102e4a;
|
|
||||||
$white: #F8F8F8;
|
|
||||||
$purple: #7765E3;
|
|
||||||
$lightpurple: #c2bbee;
|
|
||||||
$red: #d16666;
|
|
||||||
$yellow: #ff934f;
|
|
||||||
$blue: #7f96ff;
|
|
||||||
|
|
||||||
$background: $black;
|
|
||||||
$form-input-background: $gray;
|
|
||||||
$form-input-border: $white;
|
|
||||||
$text-color: $white;
|
|
||||||
$primary: $purple;
|
|
||||||
$primary-text-color: $white; // text color on primary background (buttons for instance)
|
|
||||||
$success-color: $blue;
|
|
||||||
|
|
||||||
//Code Highlighting
|
|
||||||
|
|
||||||
$code-keyword-color: #f79ac1;
|
|
||||||
$code-source-color: #a6f0ab;
|
|
||||||
$code-constant-color: #dfec84;
|
|
||||||
$code-operator-color: #eddf95;
|
|
||||||
$code-string-color: #f2ae60;
|
|
||||||
$code-comment-color: #a3b4f9;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/* Color Scheme */
|
|
||||||
$gray: #F3F3F3;
|
|
||||||
$black: #242424;
|
|
||||||
$white: #F8F8F8;
|
|
||||||
$purple: #7765E3;
|
|
||||||
$lightpurple: #c2bbee;
|
|
||||||
$red: #E92F2F;
|
|
||||||
$yellow: #ffe347;
|
|
||||||
$green: #23f0c7;
|
|
||||||
|
|
||||||
$background: $white;
|
|
||||||
$form-input-background: white;
|
|
||||||
$form-input-border: $black;
|
|
||||||
$text-color: $black;
|
|
||||||
$primary: $purple;
|
|
||||||
$primary-text-color: $white; // text color on primary background (buttons for instance)
|
|
||||||
$success-color: $green;
|
|
||||||
|
|
||||||
/* Dimensions */
|
|
||||||
|
|
||||||
$article-width: 70ch;
|
|
||||||
$horizontal-margin: 20%;
|
|
||||||
$margin: 0 $horizontal-margin;
|
|
||||||
|
|
||||||
/* Fonts */
|
|
||||||
|
|
||||||
$route159: "Route159", serif;
|
|
||||||
$playfair: "Playfair Display", serif;
|
|
||||||
$lora: "Lora", serif;
|
|
||||||
|
|
||||||
//Code Highlighting
|
|
||||||
$code-keyword-color: #45244a;
|
|
||||||
$code-source-color: #4c588c;
|
|
||||||
$code-constant-color: scale-color(magenta,$lightness:-5%);
|
|
||||||
$code-operator-color: scale-color($code-source-color,$lightness:-5%);
|
|
||||||
$code-string-color: #8a571c;
|
|
||||||
$code-comment-color: #1c4c8a;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
|
|
||||||
|
|
||||||
@import url('./feather.css');
|
|
||||||
@import url('./fonts/Route159/Route159.css');
|
|
||||||
@import url('./fonts/Lora/Lora.css');
|
|
||||||
@import url('./fonts/Playfair_Display/PlayfairDisplay.css');
|
|
||||||
|
|
||||||
@import 'dark_variables';
|
|
||||||
@import 'global';
|
|
||||||
@import 'header';
|
|
||||||
@import 'article';
|
|
||||||
@import 'forms';
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
|
|
||||||
|
|
||||||
@import url('./feather.css');
|
|
||||||
@import url('./fonts/Route159/Route159.css');
|
|
||||||
@import url('./fonts/Lora/Lora.css');
|
|
||||||
@import url('./fonts/Playfair_Display/PlayfairDisplay.css');
|
|
||||||
|
|
||||||
@import 'variables';
|
|
||||||
@import 'global';
|
|
||||||
@import 'header';
|
|
||||||
@import 'article';
|
|
||||||
@import 'forms';
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use rsass;
|
extern crate rsass;
|
||||||
|
extern crate ructe;
|
||||||
use ructe::Ructe;
|
use ructe::*;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::{ffi::OsStr, fs::*, io::Write, path::*};
|
use std::{env, fs::*, io::Write, path::PathBuf};
|
||||||
|
|
||||||
fn compute_static_hash() -> String {
|
fn compute_static_hash() -> String {
|
||||||
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat -c'%n %Y' | openssl dgst -r"
|
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat -c'%n %Y' | openssl dgst -r"
|
||||||
@@ -37,17 +37,25 @@ fn compute_static_hash() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
Ructe::from_env()
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
.expect("This must be run with cargo")
|
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("templates");
|
||||||
.compile_templates("templates")
|
compile_templates(&in_dir, &out_dir).expect("compile templates");
|
||||||
.expect("compile templates");
|
|
||||||
|
|
||||||
compile_themes().expect("Theme compilation error");
|
println!("cargo:rerun-if-changed=static/css/_article.scss");
|
||||||
recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
|
println!("cargo:rerun-if-changed=static/css/_forms.scss");
|
||||||
.expect("Couldn't copy icons");
|
println!("cargo:rerun-if-changed=static/css/_global.scss");
|
||||||
recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
|
println!("cargo:rerun-if-changed=static/css/_header.scss");
|
||||||
.expect("Couldn't copy images");
|
println!("cargo:rerun-if-changed=static/css/_variables.scss");
|
||||||
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
|
println!("cargo:rerun-if-changed=static/css/main.scss");
|
||||||
|
let mut out = File::create("static/css/main.css").expect("Couldn't create main.css");
|
||||||
|
out.write_all(
|
||||||
|
&rsass::compile_scss_file(
|
||||||
|
"static/css/main.scss".as_ref(),
|
||||||
|
rsass::OutputStyle::Compressed,
|
||||||
|
)
|
||||||
|
.expect("Error during SCSS compilation"),
|
||||||
|
)
|
||||||
|
.expect("Couldn't write CSS output");
|
||||||
|
|
||||||
let cache_id = &compute_static_hash()[..8];
|
let cache_id = &compute_static_hash()[..8];
|
||||||
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
|
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
|
||||||
@@ -66,91 +74,3 @@ fn main() {
|
|||||||
|
|
||||||
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
|
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_themes() -> std::io::Result<()> {
|
|
||||||
let input_dir = Path::new("assets").join("themes");
|
|
||||||
let output_dir = Path::new("static").join("css");
|
|
||||||
|
|
||||||
let themes = find_themes(input_dir)?;
|
|
||||||
|
|
||||||
for theme in themes {
|
|
||||||
compile_theme(&theme, &output_dir)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_themes(path: PathBuf) -> std::io::Result<Vec<PathBuf>> {
|
|
||||||
let ext = path.extension().and_then(OsStr::to_str);
|
|
||||||
if metadata(&path)?.is_dir() {
|
|
||||||
Ok(read_dir(&path)?.fold(vec![], |mut themes, ch| {
|
|
||||||
if let Ok(ch) = ch {
|
|
||||||
if let Ok(mut new) = find_themes(ch.path()) {
|
|
||||||
themes.append(&mut new);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
themes
|
|
||||||
}))
|
|
||||||
} else if (ext == Some("scss") || ext == Some("sass"))
|
|
||||||
&& !path.file_name().unwrap().to_str().unwrap().starts_with('_')
|
|
||||||
{
|
|
||||||
Ok(vec![path.clone()])
|
|
||||||
} else {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_theme(path: &Path, out_dir: &Path) -> std::io::Result<()> {
|
|
||||||
let name = path
|
|
||||||
.components()
|
|
||||||
.skip_while(|c| *c != Component::Normal(OsStr::new("themes")))
|
|
||||||
.skip(1)
|
|
||||||
.filter_map(|c| {
|
|
||||||
c.as_os_str()
|
|
||||||
.to_str()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.splitn(2, '.')
|
|
||||||
.next()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("-");
|
|
||||||
|
|
||||||
let dir = path.parent().unwrap();
|
|
||||||
|
|
||||||
let out = out_dir.join(name);
|
|
||||||
create_dir_all(&out)?;
|
|
||||||
|
|
||||||
// copy files of the theme that are not scss
|
|
||||||
for ch in read_dir(&dir)? {
|
|
||||||
recursive_copy(&ch?.path(), &out)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// compile the .scss/.sass file
|
|
||||||
let mut out = File::create(out.join("theme.css"))?;
|
|
||||||
out.write_all(
|
|
||||||
&rsass::compile_scss_file(path, rsass::OutputStyle::Compressed)
|
|
||||||
.expect("SCSS compilation error"),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recursive_copy(path: &Path, out_dir: &Path) -> std::io::Result<()> {
|
|
||||||
if metadata(path)?.is_dir() {
|
|
||||||
let out = out_dir.join(path.file_name().unwrap());
|
|
||||||
create_dir_all(out.clone())?;
|
|
||||||
|
|
||||||
for ch in read_dir(path)? {
|
|
||||||
recursive_copy(&ch?.path(), &out)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("cargo:rerun-if-changed={}", path.display());
|
|
||||||
|
|
||||||
let ext = path.extension().and_then(OsStr::to_str);
|
|
||||||
if ext != Some("scss") && ext != Some("sass") {
|
|
||||||
copy(path, out_dir.join(path.file_name().unwrap()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
--#!|_conn, path: &Path| {
|
||||||
|
--#! let mut pb = path.to_path_buf();
|
||||||
|
--#! pb.push("search_index");
|
||||||
|
--#! std::fs::remove_dir_all(pb).map_err(Error::from)
|
||||||
|
--#!}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
--#!|conn: &Connection, path: &Path| {
|
||||||
|
--#! let mut pb = path.to_path_buf();
|
||||||
|
--#! pb.push("search_index");
|
||||||
|
--#! let searcher = super::search::Searcher::create(&pb)?;
|
||||||
|
--#! searcher.fill(conn)?;
|
||||||
|
--#! searcher.commit();
|
||||||
|
--#! Ok(())
|
||||||
|
--#!}
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT 'f';
|
|
||||||
UPDATE users SET is_admin = 't' WHERE role = 0;
|
|
||||||
ALTER TABLE users DROP COLUMN role;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
ALTER TABLE users ADD COLUMN role INTEGER NOT NULL DEFAULT 2;
|
|
||||||
UPDATE users SET role = 0 WHERE is_admin = 't';
|
|
||||||
ALTER TABLE users DROP COLUMN is_admin;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
ALTER TABLE blogs DROP COLUMN theme;
|
|
||||||
ALTER TABLE users DROP COLUMN preferred_theme;
|
|
||||||
ALTER TABLE users DROP COLUMN hide_custom_css;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
ALTER TABLE blogs ADD COLUMN theme VARCHAR;
|
|
||||||
ALTER TABLE users ADD COLUMN preferred_theme VARCHAR;
|
|
||||||
ALTER TABLE users ADD COLUMN hide_custom_css BOOLEAN NOT NULL DEFAULT 'f';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
|
|
||||||
DROP TABLE timeline;
|
|
||||||
DROP TABLE timeline_definition;
|
|
||||||
DROP TABLE list_elems;
|
|
||||||
DROP TABLE lists;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
|
|
||||||
CREATE TABLE timeline_definition(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id integer REFERENCES users ON DELETE CASCADE,
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
query VARCHAR NOT NULL,
|
|
||||||
CONSTRAINT timeline_unique_user_name UNIQUE(user_id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE timeline(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
post_id integer NOT NULL REFERENCES posts ON DELETE CASCADE,
|
|
||||||
timeline_id integer NOT NULL REFERENCES timeline_definition ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE lists(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
user_id integer REFERENCES users ON DELETE CASCADE,
|
|
||||||
type integer NOT NULL,
|
|
||||||
CONSTRAINT list_unique_user_name UNIQUE(user_id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE list_elems(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
list_id integer NOT NULL REFERENCES lists ON DELETE CASCADE,
|
|
||||||
user_id integer REFERENCES users ON DELETE CASCADE,
|
|
||||||
blog_id integer REFERENCES blogs ON DELETE CASCADE,
|
|
||||||
word VARCHAR
|
|
||||||
);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
DELETE FROM timeline_definition WHERE name = 'Your feed';
|
|
||||||
DELETE FROM timeline_definition WHERE name = 'Local feed' AND query = 'local';
|
|
||||||
DELETE FROM timeline_definition WHERE name = 'Federared feed' AND query = 'all';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
INSERT INTO timeline_definition (name, query) VALUES
|
|
||||||
('Local feed', 'local'),
|
|
||||||
('Federated feed', 'all');
|
|
||||||
INSERT INTO timeline_definition (user_id,name,query)
|
|
||||||
select id,'Your feed',CONCAT('followed or [',fqn,']') from users;
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DELETE FROM apps WHERE name = 'Plume web interface';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
--#!|conn: &Connection, path: &Path| {
|
||||||
|
--#! use plume_common::utils::random_hex;
|
||||||
|
--#!
|
||||||
|
--#! let client_id = random_hex();
|
||||||
|
--#! let client_secret = random_hex();
|
||||||
|
--#! let app = crate::apps::App::insert(
|
||||||
|
--#! &*conn,
|
||||||
|
--#! crate::apps::NewApp {
|
||||||
|
--#! name: "Plume web interface".into(),
|
||||||
|
--#! client_id,
|
||||||
|
--#! client_secret,
|
||||||
|
--#! redirect_uri: None,
|
||||||
|
--#! website: Some("https://joinplu.me".into()),
|
||||||
|
--#! },
|
||||||
|
--#! ).unwrap();
|
||||||
|
--#!
|
||||||
|
--#! for i in 0..=(crate::users::User::count_local(conn).unwrap() as i32 / 20) {
|
||||||
|
--#! if let Ok(page) = crate::users::User::get_local_page(conn, (i * 20, (i + 1) * 20)) {
|
||||||
|
--#! for user in page {
|
||||||
|
--#! crate::api_tokens::ApiToken::insert(
|
||||||
|
--#! conn,
|
||||||
|
--#! crate::api_tokens::NewApiToken {
|
||||||
|
--#! app_id: app.id,
|
||||||
|
--#! user_id: user.id,
|
||||||
|
--#! value: random_hex(),
|
||||||
|
--#! scopes: "read+write".into(),
|
||||||
|
--#! },
|
||||||
|
--#! ).unwrap();
|
||||||
|
--#! }
|
||||||
|
--#! }
|
||||||
|
--#! }
|
||||||
|
--#!
|
||||||
|
--#! Ok(())
|
||||||
|
--#!}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
DELETE FROM timeline WHERE id IN
|
|
||||||
(
|
|
||||||
SELECT timeline.id FROM timeline
|
|
||||||
INNER JOIN timeline_definition ON timeline.timeline_id = timeline_definition.id
|
|
||||||
WHERE timeline_definition.query LIKE 'followed or [%]' OR
|
|
||||||
timeline_definition.query = 'local' OR
|
|
||||||
timeline_definition.query = 'all'
|
|
||||||
);
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
INSERT INTO timeline (post_id, timeline_id)
|
|
||||||
SELECT posts.id,timeline_definition.id FROM posts,timeline_definition
|
|
||||||
WHERE timeline_definition.query = 'all';
|
|
||||||
|
|
||||||
INSERT INTO timeline (post_id, timeline_id)
|
|
||||||
SELECT posts.id,timeline_definition.id FROM posts
|
|
||||||
CROSS JOIN timeline_definition
|
|
||||||
INNER JOIN blogs ON posts.blog_id = blogs.id
|
|
||||||
INNER JOIN instances ON blogs.instance_id = instances.id
|
|
||||||
WHERE timeline_definition.query = 'local' and instances.local = true;
|
|
||||||
|
|
||||||
INSERT INTO timeline (post_id, timeline_id)
|
|
||||||
SELECT posts.id,timeline_definition.id FROM posts
|
|
||||||
INNER JOIN blog_authors ON posts.blog_id = blog_authors.blog_id
|
|
||||||
LEFT JOIN follows ON blog_authors.author_id = follows.following_id
|
|
||||||
INNER JOIN timeline_definition ON follows.follower_id = timeline_definition.user_id
|
|
||||||
or blog_authors.author_id = timeline_definition.user_id
|
|
||||||
WHERE timeline_definition.query LIKE 'followed or [%]';
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
|
|
||||||
drop table email_blocklist;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
CREATE TABLE email_blocklist(id SERIAL PRIMARY KEY,
|
|
||||||
email_address TEXT UNIQUE,
|
|
||||||
note TEXT,
|
|
||||||
notify_user BOOLEAN DEFAULT FALSE,
|
|
||||||
notification_text TEXT);
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
--#!|_conn, path: &Path| {
|
||||||
|
--#! let mut pb = path.to_path_buf();
|
||||||
|
--#! pb.push("search_index");
|
||||||
|
--#! std::fs::remove_dir_all(pb).map_err(Error::from)
|
||||||
|
--#!}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
--#!|conn: &Connection, path: &Path| {
|
||||||
|
--#! let mut pb = path.to_path_buf();
|
||||||
|
--#! pb.push("search_index");
|
||||||
|
--#! let searcher = super::search::Searcher::create(&pb)?;
|
||||||
|
--#! searcher.fill(conn)?;
|
||||||
|
--#! searcher.commit();
|
||||||
|
--#! Ok(())
|
||||||
|
--#!}
|
||||||
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
CREATE TABLE IF NOT EXISTS "users_without_role" (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username VARCHAR NOT NULL,
|
|
||||||
display_name VARCHAR NOT NULL DEFAULT '',
|
|
||||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
is_admin BOOLEAN NOT NULL DEFAULT 'f',
|
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
|
||||||
email TEXT,
|
|
||||||
hashed_password TEXT,
|
|
||||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
|
||||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ap_url TEXT NOT NULL default '' UNIQUE,
|
|
||||||
private_key TEXT,
|
|
||||||
public_key TEXT NOT NULL DEFAULT '',
|
|
||||||
shared_inbox_url VARCHAR,
|
|
||||||
followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE,
|
|
||||||
avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE,
|
|
||||||
last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
fqn TEXT NOT NULL DEFAULT '',
|
|
||||||
summary_html TEXT NOT NULL DEFAULT '',
|
|
||||||
FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL,
|
|
||||||
CONSTRAINT blog_authors_unique UNIQUE (username, instance_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO users_without_role SELECT
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
display_name,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
't',
|
|
||||||
summary,
|
|
||||||
email,
|
|
||||||
hashed_password,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
shared_inbox_url,
|
|
||||||
followers_endpoint,
|
|
||||||
avatar_id,
|
|
||||||
last_fetched_date,
|
|
||||||
fqn,
|
|
||||||
summary
|
|
||||||
FROM users WHERE role = 0;
|
|
||||||
INSERT INTO users_without_role SELECT
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
display_name,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
'f',
|
|
||||||
summary,
|
|
||||||
email,
|
|
||||||
hashed_password,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
shared_inbox_url,
|
|
||||||
followers_endpoint,
|
|
||||||
avatar_id,
|
|
||||||
last_fetched_date,
|
|
||||||
fqn,
|
|
||||||
summary
|
|
||||||
FROM users WHERE role != 0;
|
|
||||||
DROP TABLE users;
|
|
||||||
ALTER TABLE users_without_role RENAME TO users;
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "users_with_role" (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username VARCHAR NOT NULL,
|
|
||||||
display_name VARCHAR NOT NULL DEFAULT '',
|
|
||||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
|
||||||
email TEXT,
|
|
||||||
hashed_password TEXT,
|
|
||||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
|
||||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ap_url TEXT NOT NULL default '' UNIQUE,
|
|
||||||
private_key TEXT,
|
|
||||||
public_key TEXT NOT NULL DEFAULT '',
|
|
||||||
shared_inbox_url VARCHAR,
|
|
||||||
followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE,
|
|
||||||
avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE,
|
|
||||||
last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
fqn TEXT NOT NULL DEFAULT '',
|
|
||||||
summary_html TEXT NOT NULL DEFAULT '',
|
|
||||||
role INTEGER NOT NULL DEFAULT 2,
|
|
||||||
FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL,
|
|
||||||
CONSTRAINT blog_authors_unique UNIQUE (username, instance_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO users_with_role SELECT
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
display_name,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
summary,
|
|
||||||
email,
|
|
||||||
hashed_password,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
shared_inbox_url,
|
|
||||||
followers_endpoint,
|
|
||||||
avatar_id,
|
|
||||||
last_fetched_date,
|
|
||||||
fqn,
|
|
||||||
summary,
|
|
||||||
0
|
|
||||||
FROM users WHERE is_admin = 't';
|
|
||||||
INSERT INTO users_with_role SELECT
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
display_name,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
summary,
|
|
||||||
email,
|
|
||||||
hashed_password,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
shared_inbox_url,
|
|
||||||
followers_endpoint,
|
|
||||||
avatar_id,
|
|
||||||
last_fetched_date,
|
|
||||||
fqn,
|
|
||||||
summary,
|
|
||||||
2
|
|
||||||
FROM users WHERE is_admin = 'f';
|
|
||||||
DROP TABLE users;
|
|
||||||
ALTER TABLE users_with_role RENAME TO users;
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
|
|
||||||
CREATE TABLE blogs_before_themes (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
actor_id VARCHAR NOT NULL,
|
|
||||||
title VARCHAR NOT NULL,
|
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
|
||||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
|
||||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ap_url text not null default '' UNIQUE,
|
|
||||||
private_key TEXT,
|
|
||||||
public_key TEXT NOT NULL DEFAULT '',
|
|
||||||
fqn TEXT NOT NULL DEFAULT '',
|
|
||||||
summary_html TEXT NOT NULL DEFAULT '',
|
|
||||||
icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
|
||||||
banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
|
||||||
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
|
|
||||||
);
|
|
||||||
INSERT INTO blogs_before_themes SELECT
|
|
||||||
id,
|
|
||||||
actor_id,
|
|
||||||
title,
|
|
||||||
summary,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
fqn,
|
|
||||||
summary_html,
|
|
||||||
icon_id,
|
|
||||||
banner_id
|
|
||||||
FROM blogs;
|
|
||||||
DROP TABLE blogs;
|
|
||||||
ALTER TABLE blogs_before_themes RENAME TO blogs;
|
|
||||||
|
|
||||||
CREATE TABLE users_before_themes (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username VARCHAR NOT NULL,
|
|
||||||
display_name VARCHAR NOT NULL DEFAULT '',
|
|
||||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
|
||||||
email TEXT,
|
|
||||||
hashed_password TEXT,
|
|
||||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
|
||||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ap_url TEXT NOT NULL default '' UNIQUE,
|
|
||||||
private_key TEXT,
|
|
||||||
public_key TEXT NOT NULL DEFAULT '',
|
|
||||||
shared_inbox_url VARCHAR,
|
|
||||||
followers_endpoint VARCHAR NOT NULL DEFAULT '' UNIQUE,
|
|
||||||
avatar_id INTEGER REFERENCES medias(id) ON DELETE CASCADE,
|
|
||||||
last_fetched_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
fqn TEXT NOT NULL DEFAULT '',
|
|
||||||
summary_html TEXT NOT NULL DEFAULT '',
|
|
||||||
role INTEGER NOT NULL DEFAULT 2,
|
|
||||||
FOREIGN KEY (avatar_id) REFERENCES medias(id) ON DELETE SET NULL,
|
|
||||||
CONSTRAINT blog_authors_unique UNIQUE (username, instance_id)
|
|
||||||
);
|
|
||||||
INSERT INTO users_before_themes SELECT
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
display_name,
|
|
||||||
outbox_url,
|
|
||||||
inbox_url,
|
|
||||||
summary,
|
|
||||||
email,
|
|
||||||
hashed_password,
|
|
||||||
instance_id,
|
|
||||||
creation_date,
|
|
||||||
ap_url,
|
|
||||||
private_key,
|
|
||||||
public_key,
|
|
||||||
shared_inbox_url,
|
|
||||||
followers_endpoint,
|
|
||||||
avatar_id,
|
|
||||||
last_fetched_date,
|
|
||||||
fqn,
|
|
||||||
summary_html,
|
|
||||||
role
|
|
||||||
FROM users;
|
|
||||||
DROP TABLE users;
|
|
||||||
ALTER TABLE users_before_themes RENAME TO users;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
ALTER TABLE blogs ADD COLUMN theme VARCHAR;
|
|
||||||
ALTER TABLE users ADD COLUMN preferred_theme VARCHAR;
|
|
||||||
ALTER TABLE users ADD COLUMN hide_custom_css BOOLEAN NOT NULL DEFAULT 'f';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
|
|
||||||
DROP TABLE timeline;
|
|
||||||
DROP TABLE timeline_definition;
|
|
||||||
DROP TABLE list_elems;
|
|
||||||
DROP TABLE lists;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
|
|
||||||
CREATE TABLE timeline_definition(
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
query VARCHAR NOT NULL,
|
|
||||||
CONSTRAINT timeline_unique_user_name UNIQUE(user_id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE timeline(
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
post_id integer NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
|
|
||||||
timeline_id integer NOT NULL REFERENCES timeline_definition(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE lists(
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name VARCHAR NOT NULL,
|
|
||||||
user_id integer REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
type integer NOT NULL,
|
|
||||||
CONSTRAINT timeline_unique_user_name UNIQUE(user_id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE list_elems(
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
list_id integer NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
|
||||||
user_id integer REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
blog_id integer REFERENCES blogs(id) ON DELETE CASCADE,
|
|
||||||
word VARCHAR
|
|
||||||
);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
DELETE FROM timeline_definition WHERE name = 'Your feed';
|
|
||||||
DELETE FROM timeline_definition WHERE name = 'Local feed' AND query = 'local';
|
|
||||||
DELETE FROM timeline_definition WHERE name = 'Federared feed' AND query = 'all';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
INSERT INTO timeline_definition (name, query) VALUES
|
|
||||||
('Local feed', 'local'),
|
|
||||||
('Federated feed', 'all');
|
|
||||||
INSERT INTO timeline_definition (user_id,name,query)
|
|
||||||
select id,'Your feed','followed or ['||fqn||']' from users;
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DELETE FROM apps WHERE name = 'Plume web interface';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
--#!|conn: &Connection, path: &Path| {
|
||||||
|
--#! use plume_common::utils::random_hex;
|
||||||
|
--#!
|
||||||
|
--#! let client_id = random_hex();
|
||||||
|
--#! let client_secret = random_hex();
|
||||||
|
--#! let app = crate::apps::App::insert(
|
||||||
|
--#! &*conn,
|
||||||
|
--#! crate::apps::NewApp {
|
||||||
|
--#! name: "Plume web interface".into(),
|
||||||
|
--#! client_id,
|
||||||
|
--#! client_secret,
|
||||||
|
--#! redirect_uri: None,
|
||||||
|
--#! website: Some("https://joinplu.me".into()),
|
||||||
|
--#! },
|
||||||
|
--#! ).unwrap();
|
||||||
|
--#!
|
||||||
|
--#! for i in 0..=(crate::users::User::count_local(conn).unwrap() as i32 / 20) {
|
||||||
|
--#! if let Ok(page) = crate::users::User::get_local_page(conn, (i * 20, (i + 1) * 20)) {
|
||||||
|
--#! for user in page {
|
||||||
|
--#! crate::api_tokens::ApiToken::insert(
|
||||||
|
--#! conn,
|
||||||
|
--#! crate::api_tokens::NewApiToken {
|
||||||
|
--#! app_id: app.id,
|
||||||
|
--#! user_id: user.id,
|
||||||
|
--#! value: random_hex(),
|
||||||
|
--#! scopes: "read+write".into(),
|
||||||
|
--#! },
|
||||||
|
--#! ).unwrap();
|
||||||
|
--#! }
|
||||||
|
--#! }
|
||||||
|
--#! }
|
||||||
|
--#!
|
||||||
|
--#! Ok(())
|
||||||
|
--#!}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
DELETE FROM timeline WHERE id IN
|
|
||||||
(
|
|
||||||
SELECT timeline.id FROM timeline
|
|
||||||
INNER JOIN timeline_definition ON timeline.timeline_id = timeline_definition.id
|
|
||||||
WHERE timeline_definition.query LIKE 'followed or [%]' OR
|
|
||||||
timeline_definition.query = 'local' OR
|
|
||||||
timeline_definition.query = 'all'
|
|
||||||
);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
INSERT INTO timeline (post_id, timeline_id)
|
|
||||||
SELECT posts.id,timeline_definition.id FROM posts,timeline_definition
|
|
||||||
WHERE timeline_definition.query = 'all';
|
|
||||||
|
|
||||||
INSERT INTO timeline (post_id, timeline_id)
|
|
||||||
SELECT posts.id,timeline_definition.id FROM posts,timeline_definition
|
|
||||||
INNER JOIN blogs ON posts.blog_id = blogs.id
|
|
||||||
INNER JOIN instances ON blogs.instance_id = instances.id
|
|
||||||
WHERE timeline_definition.query = 'local' and instances.local = 1;
|
|
||||||
|
|
||||||
INSERT INTO timeline (post_id, timeline_id)
|
|
||||||
SELECT posts.id,timeline_definition.id FROM posts
|
|
||||||
INNER JOIN blog_authors ON posts.blog_id = blog_authors.blog_id
|
|
||||||
LEFT JOIN follows ON blog_authors.author_id = follows.following_id
|
|
||||||
INNER JOIN timeline_definition ON follows.follower_id = timeline_definition.user_id
|
|
||||||
or blog_authors.author_id = timeline_definition.user_id
|
|
||||||
WHERE timeline_definition.query LIKE 'followed or [%]';
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
|
||||||
|
|
||||||
drop table email_blocklist;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- Your SQL goes here
|
|
||||||
CREATE TABLE email_blocklist(id INTEGER PRIMARY KEY,
|
|
||||||
email_address TEXT UNIQUE,
|
|
||||||
note TEXT,
|
|
||||||
notify_user BOOLEAN DEFAULT FALSE,
|
|
||||||
notification_text TEXT);
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-api"
|
name = "plume-api"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
|
||||||
[features]
|
|
||||||
ci = []
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#![cfg_attr(not(feature = "ci"), warn(missing_docs))]
|
extern crate serde;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ pub struct PostData {
|
|||||||
pub license: String,
|
pub license: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub cover_id: Option<i32>,
|
pub cover_id: Option<i32>,
|
||||||
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-cli"
|
name = "plume-cli"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "plm"
|
name = "plm"
|
||||||
@@ -11,7 +10,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
dotenv = "0.14"
|
dotenv = "0.14"
|
||||||
rpassword = "4.0"
|
rpassword = "3.0"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["r2d2", "chrono"]
|
features = ["r2d2", "chrono"]
|
||||||
@@ -23,4 +22,3 @@ path = "../plume-models"
|
|||||||
[features]
|
[features]
|
||||||
postgres = ["plume-models/postgres", "diesel/postgres"]
|
postgres = ["plume-models/postgres", "diesel/postgres"]
|
||||||
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
sqlite = ["plume-models/sqlite", "diesel/sqlite"]
|
||||||
ci = ["plume-models/ci"]
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#![cfg_attr(not(feature = "ci"), warn(missing_docs))]
|
extern crate clap;
|
||||||
|
extern crate diesel;
|
||||||
|
extern crate dotenv;
|
||||||
|
extern crate plume_models;
|
||||||
|
extern crate rpassword;
|
||||||
|
|
||||||
use clap::App;
|
use clap::App;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use dotenv;
|
|
||||||
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
use plume_models::{instance::Instance, Connection as Conn, CONFIG};
|
||||||
use std::io::{self, prelude::*};
|
use std::io::{self, prelude::*};
|
||||||
|
|
||||||
|
|||||||
+5
-11
@@ -93,11 +93,8 @@ fn init<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searcher>) {
|
fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searcher>) {
|
||||||
let path = args.value_of("path");
|
let path = args.value_of("path").unwrap_or(".");
|
||||||
let path = match path {
|
let path = Path::new(path).join("search_index");
|
||||||
Some(path) => Path::new(path).join("search_index"),
|
|
||||||
None => Path::new(&CONFIG.search_index).to_path_buf(),
|
|
||||||
};
|
|
||||||
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
|
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
|
||||||
|
|
||||||
searcher.fill(conn).expect("Couldn't import post");
|
searcher.fill(conn).expect("Couldn't import post");
|
||||||
@@ -106,12 +103,9 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unlock<'a>(args: &ArgMatches<'a>) {
|
fn unlock<'a>(args: &ArgMatches<'a>) {
|
||||||
let path = match args.value_of("path") {
|
let path = args.value_of("path").unwrap_or(".");
|
||||||
None => Path::new(&CONFIG.search_index),
|
let meta = Path::new(path).join("search_index/.tantivy-meta.lock");
|
||||||
Some(x) => Path::new(x),
|
|
||||||
};
|
|
||||||
let meta = Path::new(path).join(".tantivy-meta.lock");
|
|
||||||
remove_file(meta).unwrap();
|
remove_file(meta).unwrap();
|
||||||
let writer = Path::new(path).join(".tantivy-writer.lock");
|
let writer = Path::new(path).join("search_index/.tantivy-writer.lock");
|
||||||
remove_file(writer).unwrap();
|
remove_file(writer).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-17
@@ -52,12 +52,6 @@ pub fn command<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.long("admin")
|
.long("admin")
|
||||||
.help("Makes the user an administrator of the instance"),
|
.help("Makes the user an administrator of the instance"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::with_name("moderator")
|
|
||||||
.short("m")
|
|
||||||
.long("moderator")
|
|
||||||
.help("Makes the user a moderator of the instance"),
|
|
||||||
)
|
|
||||||
.about("Create a new user on this instance"),
|
.about("Create a new user on this instance"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
@@ -100,17 +94,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|||||||
.value_of("display-name")
|
.value_of("display-name")
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.unwrap_or_else(|| super::ask_for("Display name"));
|
.unwrap_or_else(|| super::ask_for("Display name"));
|
||||||
|
|
||||||
let admin = args.is_present("admin");
|
let admin = args.is_present("admin");
|
||||||
let moderator = args.is_present("moderator");
|
|
||||||
let role = if admin {
|
|
||||||
Role::Admin
|
|
||||||
} else if moderator {
|
|
||||||
Role::Moderator
|
|
||||||
} else {
|
|
||||||
Role::Normal
|
|
||||||
};
|
|
||||||
|
|
||||||
let bio = args.value_of("biography").unwrap_or("").to_string();
|
let bio = args.value_of("biography").unwrap_or("").to_string();
|
||||||
let email = args
|
let email = args
|
||||||
.value_of("email")
|
.value_of("email")
|
||||||
@@ -129,7 +113,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
|||||||
conn,
|
conn,
|
||||||
username,
|
username,
|
||||||
display_name,
|
display_name,
|
||||||
role,
|
admin,
|
||||||
&bio,
|
&bio,
|
||||||
email,
|
email,
|
||||||
User::hash_pass(&password).expect("Couldn't hash password"),
|
User::hash_pass(&password).expect("Couldn't hash password"),
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-common"
|
name = "plume-common"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
activitypub = "0.1.1"
|
activitypub = "0.1.1"
|
||||||
activitystreams-derive = "0.1.1"
|
activitystreams-derive = "0.1.0"
|
||||||
activitystreams-traits = "0.1.0"
|
activitystreams-traits = "0.1.0"
|
||||||
array_tool = "1.0"
|
array_tool = "1.0"
|
||||||
base64 = "0.10"
|
base64 = "0.10"
|
||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
hex = "0.3"
|
hex = "0.3"
|
||||||
hyper = "0.12.33"
|
hyper = "0.12.28"
|
||||||
openssl = "0.10.22"
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
@@ -20,8 +19,7 @@ serde = "1.0"
|
|||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
syntect = "3.3"
|
tokio = "0.1.21"
|
||||||
tokio = "0.1.22"
|
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
@@ -30,6 +28,3 @@ version = "0.4"
|
|||||||
[dependencies.pulldown-cmark]
|
[dependencies.pulldown-cmark]
|
||||||
default-features = false
|
default-features = false
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
||||||
[features]
|
|
||||||
ci = []
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ impl<T> ActivityStream<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
|
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
|
||||||
fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
|
fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
|
||||||
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
|
||||||
json["@context"] = context();
|
json["@context"] = context();
|
||||||
serde_json::to_string(&json).respond_to(request).map(|r| {
|
serde_json::to_string(&json).respond_to(request).map(|r| {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_A
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use crate::activity_pub::sign::Signer;
|
use activity_pub::sign::Signer;
|
||||||
use crate::activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
|
use activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
|
||||||
|
|
||||||
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ impl SignatureValidity {
|
|||||||
|
|
||||||
pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
|
pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
|
||||||
sender: &S,
|
sender: &S,
|
||||||
all_headers: &HeaderMap<'_>,
|
all_headers: &HeaderMap,
|
||||||
data: &request::Digest,
|
data: &request::Digest,
|
||||||
) -> SignatureValidity {
|
) -> SignatureValidity {
|
||||||
let sig_header = all_headers.get_one("Signature");
|
let sig_header = all_headers.get_one("Signature");
|
||||||
|
|||||||
+14
-5
@@ -1,17 +1,26 @@
|
|||||||
#![cfg_attr(not(feature = "ci"), warn(missing_docs))]
|
#![feature(custom_attribute, associated_type_defaults)]
|
||||||
#![feature(associated_type_defaults)]
|
|
||||||
|
|
||||||
|
extern crate activitypub;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate activitystreams_derive;
|
extern crate activitystreams_derive;
|
||||||
use activitystreams_traits;
|
extern crate activitystreams_traits;
|
||||||
|
extern crate array_tool;
|
||||||
use serde;
|
extern crate base64;
|
||||||
|
extern crate chrono;
|
||||||
|
extern crate heck;
|
||||||
|
extern crate hex;
|
||||||
|
extern crate openssl;
|
||||||
|
extern crate pulldown_cmark;
|
||||||
|
extern crate reqwest;
|
||||||
|
extern crate rocket;
|
||||||
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate shrinkwraprs;
|
extern crate shrinkwraprs;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
extern crate tokio;
|
||||||
|
|
||||||
pub mod activity_pub;
|
pub mod activity_pub;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
+17
-78
@@ -7,8 +7,6 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use syntect::html::ClassedHTMLGenerator;
|
|
||||||
use syntect::parsing::SyntaxSet;
|
|
||||||
|
|
||||||
/// Generates an hexadecimal representation of 32 bytes of random data
|
/// Generates an hexadecimal representation of 32 bytes of random data
|
||||||
pub fn random_hex() -> String {
|
pub fn random_hex() -> String {
|
||||||
@@ -48,7 +46,7 @@ enum State {
|
|||||||
Ready,
|
Ready,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_inline(tag: Tag<'_>) -> Tag<'_> {
|
fn to_inline(tag: Tag) -> Tag {
|
||||||
match tag {
|
match tag {
|
||||||
Tag::Header(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => {
|
Tag::Header(_) | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => {
|
||||||
Tag::Paragraph
|
Tag::Paragraph
|
||||||
@@ -57,54 +55,7 @@ fn to_inline(tag: Tag<'_>) -> Tag<'_> {
|
|||||||
t => t,
|
t => t,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct HighlighterContext {
|
|
||||||
content: Vec<String>,
|
|
||||||
}
|
|
||||||
fn highlight_code<'a>(
|
|
||||||
context: &mut Option<HighlighterContext>,
|
|
||||||
evt: Event<'a>,
|
|
||||||
) -> Option<Vec<Event<'a>>> {
|
|
||||||
match evt {
|
|
||||||
Event::Start(Tag::CodeBlock(lang)) => {
|
|
||||||
if lang.is_empty() {
|
|
||||||
Some(vec![Event::Start(Tag::CodeBlock(lang))])
|
|
||||||
} else {
|
|
||||||
*context = Some(HighlighterContext { content: vec![] });
|
|
||||||
Some(vec![Event::Start(Tag::CodeBlock(lang))])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::End(Tag::CodeBlock(x)) => {
|
|
||||||
let mut result = vec![];
|
|
||||||
if let Some(ctx) = context.take() {
|
|
||||||
let syntax_set = SyntaxSet::load_defaults_newlines();
|
|
||||||
let syntax = syntax_set.find_syntax_by_token(&x).unwrap_or_else(|| {
|
|
||||||
syntax_set
|
|
||||||
.find_syntax_by_name(&x)
|
|
||||||
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
|
|
||||||
});
|
|
||||||
let mut html = ClassedHTMLGenerator::new(&syntax, &syntax_set);
|
|
||||||
for line in ctx.content {
|
|
||||||
html.parse_html_for_line(&line);
|
|
||||||
}
|
|
||||||
let q = html.finalize();
|
|
||||||
result.push(Event::Html(q.into()));
|
|
||||||
}
|
|
||||||
result.push(Event::End(Tag::CodeBlock(x)));
|
|
||||||
*context = None;
|
|
||||||
Some(result)
|
|
||||||
}
|
|
||||||
Event::Text(t) => {
|
|
||||||
if let Some(mut c) = context.take() {
|
|
||||||
c.content.push(t.to_string());
|
|
||||||
*context = Some(c);
|
|
||||||
Some(vec![])
|
|
||||||
} else {
|
|
||||||
Some(vec![Event::Text(t)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Some(vec![evt]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
|
fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
|
||||||
let (s, res) = match evt {
|
let (s, res) = match evt {
|
||||||
Event::Text(txt) => match state.take() {
|
Event::Text(txt) => match state.take() {
|
||||||
@@ -146,7 +97,7 @@ fn inline_tags<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MediaProcessor<'a> = Box<dyn 'a + Fn(i32) -> Option<(String, Option<String>)>>;
|
pub type MediaProcessor<'a> = Box<'a + Fn(i32) -> Option<(String, Option<String>)>>;
|
||||||
|
|
||||||
fn process_image<'a, 'b>(
|
fn process_image<'a, 'b>(
|
||||||
evt: Event<'a>,
|
evt: Event<'a>,
|
||||||
@@ -157,7 +108,9 @@ fn process_image<'a, 'b>(
|
|||||||
match evt {
|
match evt {
|
||||||
Event::Start(Tag::Image(id, title)) => {
|
Event::Start(Tag::Image(id, title)) => {
|
||||||
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
|
||||||
if let (Some(cw), false) = (cw, inline) {
|
if inline || cw.is_none() {
|
||||||
|
Event::Start(Tag::Image(Cow::Owned(url), title))
|
||||||
|
} else {
|
||||||
// there is a cw, and where are not inline
|
// there is a cw, and where are not inline
|
||||||
Event::Html(Cow::Owned(format!(
|
Event::Html(Cow::Owned(format!(
|
||||||
r#"<label for="postcontent-cw-{id}">
|
r#"<label for="postcontent-cw-{id}">
|
||||||
@@ -168,11 +121,9 @@ fn process_image<'a, 'b>(
|
|||||||
</span>
|
</span>
|
||||||
<img src="{url}" alt=""#,
|
<img src="{url}" alt=""#,
|
||||||
id = random_hex(),
|
id = random_hex(),
|
||||||
cw = cw,
|
cw = cw.unwrap(),
|
||||||
url = url
|
url = url
|
||||||
)))
|
)))
|
||||||
} else {
|
|
||||||
Event::Start(Tag::Image(Cow::Owned(url), title))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Event::Start(Tag::Image(id, title))
|
Event::Start(Tag::Image(id, title))
|
||||||
@@ -214,24 +165,14 @@ pub fn md_to_html<'a>(
|
|||||||
};
|
};
|
||||||
let parser = Parser::new_ext(md, Options::all());
|
let parser = Parser::new_ext(md, Options::all());
|
||||||
|
|
||||||
let (parser, mentions, hashtags): (Vec<Event<'_>>, Vec<String>, Vec<String>) = parser
|
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
||||||
// Flatten text because pulldown_cmark break #hashtag in two individual text elements
|
// Flatten text because pulldown_cmark break #hashtag in two individual text elements
|
||||||
.scan(None, flatten_text)
|
.scan(None, flatten_text)
|
||||||
.flatten()
|
.flat_map(IntoIterator::into_iter)
|
||||||
.scan(None, highlight_code)
|
|
||||||
.flatten()
|
|
||||||
.map(|evt| process_image(evt, inline, &media_processor))
|
.map(|evt| process_image(evt, inline, &media_processor))
|
||||||
// Ignore headings, images, and tables if inline = true
|
// Ignore headings, images, and tables if inline = true
|
||||||
.scan((vec![], inline), inline_tags)
|
.scan((vec![], inline), inline_tags)
|
||||||
.scan(false, |in_code, evt| match evt {
|
.map(|evt| match evt {
|
||||||
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
|
||||||
*in_code = true;
|
|
||||||
Some((vec![evt], vec![], vec![]))
|
|
||||||
}
|
|
||||||
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
|
||||||
*in_code = false;
|
|
||||||
Some((vec![evt], vec![], vec![]))
|
|
||||||
}
|
|
||||||
Event::Text(txt) => {
|
Event::Text(txt) => {
|
||||||
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
|
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
|
||||||
(vec![], State::Ready, String::new(), 0, vec![], vec![]),
|
(vec![], State::Ready, String::new(), 0, vec![], vec![]),
|
||||||
@@ -247,7 +188,7 @@ pub fn md_to_html<'a>(
|
|||||||
text_acc.push(c)
|
text_acc.push(c)
|
||||||
}
|
}
|
||||||
let mention = text_acc;
|
let mention = text_acc;
|
||||||
let short_mention = mention.splitn(1, '@').next().unwrap_or("");
|
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
|
||||||
let link = Tag::Link(
|
let link = Tag::Link(
|
||||||
format!("{}@/{}/", base_url, &mention).into(),
|
format!("{}@/{}/", base_url, &mention).into(),
|
||||||
short_mention.to_owned().into(),
|
short_mention.to_owned().into(),
|
||||||
@@ -300,7 +241,7 @@ pub fn md_to_html<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Ready => {
|
State::Ready => {
|
||||||
if !*in_code && c == '@' {
|
if c == '@' {
|
||||||
events.push(Event::Text(text_acc.into()));
|
events.push(Event::Text(text_acc.into()));
|
||||||
(
|
(
|
||||||
events,
|
events,
|
||||||
@@ -310,7 +251,7 @@ pub fn md_to_html<'a>(
|
|||||||
mentions,
|
mentions,
|
||||||
hashtags,
|
hashtags,
|
||||||
)
|
)
|
||||||
} else if !*in_code && c == '#' {
|
} else if c == '#' {
|
||||||
events.push(Event::Text(text_acc.into()));
|
events.push(Event::Text(text_acc.into()));
|
||||||
(
|
(
|
||||||
events,
|
events,
|
||||||
@@ -355,9 +296,9 @@ pub fn md_to_html<'a>(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Some((evts, new_mentions, new_hashtags))
|
(evts, new_mentions, new_hashtags)
|
||||||
}
|
}
|
||||||
_ => Some((vec![evt], vec![], vec![])),
|
_ => (vec![evt], vec![], vec![]),
|
||||||
})
|
})
|
||||||
.fold(
|
.fold(
|
||||||
(vec![], vec![], vec![]),
|
(vec![], vec![], vec![]),
|
||||||
@@ -394,11 +335,9 @@ mod tests {
|
|||||||
("mention at @end", vec!["end"]),
|
("mention at @end", vec!["end"]),
|
||||||
("between parenthesis (@test)", vec!["test"]),
|
("between parenthesis (@test)", vec!["test"]),
|
||||||
("with some punctuation @test!", vec!["test"]),
|
("with some punctuation @test!", vec!["test"]),
|
||||||
(" @spaces ", vec!["spaces"]),
|
(" @spaces ", vec!["spaces"]),
|
||||||
("@is_a@mention", vec!["is_a@mention"]),
|
("@is_a@mention", vec!["is_a@mention"]),
|
||||||
("not_a@mention", vec![]),
|
("not_a@mention", vec![]),
|
||||||
("`@helo`", vec![]),
|
|
||||||
("```\n@hello\n```", vec![]),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
@@ -422,7 +361,7 @@ mod tests {
|
|||||||
("hashtag at #end", vec!["end"]),
|
("hashtag at #end", vec!["end"]),
|
||||||
("between parenthesis (#test)", vec!["test"]),
|
("between parenthesis (#test)", vec!["test"]),
|
||||||
("with some punctuation #test!", vec!["test"]),
|
("with some punctuation #test!", vec!["test"]),
|
||||||
(" #spaces ", vec!["spaces"]),
|
(" #spaces ", vec!["spaces"]),
|
||||||
("not_a#hashtag", vec![]),
|
("not_a#hashtag", vec![]),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-front"
|
name = "plume-front"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
stdweb = "=0.4.18"
|
stdweb = "=0.4.14"
|
||||||
stdweb-internal-runtime = "=0.1.4"
|
stdweb-internal-runtime = "=0.1.3"
|
||||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||||
lazy_static = "1.3"
|
lazy_static = "1.3"
|
||||||
serde = "1.0"
|
plume-api = { path = "../plume-api" }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
[features]
|
[dependencies.pulldown-cmark]
|
||||||
ci = []
|
default-features = false
|
||||||
|
version = "0.5"
|
||||||
|
|||||||
+436
-470
@@ -1,44 +1,212 @@
|
|||||||
use crate::CATALOG;
|
use pulldown_cmark::{Event, Options, Parser, Tag};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use stdweb::{
|
use stdweb::{
|
||||||
unstable::{TryFrom, TryInto},
|
unstable::{TryFrom, TryInto},
|
||||||
web::{event::*, html_element::*, *},
|
web::{event::*, html_element::*, *},
|
||||||
};
|
};
|
||||||
|
use CATALOG;
|
||||||
|
|
||||||
macro_rules! mv {
|
fn from_md(md: &str) {
|
||||||
( $( $var:ident ),* => $exp:expr ) => {
|
let md_parser = Parser::new_ext(md, Options::all());
|
||||||
{
|
md_parser.fold(
|
||||||
$( let $var = $var.clone(); )*
|
document().get_element_by_id("editor-main").unwrap(),
|
||||||
$exp
|
|last_elt, event| {
|
||||||
|
match event {
|
||||||
|
Event::Start(tag) => {
|
||||||
|
let new = match tag {
|
||||||
|
Tag::Paragraph => document().create_element("p").unwrap(),
|
||||||
|
Tag::Rule => document().create_element("hr").unwrap(),
|
||||||
|
Tag::Header(level) => {
|
||||||
|
document().create_element(&format!("h{}", level)).unwrap()
|
||||||
|
}
|
||||||
|
Tag::BlockQuote => document().create_element("blockquote").unwrap(),
|
||||||
|
Tag::CodeBlock(code) => {
|
||||||
|
let pre = document().create_element("pre").unwrap();
|
||||||
|
let code_elt = document().create_element("code").unwrap();
|
||||||
|
code_elt.append_child(&document().create_text_node(&code));
|
||||||
|
pre.append_child(&code_elt);
|
||||||
|
pre
|
||||||
|
}
|
||||||
|
Tag::List(None) => document().create_element("ul").unwrap(),
|
||||||
|
Tag::List(Some(_start_index)) => document().create_element("ol").unwrap(), // TODO: handle start_index
|
||||||
|
Tag::Item => document().create_element("li").unwrap(),
|
||||||
|
Tag::FootnoteDefinition(def) => {
|
||||||
|
let note = document().create_element("div").unwrap();
|
||||||
|
note.class_list().add("footnote");
|
||||||
|
note.append_child(&document().create_text_node(&def));
|
||||||
|
note
|
||||||
|
}
|
||||||
|
Tag::HtmlBlock => document().create_element("div").unwrap(),
|
||||||
|
Tag::Table(_alignements) => document().create_element("table").unwrap(), // TODO: handle alignements
|
||||||
|
Tag::TableHead => document().create_element("th").unwrap(),
|
||||||
|
Tag::TableRow => document().create_element("tr").unwrap(),
|
||||||
|
Tag::TableCell => document().create_element("td").unwrap(),
|
||||||
|
Tag::Emphasis => document().create_element("em").unwrap(),
|
||||||
|
Tag::Strong => document().create_element("strong").unwrap(),
|
||||||
|
Tag::Strikethrough => document().create_element("s").unwrap(),
|
||||||
|
Tag::Link(_link_type, url, text) => {
|
||||||
|
let url: &str = &url;
|
||||||
|
let text: &str = &text;
|
||||||
|
let link = document().create_element("a").unwrap();
|
||||||
|
js! {
|
||||||
|
@{&link}.href = @{url};
|
||||||
|
@{&link}.title = @{text};
|
||||||
|
};
|
||||||
|
link
|
||||||
|
}
|
||||||
|
Tag::Image(_link_type, url, text) => {
|
||||||
|
let url: &str = &url;
|
||||||
|
let text: &str = &text;
|
||||||
|
let img = document().create_element("img").unwrap();
|
||||||
|
js! {
|
||||||
|
@{&img}.src = @{url};
|
||||||
|
@{&img}.title = @{text};
|
||||||
|
@{&img}.alt = @{text};
|
||||||
|
};
|
||||||
|
img
|
||||||
|
}
|
||||||
|
};
|
||||||
|
last_elt.append_child(&new);
|
||||||
|
new
|
||||||
|
}
|
||||||
|
Event::End(_) => last_elt.parent_element().unwrap(),
|
||||||
|
Event::Text(text) => {
|
||||||
|
let node = document().create_text_node(&text);
|
||||||
|
last_elt.append_child(&node);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::Code(code) => {
|
||||||
|
let elt = document().create_element("code").unwrap();
|
||||||
|
let content = document().create_text_node(&code);
|
||||||
|
elt.append_child(&content);
|
||||||
|
last_elt.append_child(&elt);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::Html(html) => {
|
||||||
|
// TODO: sanitize it?
|
||||||
|
last_elt.set_attribute("innerHtml", &html);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::InlineHtml(html) => {
|
||||||
|
let elt = document().create_element("span").unwrap();
|
||||||
|
elt.set_attribute("innerHtml", &html);
|
||||||
|
last_elt.append_child(&elt);
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::FootnoteReference(reference) => {
|
||||||
|
last_elt // TODO
|
||||||
|
}
|
||||||
|
Event::SoftBreak => {
|
||||||
|
last_elt.append_child(&document().create_element("br").unwrap());
|
||||||
|
last_elt
|
||||||
|
}
|
||||||
|
Event::HardBreak => {
|
||||||
|
last_elt // TODO
|
||||||
|
}
|
||||||
|
Event::TaskListMarker(done) => {
|
||||||
|
last_elt // TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
MutationObserver::new(|muts, _obs| {
|
||||||
|
for m in muts {
|
||||||
|
console!(log, "mut!!");
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.observe(
|
||||||
|
&document().get_element_by_id("editor-main").unwrap(),
|
||||||
|
MutationObserverInit {
|
||||||
|
child_list: true,
|
||||||
|
attributes: true,
|
||||||
|
character_data: false,
|
||||||
|
subtree: true,
|
||||||
|
attribute_old_value: true,
|
||||||
|
character_data_old_value: false,
|
||||||
|
attribute_filter: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_md() -> String {
|
||||||
|
let root = document().get_element_by_id("editor-main").unwrap();
|
||||||
|
fold_children(&root).join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fold_children(elt: &Element) -> Vec<String> {
|
||||||
|
elt.child_nodes().iter().fold(vec![], |mut blocks, node| {
|
||||||
|
blocks.push(html_to_md(&node));
|
||||||
|
blocks
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn html_to_md(node: &Node) -> String {
|
||||||
|
console!(log, node);
|
||||||
|
if let Ok(elt) = Element::try_from(node.clone()) {
|
||||||
|
console!(log, elt.node_name().to_lowercase());
|
||||||
|
match elt.node_name().to_lowercase().as_ref() {
|
||||||
|
"hr" => "---".into(),
|
||||||
|
"h1" => format!("# {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h2" => format!("## {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h3" => format!("### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h4" => format!("#### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h5" => format!("##### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"h6" => format!("###### {}\n\n", fold_children(&elt).join("")),
|
||||||
|
"blockquote" => format!("> {}\n\n", fold_children(&elt).join("> ")),
|
||||||
|
"pre" => format!("```\n{}\n```\n\n", node.text_content().unwrap_or_default()),
|
||||||
|
"li" => match elt
|
||||||
|
.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.node_name()
|
||||||
|
.to_lowercase()
|
||||||
|
.as_ref()
|
||||||
|
{
|
||||||
|
"ol" => format!(
|
||||||
|
"{}. {}\n",
|
||||||
|
elt.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.child_nodes()
|
||||||
|
.iter()
|
||||||
|
.position(|n| Element::try_from(n).unwrap() == elt)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
fold_children(&elt).join(""),
|
||||||
|
),
|
||||||
|
_ => format!("- {}\n", fold_children(&elt).join("")),
|
||||||
|
},
|
||||||
|
"em" => format!("_{}_", fold_children(&elt).join("")),
|
||||||
|
"strong" => format!("**{}**", fold_children(&elt).join("")),
|
||||||
|
"s" => format!("~~{}~~", fold_children(&elt).join("")),
|
||||||
|
"a" => format!(
|
||||||
|
"[{}]({})",
|
||||||
|
fold_children(&elt).join(""),
|
||||||
|
String::try_from(js! { return @{&elt}.href }).unwrap()
|
||||||
|
),
|
||||||
|
"img" => format!(
|
||||||
|
"",
|
||||||
|
String::try_from(js! { return @{&elt}.alt }).unwrap(),
|
||||||
|
String::try_from(js! { return @{&elt}.src }).unwrap()
|
||||||
|
),
|
||||||
|
other => {
|
||||||
|
console!(log, "Warning: unhandled element:", other);
|
||||||
|
String::new()
|
||||||
|
} // TODO: refs, tables, raw html
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.text_content().unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_elt_value(id: &'static str) -> String {
|
fn get_elt_value(id: &'static str) -> String {
|
||||||
let elt = document().get_element_by_id(id).unwrap();
|
let elt = document().get_element_by_id(id).unwrap();
|
||||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
let inp: Result<InputElement, _> = elt.clone().try_into();
|
||||||
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
|
let select: Result<SelectElement, _> = elt.clone().try_into();
|
||||||
let select: Result<SelectElement, _> = elt.try_into();
|
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
||||||
inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
|
let res = inp.map(|i| i.raw_value()).unwrap_or_else(|_| {
|
||||||
textarea
|
textarea
|
||||||
.map(|t| t.value())
|
.map(|t| t.value())
|
||||||
.unwrap_or_else(|_| select.unwrap().raw_value())
|
.unwrap_or_else(|_| select.unwrap().value().unwrap_or_default())
|
||||||
})
|
});
|
||||||
}
|
res
|
||||||
|
|
||||||
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
|
|
||||||
let elt = document().get_element_by_id(id).unwrap();
|
|
||||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
|
||||||
let textarea: Result<TextAreaElement, _> = elt.clone().try_into();
|
|
||||||
let select: Result<SelectElement, _> = elt.try_into();
|
|
||||||
inp.map(|i| i.set_raw_value(val.as_ref()))
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
textarea
|
|
||||||
.map(|t| t.set_value(val.as_ref()))
|
|
||||||
.unwrap_or_else(|_| select.unwrap().set_raw_value(val.as_ref()))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_return(evt: KeyDownEvent) {
|
fn no_return(evt: KeyDownEvent) {
|
||||||
@@ -74,175 +242,8 @@ impl From<stdweb::private::ConversionError> for EditorError {
|
|||||||
EditorError::TypeError
|
EditorError::TypeError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const AUTOSAVE_DEBOUNCE_TIME: u32 = 5000;
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct AutosaveInformation {
|
|
||||||
contents: String,
|
|
||||||
cover: String,
|
|
||||||
last_saved: f64,
|
|
||||||
license: String,
|
|
||||||
subtitle: String,
|
|
||||||
tags: String,
|
|
||||||
title: String,
|
|
||||||
}
|
|
||||||
js_serializable!(AutosaveInformation);
|
|
||||||
fn is_basic_editor() -> bool {
|
|
||||||
if let Some(basic_editor) = window().local_storage().get("basic-editor") {
|
|
||||||
basic_editor == "true"
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_title() -> String {
|
|
||||||
if is_basic_editor() {
|
|
||||||
get_elt_value("title")
|
|
||||||
} else {
|
|
||||||
let title_field = HtmlElement::try_from(
|
|
||||||
document()
|
|
||||||
.query_selector("#plume-editor > h1")
|
|
||||||
.ok()
|
|
||||||
.unwrap()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
title_field.inner_text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_autosave_id() -> String {
|
|
||||||
format!(
|
|
||||||
"editor_contents={}",
|
|
||||||
window().location().unwrap().pathname().unwrap()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn get_editor_contents() -> String {
|
|
||||||
if is_basic_editor() {
|
|
||||||
get_elt_value("editor-content")
|
|
||||||
} else {
|
|
||||||
let editor =
|
|
||||||
HtmlElement::try_from(document().query_selector("article").ok().unwrap().unwrap())
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
editor.child_nodes().iter().fold(String::new(), |md, ch| {
|
|
||||||
let to_append = match ch.node_type() {
|
|
||||||
NodeType::Element => {
|
|
||||||
if js! { return @{&ch}.tagName; } == "DIV" {
|
|
||||||
(js! { return @{&ch}.innerHTML; })
|
|
||||||
.try_into()
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
(js! { return @{&ch}.outerHTML; })
|
|
||||||
.try_into()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NodeType::Text => ch.node_value().unwrap_or_default(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
format!("{}\n\n{}", md, to_append)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_subtitle() -> String {
|
|
||||||
if is_basic_editor() {
|
|
||||||
get_elt_value("subtitle")
|
|
||||||
} else {
|
|
||||||
let subtitle_element = HtmlElement::try_from(
|
|
||||||
document()
|
|
||||||
.query_selector("#plume-editor > h2")
|
|
||||||
.unwrap()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
subtitle_element.inner_text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn autosave() {
|
|
||||||
let info = AutosaveInformation {
|
|
||||||
contents: get_editor_contents(),
|
|
||||||
title: get_title(),
|
|
||||||
subtitle: get_subtitle(),
|
|
||||||
tags: get_elt_value("tags"),
|
|
||||||
license: get_elt_value("license"),
|
|
||||||
last_saved: Date::now(),
|
|
||||||
cover: get_elt_value("cover"),
|
|
||||||
};
|
|
||||||
let id = get_autosave_id();
|
|
||||||
match window()
|
|
||||||
.local_storage()
|
|
||||||
.insert(&id, &serde_json::to_string(&info).unwrap())
|
|
||||||
{
|
|
||||||
Ok(_) => {}
|
|
||||||
_ => console!(log, "Autosave failed D:"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//This is only necessary until we go to stdweb 4.20 at least
|
|
||||||
fn confirm(message: &str) -> bool {
|
|
||||||
let result: bool = js! {return confirm(@{message});} == true;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
fn load_autosave() {
|
|
||||||
if let Some(autosave_str) = window().local_storage().get(&get_autosave_id()) {
|
|
||||||
let autosave_info: AutosaveInformation = serde_json::from_str(&autosave_str).ok().unwrap();
|
|
||||||
let message = i18n!(
|
|
||||||
CATALOG,
|
|
||||||
"Do you want to load the local autosave last edited at {}?";
|
|
||||||
Date::from_time(autosave_info.last_saved).to_date_string()
|
|
||||||
);
|
|
||||||
if confirm(&message) {
|
|
||||||
set_value("editor-content", &autosave_info.contents);
|
|
||||||
set_value("title", &autosave_info.title);
|
|
||||||
set_value("subtitle", &autosave_info.subtitle);
|
|
||||||
set_value("tags", &autosave_info.tags);
|
|
||||||
set_value("license", &autosave_info.license);
|
|
||||||
set_value("cover", &autosave_info.cover);
|
|
||||||
} else {
|
|
||||||
clear_autosave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn clear_autosave() {
|
|
||||||
window().local_storage().remove(&get_autosave_id());
|
|
||||||
console!(log, &format!("Saved to {}", &get_autosave_id()));
|
|
||||||
}
|
|
||||||
lazy_static! {
|
|
||||||
static ref AUTOSAVE_TIMEOUT: Mutex<Option<TimeoutHandle>> = Mutex::new(None);
|
|
||||||
}
|
|
||||||
fn autosave_debounce() {
|
|
||||||
let timeout = &mut AUTOSAVE_TIMEOUT.lock().unwrap();
|
|
||||||
if let Some(timeout) = timeout.take() {
|
|
||||||
timeout.clear();
|
|
||||||
}
|
|
||||||
**timeout = Some(window().set_clearable_timeout(autosave, AUTOSAVE_DEBOUNCE_TIME));
|
|
||||||
}
|
|
||||||
fn init_widget(
|
|
||||||
parent: &Element,
|
|
||||||
tag: &'static str,
|
|
||||||
placeholder_text: String,
|
|
||||||
content: String,
|
|
||||||
disable_return: bool,
|
|
||||||
) -> Result<HtmlElement, EditorError> {
|
|
||||||
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
|
|
||||||
if !content.is_empty() {
|
|
||||||
widget.dataset().insert("edited", "true")?;
|
|
||||||
}
|
|
||||||
widget.append_child(&document().create_text_node(&content));
|
|
||||||
if disable_return {
|
|
||||||
widget.add_event_listener(no_return);
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.append_child(&widget);
|
fn filter_paste(elt: &Element) {
|
||||||
// We need to do that to make sure the placeholder is correctly rendered
|
|
||||||
widget.focus();
|
|
||||||
widget.blur();
|
|
||||||
|
|
||||||
filter_paste(&widget);
|
|
||||||
|
|
||||||
Ok(widget)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filter_paste(elt: &HtmlElement) {
|
|
||||||
// Only insert text when pasting something
|
// Only insert text when pasting something
|
||||||
js! {
|
js! {
|
||||||
@{&elt}.addEventListener("paste", function (evt) {
|
@{&elt}.addEventListener("paste", function (evt) {
|
||||||
@@ -253,103 +254,96 @@ fn filter_paste(elt: &HtmlElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init() -> Result<(), EditorError> {
|
pub fn init() -> Result<(), EditorError> {
|
||||||
if let Some(ed) = document().get_element_by_id("plume-fallback-editor") {
|
|
||||||
load_autosave();
|
|
||||||
ed.add_event_listener(|_: SubmitEvent| clear_autosave());
|
|
||||||
}
|
|
||||||
// Check if the user wants to use the basic editor
|
// Check if the user wants to use the basic editor
|
||||||
if window()
|
if let Some(basic_editor) = window().local_storage().get("basic-editor") {
|
||||||
.local_storage()
|
if basic_editor == "true" {
|
||||||
.get("basic-editor")
|
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
|
||||||
.map(|x| x == "true")
|
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
|
||||||
.unwrap_or(true)
|
let editor_button = document().create_element("a")?;
|
||||||
{
|
js! { @{&editor_button}.href = "#"; }
|
||||||
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
|
editor_button.add_event_listener(|_: ClickEvent| {
|
||||||
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
|
window().local_storage().remove("basic-editor");
|
||||||
let editor_button = document().create_element("a")?;
|
window().history().go(0).ok(); // refresh
|
||||||
js! { @{&editor_button}.href = "#"; }
|
});
|
||||||
editor_button.add_event_listener(|_: ClickEvent| {
|
editor_button.append_child(
|
||||||
window().local_storage().remove("basic-editor");
|
&document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
|
||||||
window().history().go(0).ok(); // refresh
|
);
|
||||||
});
|
editor.insert_before(&editor_button, &title_label).ok();
|
||||||
editor_button.append_child(
|
return Ok(());
|
||||||
&document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
|
}
|
||||||
);
|
|
||||||
editor.insert_before(&editor_button, &title_label).ok();
|
|
||||||
document()
|
|
||||||
.get_element_by_id("editor-content")
|
|
||||||
.unwrap()
|
|
||||||
.add_event_listener(|_: KeyDownEvent| autosave_debounce());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
init_editor()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we didn't returned above
|
||||||
|
init_editor()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_editor() -> Result<(), EditorError> {
|
fn init_editor() -> Result<(), EditorError> {
|
||||||
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
||||||
|
document().body()?.set_attribute("id", "editor")?;
|
||||||
|
|
||||||
|
let aside = document().get_element_by_id("plume-editor-aside")?;
|
||||||
|
|
||||||
// Show the editor
|
// Show the editor
|
||||||
js! { @{&ed}.style.display = "block"; };
|
js! {
|
||||||
|
@{&ed}.style.display = "grid";
|
||||||
|
@{&aside}.style.display = "block";
|
||||||
|
};
|
||||||
// And hide the HTML-only fallback
|
// And hide the HTML-only fallback
|
||||||
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
||||||
let old_title = document().get_element_by_id("plume-editor-title")?;
|
|
||||||
js! {
|
js! {
|
||||||
@{&old_ed}.style.display = "none";
|
@{&old_ed}.style.display = "none";
|
||||||
@{&old_title}.style.display = "none";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get content from the old editor (when editing an article for instance)
|
|
||||||
let title_val = get_elt_value("title");
|
|
||||||
let subtitle_val = get_elt_value("subtitle");
|
|
||||||
let content_val = get_elt_value("editor-content");
|
|
||||||
// And pre-fill the new editor with this values
|
// And pre-fill the new editor with this values
|
||||||
let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?;
|
let title = document().get_element_by_id("editor-title")?;
|
||||||
let subtitle = init_widget(
|
let subtitle = document().get_element_by_id("editor-subtitle")?;
|
||||||
&ed,
|
let source = get_elt_value("editor-content");
|
||||||
"h2",
|
|
||||||
i18n!(CATALOG, "Subtitle, or summary"),
|
|
||||||
subtitle_val,
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
let content = init_widget(
|
|
||||||
&ed,
|
|
||||||
"article",
|
|
||||||
i18n!(CATALOG, "Write your article here. Markdown is supported."),
|
|
||||||
content_val.clone(),
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
js! { @{&content}.innerHTML = @{content_val}; };
|
|
||||||
|
|
||||||
// character counter
|
setup_toolbar();
|
||||||
content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
|
from_md(&source);
|
||||||
window().set_timeout(mv!(content => move || {
|
|
||||||
if let Some(e) = document().get_element_by_id("char-count") {
|
title.add_event_listener(no_return);
|
||||||
let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
|
subtitle.add_event_listener(no_return);
|
||||||
let text = i18n!(CATALOG, "Around {} characters left"; count);
|
|
||||||
HtmlElement::try_from(e).map(|e| {
|
filter_paste(&title);
|
||||||
js!{@{e}.innerText = @{text}};
|
filter_paste(&subtitle);
|
||||||
}).ok();
|
// TODO: filter_paste(&content);
|
||||||
|
|
||||||
|
document()
|
||||||
|
.get_element_by_id("publish")?
|
||||||
|
.add_event_listener(|_: ClickEvent| {
|
||||||
|
let publish_page = document().get_element_by_id("publish-page").unwrap();
|
||||||
|
let options_page = document().get_element_by_id("options-page").unwrap();
|
||||||
|
js! {
|
||||||
|
@{&options_page}.style.display = "none";
|
||||||
|
@{&publish_page}.style.display = "flex";
|
||||||
};
|
};
|
||||||
}), 0);
|
});
|
||||||
autosave_debounce();
|
|
||||||
}));
|
|
||||||
|
|
||||||
document().get_element_by_id("publish")?.add_event_listener(
|
document()
|
||||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
.get_element_by_id("cancel-publish")?
|
||||||
let popup = document().get_element_by_id("publish-popup").or_else(||
|
.add_event_listener(|_: ClickEvent| {
|
||||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
let publish_page = document().get_element_by_id("publish-page").unwrap();
|
||||||
).unwrap();
|
let options_page = document().get_element_by_id("options-page").unwrap();
|
||||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
js! {
|
||||||
init_popup_bg().ok()
|
@{&publish_page}.style.display = "none";
|
||||||
).unwrap();
|
@{&options_page}.style.display = "flex";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
popup.class_list().add("show").unwrap();
|
document()
|
||||||
bg.class_list().add("show").unwrap();
|
.get_element_by_id("confirm-publish")?
|
||||||
}),
|
.add_event_listener(|_: ClickEvent| {
|
||||||
);
|
save(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
document()
|
||||||
|
.get_element_by_id("save-draft")?
|
||||||
|
.add_event_listener(|_: ClickEvent| {
|
||||||
|
save(true);
|
||||||
|
});
|
||||||
|
|
||||||
show_errors();
|
show_errors();
|
||||||
setup_close_button();
|
setup_close_button();
|
||||||
@@ -357,6 +351,176 @@ fn init_editor() -> Result<(), EditorError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_style(style: &str) {
|
||||||
|
if let Some(select) = document()
|
||||||
|
.get_element_by_id("toolbar-style")
|
||||||
|
.and_then(|e| SelectElement::try_from(e).ok())
|
||||||
|
{
|
||||||
|
select.set_value(Some(style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_toolbar() {
|
||||||
|
let toolbar = document().get_element_by_id("editor-toolbar").unwrap();
|
||||||
|
|
||||||
|
// List of styles (headings, quote, code, etc)
|
||||||
|
let style_select =
|
||||||
|
SelectElement::try_from(document().create_element("select").unwrap()).unwrap();
|
||||||
|
let options = vec![
|
||||||
|
("p", i18n!(CATALOG, "Paragraph")),
|
||||||
|
("ul", i18n!(CATALOG, "List")),
|
||||||
|
("ol", i18n!(CATALOG, "Ordered list")),
|
||||||
|
("h1", i18n!(CATALOG, "Heading 1")),
|
||||||
|
("h2", i18n!(CATALOG, "Heading 2")),
|
||||||
|
("h3", i18n!(CATALOG, "Heading 3")),
|
||||||
|
("h4", i18n!(CATALOG, "Heading 4")),
|
||||||
|
("h5", i18n!(CATALOG, "Heading 5")),
|
||||||
|
("h6", i18n!(CATALOG, "Heading 6")),
|
||||||
|
("blockquote", i18n!(CATALOG, "Quote")),
|
||||||
|
("pre", i18n!(CATALOG, "Code")),
|
||||||
|
];
|
||||||
|
for (tag, name) in options.clone() {
|
||||||
|
let opt = document().create_element("option").unwrap();
|
||||||
|
opt.set_attribute("value", tag);
|
||||||
|
opt.append_child(&document().create_text_node(&name));
|
||||||
|
style_select.append_child(&opt)
|
||||||
|
}
|
||||||
|
style_select.set_attribute("id", "toolbar-style");
|
||||||
|
|
||||||
|
let options_clone = options.clone();
|
||||||
|
document().add_event_listener(move |_: SelectionChangeEvent| {
|
||||||
|
let block = std::iter::successors(
|
||||||
|
window().get_selection().and_then(|s| s.anchor_node()),
|
||||||
|
|node| {
|
||||||
|
let t = node.node_name().to_lowercase();
|
||||||
|
if options_clone.iter().any(|(tag, _)| *tag == &t) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
node.parent_node()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.last();
|
||||||
|
|
||||||
|
if let Some(b) = block {
|
||||||
|
select_style(&b.node_name().to_lowercase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
style_select.add_event_listener(move |_: ChangeEvent| {
|
||||||
|
let block = std::iter::successors(
|
||||||
|
window().get_selection().and_then(|s| s.anchor_node()),
|
||||||
|
|node| {
|
||||||
|
let t = node.node_name().to_lowercase();
|
||||||
|
if options.iter().any(|(tag, _)| *tag == &t) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
node.parent_node()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.last();
|
||||||
|
|
||||||
|
if let Some(block) = block {
|
||||||
|
if let Some(select) = document()
|
||||||
|
.get_element_by_id("toolbar-style")
|
||||||
|
.and_then(|e| SelectElement::try_from(e).ok())
|
||||||
|
{
|
||||||
|
let tag = select.value();
|
||||||
|
|
||||||
|
let new = document().create_element(&tag.unwrap_or_default()).unwrap();
|
||||||
|
for ch in block.child_nodes() {
|
||||||
|
block.remove_child(&ch);
|
||||||
|
new.append_child(&ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.parent_node().unwrap().replace_child(&new, &block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
|
||||||
|
// Italics
|
||||||
|
|
||||||
|
// Insert an image
|
||||||
|
|
||||||
|
toolbar.append_child(&style_select);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(is_draft: bool) {
|
||||||
|
let req = XmlHttpRequest::new();
|
||||||
|
if bool::try_from(js! { return window.editing }).unwrap_or(false) {
|
||||||
|
req.open(
|
||||||
|
"PUT",
|
||||||
|
&format!(
|
||||||
|
"/api/v1/posts/{}",
|
||||||
|
i32::try_from(js! { return window.post_id }).unwrap()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
req.open("POST", "/api/v1/posts").unwrap();
|
||||||
|
}
|
||||||
|
req.set_request_header("Accept", "application/json")
|
||||||
|
.unwrap();
|
||||||
|
req.set_request_header("Content-Type", "application/json")
|
||||||
|
.unwrap();
|
||||||
|
req.set_request_header(
|
||||||
|
"Authorization",
|
||||||
|
&format!(
|
||||||
|
"Bearer {}",
|
||||||
|
String::try_from(js! { return window.api_token }).unwrap()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let req_clone = req.clone();
|
||||||
|
req.add_event_listener(move |_: ProgressLoadEvent| {
|
||||||
|
if let Ok(Some(res)) = req_clone.response_text() {
|
||||||
|
serde_json::from_str(&res)
|
||||||
|
.map(|res: plume_api::posts::PostData| {
|
||||||
|
let url = res.url;
|
||||||
|
js! {
|
||||||
|
window.location.href = @{url};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.map_err(|_| {
|
||||||
|
let json: serde_json::Value = serde_json::from_str(&res).unwrap();
|
||||||
|
window().alert(&format!(
|
||||||
|
"Error: {}",
|
||||||
|
json["error"].as_str().unwrap_or_default()
|
||||||
|
));
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console!(log, to_md());
|
||||||
|
let data = plume_api::posts::NewPostData {
|
||||||
|
title: HtmlElement::try_from(document().get_element_by_id("editor-title").unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.inner_text(),
|
||||||
|
subtitle: document()
|
||||||
|
.get_element_by_id("editor-subtitle")
|
||||||
|
.map(|s| HtmlElement::try_from(s).unwrap().inner_text()),
|
||||||
|
source: to_md(),
|
||||||
|
author: String::new(), // it is ignored anyway (TODO: remove it ??)
|
||||||
|
blog_id: i32::try_from(js! { return window.blog_id }).ok(),
|
||||||
|
published: Some(!is_draft),
|
||||||
|
creation_date: None,
|
||||||
|
license: Some(get_elt_value("license")),
|
||||||
|
tags: Some(
|
||||||
|
get_elt_value("tags")
|
||||||
|
.split(',')
|
||||||
|
.map(|t| t.trim().to_string())
|
||||||
|
.filter(|t| !t.is_empty())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
cover_id: get_elt_value("cover").parse().ok(),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&data).unwrap();
|
||||||
|
req.send_with_string(&json).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn setup_close_button() {
|
fn setup_close_button() {
|
||||||
if let Some(button) = document().get_element_by_id("close-editor") {
|
if let Some(button) = document().get_element_by_id("close-editor") {
|
||||||
button.add_event_listener(|_: ClickEvent| {
|
button.add_event_listener(|_: ClickEvent| {
|
||||||
@@ -388,201 +552,3 @@ fn show_errors() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_popup(
|
|
||||||
title: &HtmlElement,
|
|
||||||
subtitle: &HtmlElement,
|
|
||||||
content: &HtmlElement,
|
|
||||||
old_ed: &Element,
|
|
||||||
) -> Result<Element, EditorError> {
|
|
||||||
let popup = document().create_element("div")?;
|
|
||||||
popup.class_list().add("popup")?;
|
|
||||||
popup.set_attribute("id", "publish-popup")?;
|
|
||||||
|
|
||||||
let tags = get_elt_value("tags")
|
|
||||||
.split(',')
|
|
||||||
.map(str::trim)
|
|
||||||
.map(str::to_string)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let license = get_elt_value("license");
|
|
||||||
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
|
|
||||||
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
|
|
||||||
|
|
||||||
let cover_label = document().create_element("label")?;
|
|
||||||
cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
|
|
||||||
cover_label.set_attribute("for", "cover")?;
|
|
||||||
let cover = document().get_element_by_id("cover")?;
|
|
||||||
cover.parent_element()?.remove_child(&cover).ok();
|
|
||||||
popup.append_child(&cover_label);
|
|
||||||
popup.append_child(&cover);
|
|
||||||
|
|
||||||
if let Some(draft_checkbox) = document().get_element_by_id("draft") {
|
|
||||||
let draft_label = document().create_element("label")?;
|
|
||||||
draft_label.set_attribute("for", "popup-draft")?;
|
|
||||||
|
|
||||||
let draft = document().create_element("input").unwrap();
|
|
||||||
js! {
|
|
||||||
@{&draft}.id = "popup-draft";
|
|
||||||
@{&draft}.name = "popup-draft";
|
|
||||||
@{&draft}.type = "checkbox";
|
|
||||||
@{&draft}.checked = @{&draft_checkbox}.checked;
|
|
||||||
};
|
|
||||||
|
|
||||||
draft_label.append_child(&draft);
|
|
||||||
draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
|
|
||||||
popup.append_child(&draft_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = document().create_element("input")?;
|
|
||||||
js! {
|
|
||||||
@{&button}.type = "submit";
|
|
||||||
@{&button}.value = @{i18n!(CATALOG, "Publish")};
|
|
||||||
};
|
|
||||||
button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
|
|
||||||
button.add_event_listener(
|
|
||||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
|
||||||
title.focus(); // Remove the placeholder before publishing
|
|
||||||
set_value("title", title.inner_text());
|
|
||||||
subtitle.focus();
|
|
||||||
set_value("subtitle", subtitle.inner_text());
|
|
||||||
content.focus();
|
|
||||||
set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
|
|
||||||
let to_append = match ch.node_type() {
|
|
||||||
NodeType::Element => {
|
|
||||||
if js!{ return @{&ch}.tagName; } == "DIV" {
|
|
||||||
(js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
(js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
NodeType::Text => ch.node_value().unwrap_or_default(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
format!("{}\n\n{}", md, to_append)
|
|
||||||
}));
|
|
||||||
set_value("tags", get_elt_value("popup-tags"));
|
|
||||||
if let Some(draft) = document().get_element_by_id("popup-draft") {
|
|
||||||
js!{
|
|
||||||
document.getElementById("draft").checked = @{draft}.checked;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let cover = document().get_element_by_id("cover").unwrap();
|
|
||||||
cover.parent_element().unwrap().remove_child(&cover).ok();
|
|
||||||
old_ed.append_child(&cover);
|
|
||||||
set_value("license", get_elt_value("popup-license"));
|
|
||||||
clear_autosave();
|
|
||||||
js! {
|
|
||||||
@{&old_ed}.submit();
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
popup.append_child(&button);
|
|
||||||
|
|
||||||
document().body()?.append_child(&popup);
|
|
||||||
Ok(popup)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_popup_bg() -> Result<Element, EditorError> {
|
|
||||||
let bg = document().create_element("div")?;
|
|
||||||
bg.class_list().add("popup-bg")?;
|
|
||||||
bg.set_attribute("id", "popup-bg")?;
|
|
||||||
|
|
||||||
document().body()?.append_child(&bg);
|
|
||||||
bg.add_event_listener(|_: ClickEvent| close_popup());
|
|
||||||
Ok(bg)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
|
||||||
match document().query_selector(selector) {
|
|
||||||
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
|
|
||||||
if let Some(len) = form
|
|
||||||
.get_attribute("content-size")
|
|
||||||
.and_then(|s| s.parse::<i32>().ok())
|
|
||||||
{
|
|
||||||
(js! {
|
|
||||||
let x = encodeURIComponent(@{content}.innerHTML)
|
|
||||||
.replace(/%20/g, "+")
|
|
||||||
.replace(/%0A/g, "%0D%0A")
|
|
||||||
.replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
|
|
||||||
.length + 2;
|
|
||||||
console.log(x);
|
|
||||||
return x;
|
|
||||||
})
|
|
||||||
.try_into()
|
|
||||||
.map(|c: i32| len - c)
|
|
||||||
.ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_popup() {
|
|
||||||
let hide = |x: Element| x.class_list().remove("show");
|
|
||||||
document().get_element_by_id("publish-popup").map(hide);
|
|
||||||
document().get_element_by_id("popup-bg").map(hide);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
|
|
||||||
let label = document().create_element("label").unwrap();
|
|
||||||
label.append_child(&document().create_text_node(label_text));
|
|
||||||
label.set_attribute("for", name).unwrap();
|
|
||||||
|
|
||||||
let inp: InputElement = document()
|
|
||||||
.create_element("input")
|
|
||||||
.unwrap()
|
|
||||||
.try_into()
|
|
||||||
.unwrap();
|
|
||||||
inp.set_attribute("name", name).unwrap();
|
|
||||||
inp.set_attribute("id", name).unwrap();
|
|
||||||
|
|
||||||
form.append_child(&label);
|
|
||||||
form.append_child(&inp);
|
|
||||||
inp
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_editable(tag: &'static str) -> Element {
|
|
||||||
let elt = document()
|
|
||||||
.create_element(tag)
|
|
||||||
.expect("Couldn't create editable element");
|
|
||||||
elt.set_attribute("contenteditable", "true")
|
|
||||||
.expect("Couldn't make the element editable");
|
|
||||||
elt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
|
|
||||||
elt.dataset().insert("placeholder", text).unwrap();
|
|
||||||
elt.dataset().insert("edited", "false").unwrap();
|
|
||||||
|
|
||||||
elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
|
|
||||||
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
|
||||||
clear_children(&elt);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
|
|
||||||
if elt.dataset().get("edited").unwrap().as_str() != "true" {
|
|
||||||
clear_children(&elt);
|
|
||||||
|
|
||||||
let ph = document().create_element("span").expect("Couldn't create placeholder");
|
|
||||||
ph.class_list().add("placeholder").expect("Couldn't add class");
|
|
||||||
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
|
|
||||||
elt.append_child(&ph);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
|
|
||||||
elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
|
|
||||||
"false"
|
|
||||||
} else {
|
|
||||||
"true"
|
|
||||||
}).expect("Couldn't update edition state");
|
|
||||||
}));
|
|
||||||
elt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_children(elt: &HtmlElement) {
|
|
||||||
for child in elt.child_nodes() {
|
|
||||||
elt.remove_child(&child).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+20
-10
@@ -1,13 +1,16 @@
|
|||||||
#![recursion_limit = "128"]
|
#![recursion_limit = "128"]
|
||||||
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||||
#![cfg_attr(not(feature = "ci"), warn(missing_docs))]
|
|
||||||
|
|
||||||
|
extern crate gettext;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate gettext_macros;
|
extern crate gettext_macros;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
extern crate pulldown_cmark;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate stdweb;
|
extern crate stdweb;
|
||||||
|
extern crate serde_json;
|
||||||
|
|
||||||
use stdweb::web::{event::*, *};
|
use stdweb::web::{event::*, *};
|
||||||
|
|
||||||
init_i18n!(
|
init_i18n!(
|
||||||
@@ -45,21 +48,28 @@ lazy_static! {
|
|||||||
let catalogs = include_i18n!();
|
let catalogs = include_i18n!();
|
||||||
let lang = js! { return navigator.language }.into_string().unwrap();
|
let lang = js! { return navigator.language }.into_string().unwrap();
|
||||||
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
||||||
|
|
||||||
let english_position = catalogs
|
|
||||||
.iter()
|
|
||||||
.position(|(language_code, _)| *language_code == "en")
|
|
||||||
.unwrap();
|
|
||||||
catalogs
|
catalogs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(l, _)| l == &lang)
|
.find(|(l, _)| l == &lang)
|
||||||
.unwrap_or(&catalogs[english_position])
|
.unwrap_or(&catalogs[0])
|
||||||
.clone()
|
.clone()
|
||||||
.1
|
.1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
std::panic::set_hook(Box::new(|info: &std::panic::PanicInfo| {
|
||||||
|
let mut msg = info.to_string();
|
||||||
|
msg.push_str("\n\nStack:\n\n");
|
||||||
|
let e = error::Error::new("Panicked");
|
||||||
|
let stack = js! { return @{&e}.stack; }
|
||||||
|
.into_string()
|
||||||
|
.unwrap_or_default();
|
||||||
|
msg.push_str(&stack);
|
||||||
|
msg.push_str("\n\n");
|
||||||
|
console!(error, msg);
|
||||||
|
}));
|
||||||
|
|
||||||
menu();
|
menu();
|
||||||
search();
|
search();
|
||||||
editor::init()
|
editor::init()
|
||||||
@@ -67,19 +77,19 @@ fn main() {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle menu on mobile devices
|
/// Toggle menu on mobile device
|
||||||
///
|
///
|
||||||
/// It should normally be working fine even without this code
|
/// It should normally be working fine even without this code
|
||||||
/// But :focus-within is not yet supported by Webkit/Blink
|
/// But :focus-within is not yet supported by Webkit/Blink
|
||||||
fn menu() {
|
fn menu() {
|
||||||
if let Some(button) = document().get_element_by_id("menu") {
|
if let Some(button) = document().get_element_by_id("menu") {
|
||||||
if let Some(menu) = document().get_element_by_id("content") {
|
if let Some(menu) = document().get_element_by_id("content") {
|
||||||
button.add_event_listener(|_: TouchEnd| {
|
button.add_event_listener(|_: ClickEvent| {
|
||||||
document()
|
document()
|
||||||
.get_element_by_id("menu")
|
.get_element_by_id("menu")
|
||||||
.map(|menu| menu.class_list().add("show"));
|
.map(|menu| menu.class_list().add("show"));
|
||||||
});
|
});
|
||||||
menu.add_event_listener(|_: TouchEnd| {
|
menu.add_event_listener(|_: ClickEvent| {
|
||||||
document()
|
document()
|
||||||
.get_element_by_id("menu")
|
.get_element_by_id("menu")
|
||||||
.map(|menu| menu.class_list().remove("show"));
|
.map(|menu| menu.class_list().remove("show"));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-macro"
|
name = "plume-macro"
|
||||||
version = "0.4.0"
|
version = "0.1.0"
|
||||||
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
authors = ["Trinity Pointard <trinity.pointard@insa-rennes.fr>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Plume procedural macros"
|
description = "Plume procedural macros"
|
||||||
@@ -19,4 +19,3 @@ syn = "0.15.27"
|
|||||||
default = []
|
default = []
|
||||||
postgres = []
|
postgres = []
|
||||||
sqlite = []
|
sqlite = []
|
||||||
ci = []
|
|
||||||
|
|||||||
+14
-12
@@ -1,8 +1,8 @@
|
|||||||
#![recursion_limit = "128"]
|
#![recursion_limit = "128"]
|
||||||
#![cfg_attr(not(feature = "ci"), warn(missing_docs))]
|
extern crate proc_macro;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate quote;
|
extern crate quote;
|
||||||
|
extern crate syn;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
@@ -103,17 +103,19 @@ fn file_to_migration(file: &str) -> TokenStream2 {
|
|||||||
acc.push_str(line);
|
acc.push_str(line);
|
||||||
acc.push('\n');
|
acc.push('\n');
|
||||||
}
|
}
|
||||||
} else if line.starts_with("--#!") {
|
|
||||||
acc.push_str(&line[4..]);
|
|
||||||
acc.push('\n');
|
|
||||||
} else if line.starts_with("--") {
|
|
||||||
continue;
|
|
||||||
} else {
|
} else {
|
||||||
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into());
|
if line.starts_with("--#!") {
|
||||||
actions.push(quote!(Action::Function(&#func)));
|
acc.push_str(&line[4..]);
|
||||||
sql = true;
|
acc.push('\n');
|
||||||
acc = line.to_string();
|
} else if line.starts_with("--") {
|
||||||
acc.push('\n');
|
continue;
|
||||||
|
} else {
|
||||||
|
let func: TokenStream2 = trampoline(TokenStream::from_str(&acc).unwrap().into());
|
||||||
|
actions.push(quote!(Action::Function(&#func)));
|
||||||
|
sql = true;
|
||||||
|
acc = line.to_string();
|
||||||
|
acc.push('\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !acc.trim().is_empty() {
|
if !acc.trim().is_empty() {
|
||||||
|
|||||||
+8
-12
@@ -1,35 +1,32 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "plume-models"
|
name = "plume-models"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
authors = ["Plume contributors"]
|
authors = ["Plume contributors"]
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
activitypub = "0.1.1"
|
activitypub = "0.1.1"
|
||||||
ammonia = "2.1.1"
|
ammonia = "2.1.1"
|
||||||
askama_escape = "0.1"
|
askama_escape = "0.1"
|
||||||
bcrypt = "0.5"
|
bcrypt = "0.4"
|
||||||
guid-create = "0.1"
|
guid-create = "0.1"
|
||||||
heck = "0.3.0"
|
heck = "0.3.0"
|
||||||
itertools = "0.8.0"
|
itertools = "0.8.0"
|
||||||
lazy_static = "1.0"
|
lazy_static = "*"
|
||||||
migrations_internals= "1.4.0"
|
migrations_internals= "1.4.0"
|
||||||
openssl = "0.10.22"
|
openssl = "0.10.22"
|
||||||
rocket = "0.4.0"
|
rocket = "0.4.0"
|
||||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
scheduled-thread-pool = "0.2.2"
|
scheduled-thread-pool = "0.2.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tantivy = "0.10.1"
|
tantivy = "0.9.1"
|
||||||
url = "2.1"
|
url = "1.7"
|
||||||
walkdir = "2.2"
|
|
||||||
webfinger = "0.4.1"
|
webfinger = "0.4.1"
|
||||||
whatlang = "0.7.1"
|
whatlang = "0.7.1"
|
||||||
shrinkwraprs = "0.2.1"
|
shrinkwraprs = "0.2.1"
|
||||||
diesel-derive-newtype = "0.1.2"
|
diesel-derive-newtype = "0.1.2"
|
||||||
glob = "0.3.0"
|
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
@@ -52,6 +49,5 @@ path = "../plume-macro"
|
|||||||
diesel_migrations = "1.3.0"
|
diesel_migrations = "1.3.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
postgres = ["diesel/postgres", "plume-macro/postgres"]
|
||||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
sqlite = ["diesel/sqlite", "plume-macro/sqlite"]
|
||||||
ci = ["plume-macro/ci"]
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::users::User;
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{self, FromRequest, Request},
|
request::{self, FromRequest, Request},
|
||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use users::User;
|
||||||
|
|
||||||
/// Wrapper around User to use as a request guard on pages reserved to admins.
|
/// Wrapper around User to use as a request guard on pages reserved to admins.
|
||||||
pub struct Admin(pub User);
|
pub struct Admin(pub User);
|
||||||
|
|
||||||
@@ -13,26 +14,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
|||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> {
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> {
|
||||||
let user = request.guard::<User>()?;
|
let user = request.guard::<User>()?;
|
||||||
if user.is_admin() {
|
if user.is_admin {
|
||||||
Outcome::Success(Admin(user))
|
Outcome::Success(Admin(user))
|
||||||
} else {
|
} else {
|
||||||
Outcome::Failure((Status::Unauthorized, ()))
|
Outcome::Failure((Status::Unauthorized, ()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `Admin` but for moderators.
|
|
||||||
pub struct Moderator(pub User);
|
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Moderator {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Moderator, ()> {
|
|
||||||
let user = request.guard::<User>()?;
|
|
||||||
if user.is_moderator() {
|
|
||||||
Outcome::Success(Moderator(user))
|
|
||||||
} else {
|
|
||||||
Outcome::Failure((Status::Unauthorized, ()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::{db_conn::DbConn, schema::api_tokens, Error, Result};
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
@@ -7,6 +6,10 @@ use rocket::{
|
|||||||
Outcome,
|
Outcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use db_conn::DbConn;
|
||||||
|
use schema::api_tokens;
|
||||||
|
use {Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable)]
|
#[derive(Clone, Queryable)]
|
||||||
pub struct ApiToken {
|
pub struct ApiToken {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -41,6 +44,18 @@ impl ApiToken {
|
|||||||
get!(api_tokens);
|
get!(api_tokens);
|
||||||
insert!(api_tokens, NewApiToken);
|
insert!(api_tokens, NewApiToken);
|
||||||
find_by!(api_tokens, find_by_value, value as &str);
|
find_by!(api_tokens, find_by_value, value as &str);
|
||||||
|
find_by!(
|
||||||
|
api_tokens,
|
||||||
|
find_by_app_and_user,
|
||||||
|
app_id as i32,
|
||||||
|
user_id as i32
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The token for Plume's front-end
|
||||||
|
pub fn web_token(conn: &crate::Connection, user_id: i32) -> Result<ApiToken> {
|
||||||
|
let app = crate::apps::App::find_by_name(conn, "Plume web interface")?;
|
||||||
|
Self::find_by_app_and_user(conn, app.id, user_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn can(&self, what: &'static str, scope: &'static str) -> bool {
|
pub fn can(&self, what: &'static str, scope: &'static str) -> bool {
|
||||||
let full_scope = what.to_owned() + ":" + scope;
|
let full_scope = what.to_owned() + ":" + scope;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use crate::{schema::apps, Error, Result};
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use schema::apps;
|
||||||
|
use {Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Serialize)]
|
#[derive(Clone, Queryable, Serialize)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -27,4 +29,5 @@ impl App {
|
|||||||
get!(apps);
|
get!(apps);
|
||||||
insert!(apps, NewApp);
|
insert!(apps, NewApp);
|
||||||
find_by!(apps, find_by_client_id, client_id as &str);
|
find_by!(apps, find_by_client_id, client_id as &str);
|
||||||
|
find_by!(apps, find_by_name, name as &str);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
use crate::{schema::email_blocklist, Connection, Error, Result};
|
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, TextExpressionMethods};
|
|
||||||
use glob::Pattern;
|
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
|
||||||
#[table_name = "email_blocklist"]
|
|
||||||
pub struct BlocklistedEmail {
|
|
||||||
pub id: i32,
|
|
||||||
pub email_address: String,
|
|
||||||
pub note: String,
|
|
||||||
pub notify_user: bool,
|
|
||||||
pub notification_text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Insertable, FromForm)]
|
|
||||||
#[table_name = "email_blocklist"]
|
|
||||||
pub struct NewBlocklistedEmail {
|
|
||||||
pub email_address: String,
|
|
||||||
pub note: String,
|
|
||||||
pub notify_user: bool,
|
|
||||||
pub notification_text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlocklistedEmail {
|
|
||||||
insert!(email_blocklist, NewBlocklistedEmail);
|
|
||||||
get!(email_blocklist);
|
|
||||||
find_by!(email_blocklist, find_by_id, id as i32);
|
|
||||||
pub fn delete_entries(conn: &Connection, ids: Vec<i32>) -> Result<bool> {
|
|
||||||
use diesel::delete;
|
|
||||||
for i in ids {
|
|
||||||
let be: BlocklistedEmail = BlocklistedEmail::find_by_id(&conn, i)?;
|
|
||||||
delete(&be).execute(conn)?;
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
pub fn find_for_domain(conn: &Connection, domain: &str) -> Result<Vec<BlocklistedEmail>> {
|
|
||||||
let effective = format!("%@{}", domain);
|
|
||||||
email_blocklist::table
|
|
||||||
.filter(email_blocklist::email_address.like(effective))
|
|
||||||
.load::<BlocklistedEmail>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
pub fn matches_blocklist(conn: &Connection, email: &str) -> Result<Option<BlocklistedEmail>> {
|
|
||||||
let mut result = email_blocklist::table.load::<BlocklistedEmail>(conn)?;
|
|
||||||
for i in result.drain(..) {
|
|
||||||
if let Ok(x) = Pattern::new(&i.email_address) {
|
|
||||||
if x.matches(email) {
|
|
||||||
return Ok(Some(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<BlocklistedEmail>> {
|
|
||||||
email_blocklist::table
|
|
||||||
.offset(min.into())
|
|
||||||
.limit((max - min).into())
|
|
||||||
.load::<BlocklistedEmail>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
pub fn count(conn: &Connection) -> Result<i64> {
|
|
||||||
email_blocklist::table
|
|
||||||
.count()
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
pub fn pattern_errors(pat: &str) -> Option<glob::PatternError> {
|
|
||||||
let c = Pattern::new(pat);
|
|
||||||
c.err()
|
|
||||||
}
|
|
||||||
pub fn new(
|
|
||||||
conn: &Connection,
|
|
||||||
pattern: &str,
|
|
||||||
note: &str,
|
|
||||||
show_notification: bool,
|
|
||||||
notification_text: &str,
|
|
||||||
) -> Result<BlocklistedEmail> {
|
|
||||||
let c = NewBlocklistedEmail {
|
|
||||||
email_address: pattern.to_owned(),
|
|
||||||
note: note.to_owned(),
|
|
||||||
notify_user: show_notification,
|
|
||||||
notification_text: notification_text.to_owned(),
|
|
||||||
};
|
|
||||||
BlocklistedEmail::insert(conn, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{instance::tests as instance_tests, tests::rockets, Connection as Conn};
|
|
||||||
use diesel::Connection;
|
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<BlocklistedEmail> {
|
|
||||||
instance_tests::fill_database(conn);
|
|
||||||
let domainblock =
|
|
||||||
BlocklistedEmail::new(conn, "*@bad-actor.com", "Mean spammers", false, "").unwrap();
|
|
||||||
let userblock = BlocklistedEmail::new(
|
|
||||||
conn,
|
|
||||||
"spammer@lax-administration.com",
|
|
||||||
"Decent enough domain, but this user is a problem.",
|
|
||||||
true,
|
|
||||||
"Stop it please",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
vec![domainblock, userblock]
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_match() {
|
|
||||||
let r = rockets();
|
|
||||||
let conn = &*r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let various = fill_database(conn);
|
|
||||||
let match1 = "user1@bad-actor.com";
|
|
||||||
let match2 = "spammer@lax-administration.com";
|
|
||||||
let no_match = "happy-user@lax-administration.com";
|
|
||||||
assert_eq!(
|
|
||||||
BlocklistedEmail::matches_blocklist(conn, match1)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
.id,
|
|
||||||
various[0].id
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
BlocklistedEmail::matches_blocklist(conn, match2)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
.id,
|
|
||||||
various[1].id
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
BlocklistedEmail::matches_blocklist(conn, no_match)
|
|
||||||
.unwrap()
|
|
||||||
.is_none(),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::{schema::blog_authors, Error, Result};
|
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use schema::blog_authors;
|
||||||
|
use {Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
pub struct BlogAuthor {
|
pub struct BlogAuthor {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
|||||||
+44
-78
@@ -1,28 +1,28 @@
|
|||||||
use crate::{
|
use activitypub::{actor::Group, collection::OrderedCollection, object::Image, CustomObject};
|
||||||
ap_url, instance::*, medias::Media, posts::Post, safe_string::SafeString, schema::blogs,
|
|
||||||
search::Searcher, users::User, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
|
||||||
};
|
|
||||||
use activitypub::{
|
|
||||||
actor::Group,
|
|
||||||
collection::{OrderedCollection, OrderedCollectionPage},
|
|
||||||
object::Image,
|
|
||||||
CustomObject,
|
|
||||||
};
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use openssl::{
|
use openssl::{
|
||||||
hash::MessageDigest,
|
hash::MessageDigest,
|
||||||
pkey::{PKey, Private},
|
pkey::{PKey, Private},
|
||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sign::{Signer, Verifier},
|
sign::{Signer, Verifier},
|
||||||
};
|
};
|
||||||
|
use serde_json;
|
||||||
|
use url::Url;
|
||||||
|
use webfinger::*;
|
||||||
|
|
||||||
|
use instance::*;
|
||||||
|
use medias::Media;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsActor, FromId},
|
inbox::{AsActor, FromId},
|
||||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||||
};
|
};
|
||||||
use serde_json;
|
use posts::Post;
|
||||||
use url::Url;
|
use safe_string::SafeString;
|
||||||
use webfinger::*;
|
use schema::blogs;
|
||||||
|
use search::Searcher;
|
||||||
|
use users::User;
|
||||||
|
use {Connection, Error, PlumeRocket, Result};
|
||||||
|
|
||||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||||
|
|
||||||
@@ -44,7 +44,6 @@ pub struct Blog {
|
|||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
pub icon_id: Option<i32>,
|
pub icon_id: Option<i32>,
|
||||||
pub banner_id: Option<i32>,
|
pub banner_id: Option<i32>,
|
||||||
pub theme: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
#[derive(Default, Insertable)]
|
||||||
@@ -62,7 +61,6 @@ pub struct NewBlog {
|
|||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
pub icon_id: Option<i32>,
|
pub icon_id: Option<i32>,
|
||||||
pub banner_id: Option<i32>,
|
pub banner_id: Option<i32>,
|
||||||
pub theme: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLOG_PREFIX: &str = "~";
|
const BLOG_PREFIX: &str = "~";
|
||||||
@@ -101,8 +99,8 @@ impl Blog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_authors(&self, conn: &Connection) -> Result<Vec<User>> {
|
pub fn list_authors(&self, conn: &Connection) -> Result<Vec<User>> {
|
||||||
use crate::schema::blog_authors;
|
use schema::blog_authors;
|
||||||
use crate::schema::users;
|
use schema::users;
|
||||||
let authors_ids = blog_authors::table
|
let authors_ids = blog_authors::table
|
||||||
.filter(blog_authors::blog_id.eq(self.id))
|
.filter(blog_authors::blog_id.eq(self.id))
|
||||||
.select(blog_authors::author_id);
|
.select(blog_authors::author_id);
|
||||||
@@ -113,7 +111,7 @@ impl Blog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_authors(&self, conn: &Connection) -> Result<i64> {
|
pub fn count_authors(&self, conn: &Connection) -> Result<i64> {
|
||||||
use crate::schema::blog_authors;
|
use schema::blog_authors;
|
||||||
blog_authors::table
|
blog_authors::table
|
||||||
.filter(blog_authors::blog_id.eq(self.id))
|
.filter(blog_authors::blog_id.eq(self.id))
|
||||||
.count()
|
.count()
|
||||||
@@ -122,7 +120,7 @@ impl Blog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_for_author(conn: &Connection, author: &User) -> Result<Vec<Blog>> {
|
pub fn find_for_author(conn: &Connection, author: &User) -> Result<Vec<Blog>> {
|
||||||
use crate::schema::blog_authors;
|
use schema::blog_authors;
|
||||||
let author_ids = blog_authors::table
|
let author_ids = blog_authors::table
|
||||||
.filter(blog_authors::author_id.eq(author.id))
|
.filter(blog_authors::author_id.eq(author.id))
|
||||||
.select(blog_authors::blog_id);
|
.select(blog_authors::blog_id);
|
||||||
@@ -135,8 +133,10 @@ impl Blog {
|
|||||||
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
|
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
|
||||||
let from_db = blogs::table
|
let from_db = blogs::table
|
||||||
.filter(blogs::fqn.eq(fqn))
|
.filter(blogs::fqn.eq(fqn))
|
||||||
.first(&*c.conn)
|
.limit(1)
|
||||||
.optional()?;
|
.load::<Blog>(&*c.conn)?
|
||||||
|
.into_iter()
|
||||||
|
.next();
|
||||||
if let Some(from_db) = from_db {
|
if let Some(from_db) = from_db {
|
||||||
Ok(from_db)
|
Ok(from_db)
|
||||||
} else {
|
} else {
|
||||||
@@ -220,49 +220,12 @@ impl Blog {
|
|||||||
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
|
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
|
||||||
coll.collection_props
|
coll.collection_props
|
||||||
.set_total_items_u64(self.get_activities(conn)?.len() as u64)?;
|
.set_total_items_u64(self.get_activities(conn)?.len() as u64)?;
|
||||||
coll.collection_props
|
|
||||||
.set_first_link(Id::new(ap_url(&format!("{}?page=1", &self.outbox_url))))?;
|
|
||||||
coll.collection_props
|
|
||||||
.set_last_link(Id::new(ap_url(&format!(
|
|
||||||
"{}?page={}",
|
|
||||||
&self.outbox_url,
|
|
||||||
(self.get_activities(conn)?.len() as u64 + ITEMS_PER_PAGE as u64 - 1) as u64
|
|
||||||
/ ITEMS_PER_PAGE as u64
|
|
||||||
))))?;
|
|
||||||
Ok(ActivityStream::new(coll))
|
|
||||||
}
|
|
||||||
pub fn outbox_page(
|
|
||||||
&self,
|
|
||||||
conn: &Connection,
|
|
||||||
(min, max): (i32, i32),
|
|
||||||
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
|
||||||
let mut coll = OrderedCollectionPage::default();
|
|
||||||
let acts = self.get_activity_page(&conn, (min, max))?;
|
|
||||||
//This still doesn't do anything because the outbox
|
|
||||||
//doesn't do anything yet
|
|
||||||
coll.collection_page_props.set_next_link(Id::new(&format!(
|
|
||||||
"{}?page={}",
|
|
||||||
&self.outbox_url,
|
|
||||||
min / ITEMS_PER_PAGE + 1
|
|
||||||
)))?;
|
|
||||||
coll.collection_page_props.set_prev_link(Id::new(&format!(
|
|
||||||
"{}?page={}",
|
|
||||||
&self.outbox_url,
|
|
||||||
min / ITEMS_PER_PAGE - 1
|
|
||||||
)))?;
|
|
||||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
|
||||||
Ok(ActivityStream::new(coll))
|
Ok(ActivityStream::new(coll))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
|
fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
fn get_activity_page(
|
|
||||||
&self,
|
|
||||||
_conn: &Connection,
|
|
||||||
(_min, _max): (i32, i32),
|
|
||||||
) -> Result<Vec<serde_json::Value>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||||
@@ -309,7 +272,7 @@ impl Blog {
|
|||||||
pub fn icon_url(&self, conn: &Connection) -> String {
|
pub fn icon_url(&self, conn: &Connection) -> String {
|
||||||
self.icon_id
|
self.icon_id
|
||||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||||
.unwrap_or_else(|| "/static/images/default-avatar.png".to_string())
|
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn banner_url(&self, conn: &Connection) -> Option<String> {
|
pub fn banner_url(&self, conn: &Connection) -> Option<String> {
|
||||||
@@ -429,7 +392,6 @@ impl FromId<PlumeRocket> for Blog {
|
|||||||
.summary_string()
|
.summary_string()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
),
|
),
|
||||||
theme: None,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -496,16 +458,14 @@ impl NewBlog {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use blog_authors::*;
|
||||||
blog_authors::*,
|
|
||||||
instance::tests as instance_tests,
|
|
||||||
medias::NewMedia,
|
|
||||||
search::tests::get_searcher,
|
|
||||||
tests::{db, rockets},
|
|
||||||
users::tests as usersTests,
|
|
||||||
Connection as Conn,
|
|
||||||
};
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use instance::tests as instance_tests;
|
||||||
|
use medias::NewMedia;
|
||||||
|
use search::tests::get_searcher;
|
||||||
|
use tests::{db, rockets};
|
||||||
|
use users::tests as usersTests;
|
||||||
|
use Connection as Conn;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
||||||
instance_tests::fill_database(conn);
|
instance_tests::fill_database(conn);
|
||||||
@@ -609,8 +569,9 @@ pub(crate) mod tests {
|
|||||||
Instance::get_local().unwrap().id
|
Instance::get_local().unwrap().id
|
||||||
);
|
);
|
||||||
// TODO add tests for remote instance
|
// TODO add tests for remote instance
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -710,8 +671,9 @@ pub(crate) mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[1].id));
|
.any(|b| b.id == blog[1].id));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -734,8 +696,9 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(Blog::find_by_fqn(&r, "SomeName").unwrap().id, blog.id);
|
assert_eq!(Blog::find_by_fqn(&r, "SomeName").unwrap().id, blog.id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -757,8 +720,9 @@ pub(crate) mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(blog.fqn, "SomeName");
|
assert_eq!(blog.fqn, "SomeName");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -769,8 +733,9 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
blogs[0].delete(conn, &get_searcher()).unwrap();
|
blogs[0].delete(conn, &get_searcher()).unwrap();
|
||||||
assert!(Blog::get(conn, blogs[0].id).is_err());
|
assert!(Blog::get(conn, blogs[0].id).is_err());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -839,8 +804,9 @@ pub(crate) mod tests {
|
|||||||
assert!(Blog::get(conn, blog[1].id).is_err());
|
assert!(Blog::get(conn, blog[1].id).is_err());
|
||||||
user[1].delete(conn, &searcher).unwrap();
|
user[1].delete(conn, &searcher).unwrap();
|
||||||
assert!(Blog::get(conn, blog[0].id).is_err());
|
assert!(Blog::get(conn, blog[0].id).is_err());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -901,6 +867,6 @@ pub(crate) mod tests {
|
|||||||
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
|
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use crate::{comments::Comment, schema::comment_seers, users::User, Connection, Error, Result};
|
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use comments::Comment;
|
||||||
|
use schema::comment_seers;
|
||||||
|
use users::User;
|
||||||
|
use {Connection, Error, Result};
|
||||||
|
|
||||||
#[derive(Queryable, Clone)]
|
#[derive(Queryable, Clone)]
|
||||||
pub struct CommentSeers {
|
pub struct CommentSeers {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
use crate::{
|
|
||||||
comment_seers::{CommentSeers, NewCommentSeers},
|
|
||||||
instance::Instance,
|
|
||||||
medias::Media,
|
|
||||||
mentions::Mention,
|
|
||||||
notifications::*,
|
|
||||||
posts::Post,
|
|
||||||
safe_string::SafeString,
|
|
||||||
schema::comments,
|
|
||||||
users::User,
|
|
||||||
Connection, Error, PlumeRocket, Result,
|
|
||||||
};
|
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
activity::{Create, Delete},
|
activity::{Create, Delete},
|
||||||
link,
|
link,
|
||||||
@@ -17,16 +5,26 @@ use activitypub::{
|
|||||||
};
|
};
|
||||||
use chrono::{self, NaiveDateTime};
|
use chrono::{self, NaiveDateTime};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use plume_common::{
|
|
||||||
activity_pub::{
|
|
||||||
inbox::{AsActor, AsObject, FromId},
|
|
||||||
Id, IntoId, PUBLIC_VISIBILITY,
|
|
||||||
},
|
|
||||||
utils,
|
|
||||||
};
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||||
|
use instance::Instance;
|
||||||
|
use medias::Media;
|
||||||
|
use mentions::Mention;
|
||||||
|
use notifications::*;
|
||||||
|
use plume_common::activity_pub::{
|
||||||
|
inbox::{AsActor, AsObject, FromId},
|
||||||
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
|
};
|
||||||
|
use plume_common::utils;
|
||||||
|
use posts::Post;
|
||||||
|
use safe_string::SafeString;
|
||||||
|
use schema::comments;
|
||||||
|
use users::User;
|
||||||
|
use {Connection, Error, PlumeRocket, Result};
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -79,7 +77,7 @@ impl Comment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||||
use crate::schema::users;
|
use schema::users;
|
||||||
let local_authors = users::table
|
let local_authors = users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
.select(users::id);
|
.select(users::id);
|
||||||
@@ -129,8 +127,9 @@ impl Comment {
|
|||||||
)?))?;
|
)?))?;
|
||||||
note.object_props
|
note.object_props
|
||||||
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||||
note.object_props.set_attributed_to_link(author.into_id())?;
|
note.object_props
|
||||||
note.object_props.set_to_link_vec(to)?;
|
.set_attributed_to_link(author.clone().into_id())?;
|
||||||
|
note.object_props.set_to_link_vec(to.clone())?;
|
||||||
note.object_props.set_tag_link_vec(
|
note.object_props.set_tag_link_vec(
|
||||||
mentions
|
mentions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -437,7 +436,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ pub struct Config {
|
|||||||
pub search_index: String,
|
pub search_index: String,
|
||||||
pub rocket: Result<RocketConfig, RocketError>,
|
pub rocket: Result<RocketConfig, RocketError>,
|
||||||
pub logo: LogoConfig,
|
pub logo: LogoConfig,
|
||||||
pub default_theme: String,
|
|
||||||
pub media_directory: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -201,8 +199,5 @@ lazy_static! {
|
|||||||
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
|
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
|
||||||
rocket: get_rocket_config(),
|
rocket: get_rocket_config(),
|
||||||
logo: LogoConfig::default(),
|
logo: LogoConfig::default(),
|
||||||
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
|
|
||||||
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
|
|
||||||
.unwrap_or_else(|_| "static/media".to_owned()),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::Connection;
|
|
||||||
use diesel::r2d2::{
|
use diesel::r2d2::{
|
||||||
ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection,
|
ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection,
|
||||||
};
|
};
|
||||||
@@ -11,6 +10,8 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use Connection;
|
||||||
|
|
||||||
pub type DbPool = Pool<ConnectionManager<Connection>>;
|
pub type DbPool = Pool<ConnectionManager<Connection>>;
|
||||||
|
|
||||||
// From rocket documentation
|
// From rocket documentation
|
||||||
@@ -25,7 +26,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
|
|||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||||
let pool = request.guard::<State<'_, DbPool>>()?;
|
let pool = request.guard::<State<DbPool>>()?;
|
||||||
match pool.get() {
|
match pool.get() {
|
||||||
Ok(conn) => Outcome::Success(DbConn(conn)),
|
Ok(conn) => Outcome::Success(DbConn(conn)),
|
||||||
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
|
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
|
||||||
@@ -58,18 +59,3 @@ impl CustomizeConnection<Connection, ConnError> for PragmaForeignKey {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) mod tests {
|
|
||||||
use super::*;
|
|
||||||
use diesel::Connection as _;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TestConnectionCustomizer;
|
|
||||||
impl CustomizeConnection<Connection, ConnError> for TestConnectionCustomizer {
|
|
||||||
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
|
|
||||||
PragmaForeignKey.on_acquire(conn)?;
|
|
||||||
Ok(conn.begin_test_transaction().unwrap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use crate::{
|
|
||||||
ap_url, notifications::*, schema::follows, users::User, Connection, Error, PlumeRocket, Result,
|
|
||||||
CONFIG,
|
|
||||||
};
|
|
||||||
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
|
use activitypub::activity::{Accept, Follow as FollowAct, Undo};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
|
|
||||||
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
broadcast,
|
broadcast,
|
||||||
inbox::{AsActor, AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
sign::Signer,
|
sign::Signer,
|
||||||
Id, IntoId, PUBLIC_VISIBILITY,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
};
|
};
|
||||||
|
use schema::follows;
|
||||||
|
use users::User;
|
||||||
|
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
|
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
|
||||||
#[belongs_to(User, foreign_key = "following_id")]
|
#[belongs_to(User, foreign_key = "following_id")]
|
||||||
@@ -55,7 +56,8 @@ impl Follow {
|
|||||||
let target = User::get(conn, self.following_id)?;
|
let target = User::get(conn, self.following_id)?;
|
||||||
|
|
||||||
let mut act = FollowAct::default();
|
let mut act = FollowAct::default();
|
||||||
act.follow_props.set_actor_link::<Id>(user.into_id())?;
|
act.follow_props
|
||||||
|
.set_actor_link::<Id>(user.clone().into_id())?;
|
||||||
act.follow_props
|
act.follow_props
|
||||||
.set_object_link::<Id>(target.clone().into_id())?;
|
.set_object_link::<Id>(target.clone().into_id())?;
|
||||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
@@ -200,8 +202,9 @@ impl IntoId for Follow {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{tests::db, users::tests as user_tests};
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use tests::db;
|
||||||
|
use users::tests as user_tests;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_id() {
|
fn test_id() {
|
||||||
@@ -232,8 +235,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("Couldn't insert new follow");
|
.expect("Couldn't insert new follow");
|
||||||
assert_eq!(follow.ap_url, String::from("https://some.url/"));
|
assert_eq!(follow.ap_url, String::from("https://some.url/"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -210,6 +212,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -252,8 +255,9 @@ pub(crate) mod tests {
|
|||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
});
|
});
|
||||||
assert!(super::inbox(&r, ok_act).is_ok());
|
assert!(super::inbox(&r, ok_act).is_ok());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -278,6 +282,7 @@ pub(crate) mod tests {
|
|||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
});
|
});
|
||||||
assert!(super::inbox(&r, ok_act).is_ok());
|
assert!(super::inbox(&r, ok_act).is_ok());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -331,6 +336,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -356,6 +362,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -394,6 +401,7 @@ pub(crate) mod tests {
|
|||||||
"type": "Undo",
|
"type": "Undo",
|
||||||
});
|
});
|
||||||
assert!(super::inbox(&r, ok_act).is_ok());
|
assert!(super::inbox(&r, ok_act).is_ok());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -432,6 +440,7 @@ pub(crate) mod tests {
|
|||||||
"type": "Undo",
|
"type": "Undo",
|
||||||
});
|
});
|
||||||
assert!(super::inbox(&r, ok_act).is_ok());
|
assert!(super::inbox(&r, ok_act).is_ok());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -470,6 +479,7 @@ pub(crate) mod tests {
|
|||||||
"type": "Undo",
|
"type": "Undo",
|
||||||
});
|
});
|
||||||
assert!(super::inbox(&r, ok_act).is_ok());
|
assert!(super::inbox(&r, ok_act).is_ok());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -499,6 +509,7 @@ pub(crate) mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
super::inbox(&r, act).unwrap();
|
super::inbox(&r, act).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use crate::{
|
|
||||||
ap_url,
|
|
||||||
medias::Media,
|
|
||||||
safe_string::SafeString,
|
|
||||||
schema::{instances, users},
|
|
||||||
users::{Role, User},
|
|
||||||
Connection, Error, Result,
|
|
||||||
};
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use plume_common::utils::md_to_html;
|
use std::iter::Iterator;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use ap_url;
|
||||||
|
use medias::Media;
|
||||||
|
use plume_common::utils::md_to_html;
|
||||||
|
use safe_string::SafeString;
|
||||||
|
use schema::{instances, users};
|
||||||
|
use users::User;
|
||||||
|
use {Connection, Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable)]
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
pub struct Instance {
|
pub struct Instance {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -61,8 +61,11 @@ impl Instance {
|
|||||||
pub fn get_local_uncached(conn: &Connection) -> Result<Instance> {
|
pub fn get_local_uncached(conn: &Connection) -> Result<Instance> {
|
||||||
instances::table
|
instances::table
|
||||||
.filter(instances::local.eq(true))
|
.filter(instances::local.eq(true))
|
||||||
.first(conn)
|
.limit(1)
|
||||||
.map_err(Error::from)
|
.load::<Instance>(conn)?
|
||||||
|
.into_iter()
|
||||||
|
.nth(0)
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache_local(conn: &Connection) {
|
pub fn cache_local(conn: &Connection) {
|
||||||
@@ -114,7 +117,7 @@ impl Instance {
|
|||||||
pub fn has_admin(&self, conn: &Connection) -> Result<bool> {
|
pub fn has_admin(&self, conn: &Connection) -> Result<bool> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(self.id))
|
.filter(users::instance_id.eq(self.id))
|
||||||
.filter(users::role.eq(Role::Admin as i32))
|
.filter(users::is_admin.eq(true))
|
||||||
.load::<User>(conn)
|
.load::<User>(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.map(|r| !r.is_empty())
|
.map(|r| !r.is_empty())
|
||||||
@@ -123,8 +126,9 @@ impl Instance {
|
|||||||
pub fn main_admin(&self, conn: &Connection) -> Result<User> {
|
pub fn main_admin(&self, conn: &Connection) -> Result<User> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(self.id))
|
.filter(users::instance_id.eq(self.id))
|
||||||
.filter(users::role.eq(Role::Admin as i32))
|
.filter(users::is_admin.eq(true))
|
||||||
.first(conn)
|
.limit(1)
|
||||||
|
.get_result::<User>(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +149,6 @@ impl Instance {
|
|||||||
open_registrations: bool,
|
open_registrations: bool,
|
||||||
short_description: SafeString,
|
short_description: SafeString,
|
||||||
long_description: SafeString,
|
long_description: SafeString,
|
||||||
default_license: String,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (sd, _, _) = md_to_html(
|
let (sd, _, _) = md_to_html(
|
||||||
short_description.as_ref(),
|
short_description.as_ref(),
|
||||||
@@ -167,7 +170,6 @@ impl Instance {
|
|||||||
instances::long_description.eq(long_description),
|
instances::long_description.eq(long_description),
|
||||||
instances::short_description_html.eq(sd),
|
instances::short_description_html.eq(sd),
|
||||||
instances::long_description_html.eq(ld),
|
instances::long_description_html.eq(ld),
|
||||||
instances::default_license.eq(default_license),
|
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -184,67 +186,14 @@ impl Instance {
|
|||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of the local instance themes (all files matching `static/css/NAME/theme.css`)
|
|
||||||
///
|
|
||||||
/// The list only contains the name of the themes, without their extension or full path.
|
|
||||||
pub fn list_themes() -> Result<Vec<String>> {
|
|
||||||
// List all the files in static/css
|
|
||||||
std::path::Path::new("static")
|
|
||||||
.join("css")
|
|
||||||
.read_dir()
|
|
||||||
.map(|files| {
|
|
||||||
files
|
|
||||||
.filter_map(std::result::Result::ok)
|
|
||||||
// Only keep actual directories (each theme has its own dir)
|
|
||||||
.filter(|f| f.file_type().map(|t| t.is_dir()).unwrap_or(false))
|
|
||||||
// Only keep the directory name (= theme name)
|
|
||||||
.filter_map(|f| {
|
|
||||||
f.path()
|
|
||||||
.file_name()
|
|
||||||
.and_then(std::ffi::OsStr::to_str)
|
|
||||||
.map(std::borrow::ToOwned::to_owned)
|
|
||||||
})
|
|
||||||
// Ignore the one starting with "blog-": these are the blog themes
|
|
||||||
.filter(|f| !f.starts_with("blog-"))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a list of the local blog themes (all files matching `static/css/blog-NAME/theme.css`)
|
|
||||||
///
|
|
||||||
/// The list only contains the name of the themes, without their extension or full path.
|
|
||||||
pub fn list_blog_themes() -> Result<Vec<String>> {
|
|
||||||
// List all the files in static/css
|
|
||||||
std::path::Path::new("static")
|
|
||||||
.join("css")
|
|
||||||
.read_dir()
|
|
||||||
.map(|files| {
|
|
||||||
files
|
|
||||||
.filter_map(std::result::Result::ok)
|
|
||||||
// Only keep actual directories (each theme has its own dir)
|
|
||||||
.filter(|f| f.file_type().map(|t| t.is_dir()).unwrap_or(false))
|
|
||||||
// Only keep the directory name (= theme name)
|
|
||||||
.filter_map(|f| {
|
|
||||||
f.path()
|
|
||||||
.file_name()
|
|
||||||
.and_then(std::ffi::OsStr::to_str)
|
|
||||||
.map(std::borrow::ToOwned::to_owned)
|
|
||||||
})
|
|
||||||
// Only keep the one starting with "blog-": these are the blog themes
|
|
||||||
.filter(|f| f.starts_with("blog-"))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{tests::db, Connection as Conn};
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use tests::db;
|
||||||
|
use Connection as Conn;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
|
||||||
let res = vec![
|
let res = vec![
|
||||||
@@ -338,6 +287,7 @@ pub(crate) mod tests {
|
|||||||
res.short_description_html.get(),
|
res.short_description_html.get(),
|
||||||
&inserted.short_description_html
|
&inserted.short_description_html
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -398,6 +348,7 @@ pub(crate) mod tests {
|
|||||||
assert!(last_domaine <= page[0].public_domain);
|
assert!(last_domaine <= page[0].public_domain);
|
||||||
last_domaine = page[0].public_domain.clone();
|
last_domaine = page[0].public_domain.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -460,6 +411,7 @@ pub(crate) mod tests {
|
|||||||
.count(),
|
.count(),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -476,7 +428,6 @@ pub(crate) mod tests {
|
|||||||
false,
|
false,
|
||||||
SafeString::new("[short](#link)"),
|
SafeString::new("[short](#link)"),
|
||||||
SafeString::new("[long_description](/with_link)"),
|
SafeString::new("[long_description](/with_link)"),
|
||||||
"CC-BY-SAO".to_owned(),
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let inst = Instance::get(conn, inst.id).unwrap();
|
let inst = Instance::get(conn, inst.id).unwrap();
|
||||||
@@ -495,7 +446,7 @@ pub(crate) mod tests {
|
|||||||
inst.short_description_html,
|
inst.short_description_html,
|
||||||
SafeString::new("<p><a href=\"#link\">short</a></p>\n")
|
SafeString::new("<p><a href=\"#link\">short</a></p>\n")
|
||||||
);
|
);
|
||||||
assert_eq!(inst.default_license, "CC-BY-SAO".to_owned());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-27
@@ -1,22 +1,40 @@
|
|||||||
#![feature(try_trait)]
|
#![feature(try_trait)]
|
||||||
#![feature(never_type)]
|
#![feature(never_type)]
|
||||||
|
#![feature(custom_attribute)]
|
||||||
#![feature(proc_macro_hygiene)]
|
#![feature(proc_macro_hygiene)]
|
||||||
#![cfg_attr(not(feature = "ci"), warn(missing_docs))]
|
|
||||||
|
|
||||||
|
extern crate activitypub;
|
||||||
|
extern crate ammonia;
|
||||||
|
extern crate askama_escape;
|
||||||
|
extern crate bcrypt;
|
||||||
|
extern crate chrono;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
|
extern crate guid_create;
|
||||||
|
extern crate heck;
|
||||||
|
extern crate itertools;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
extern crate migrations_internals;
|
||||||
|
extern crate openssl;
|
||||||
|
extern crate plume_api;
|
||||||
|
extern crate plume_common;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate plume_macro;
|
extern crate plume_macro;
|
||||||
#[macro_use]
|
extern crate reqwest;
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
extern crate rocket_i18n;
|
||||||
|
extern crate scheduled_thread_pool;
|
||||||
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate tantivy;
|
extern crate tantivy;
|
||||||
|
extern crate url;
|
||||||
|
extern crate webfinger;
|
||||||
|
extern crate whatlang;
|
||||||
|
|
||||||
use plume_common::activity_pub::inbox::InboxError;
|
use plume_common::activity_pub::inbox::InboxError;
|
||||||
|
|
||||||
@@ -34,7 +52,6 @@ pub type Connection = diesel::PgConnection;
|
|||||||
/// All the possible errors that can be encoutered in this crate
|
/// All the possible errors that can be encoutered in this crate
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Blocklisted(bool, String),
|
|
||||||
Db(diesel::result::Error),
|
Db(diesel::result::Error),
|
||||||
Inbox(Box<InboxError<Error>>),
|
Inbox(Box<InboxError<Error>>),
|
||||||
InvalidValue,
|
InvalidValue,
|
||||||
@@ -45,9 +62,9 @@ pub enum Error {
|
|||||||
SerDe,
|
SerDe,
|
||||||
Search(search::SearcherError),
|
Search(search::SearcherError),
|
||||||
Signature,
|
Signature,
|
||||||
TimelineQuery(timeline::query::QueryError),
|
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
Url,
|
Url,
|
||||||
|
Validation(String),
|
||||||
Webfinger,
|
Webfinger,
|
||||||
Expired,
|
Expired,
|
||||||
}
|
}
|
||||||
@@ -57,7 +74,7 @@ impl From<bcrypt::BcryptError> for Error {
|
|||||||
Error::Signature
|
Error::Signature
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const ITEMS_PER_PAGE: i32 = 12;
|
|
||||||
impl From<openssl::error::ErrorStack> for Error {
|
impl From<openssl::error::ErrorStack> for Error {
|
||||||
fn from(_: openssl::error::ErrorStack) -> Self {
|
fn from(_: openssl::error::ErrorStack) -> Self {
|
||||||
Error::Signature
|
Error::Signature
|
||||||
@@ -121,12 +138,6 @@ impl From<search::SearcherError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<timeline::query::QueryError> for Error {
|
|
||||||
fn from(err: timeline::query::QueryError) -> Self {
|
|
||||||
Error::TimelineQuery(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
impl From<std::io::Error> for Error {
|
||||||
fn from(err: std::io::Error) -> Self {
|
fn from(err: std::io::Error) -> Self {
|
||||||
Error::Io(err)
|
Error::Io(err)
|
||||||
@@ -163,8 +174,11 @@ macro_rules! find_by {
|
|||||||
pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result<Self> {
|
pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result<Self> {
|
||||||
$table::table
|
$table::table
|
||||||
$(.filter($table::$col.eq($col)))+
|
$(.filter($table::$col.eq($col)))+
|
||||||
.first(conn)
|
.limit(1)
|
||||||
.map_err(Error::from)
|
.load::<Self>(conn)?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -210,8 +224,11 @@ macro_rules! get {
|
|||||||
pub fn get(conn: &crate::Connection, id: i32) -> Result<Self> {
|
pub fn get(conn: &crate::Connection, id: i32) -> Result<Self> {
|
||||||
$table::table
|
$table::table
|
||||||
.filter($table::id.eq(id))
|
.filter($table::id.eq(id))
|
||||||
.first(conn)
|
.limit(1)
|
||||||
.map_err(Error::from)
|
.load::<Self>(conn)?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -229,13 +246,12 @@ macro_rules! get {
|
|||||||
/// Model::insert(connection, NewModelType::new());
|
/// Model::insert(connection, NewModelType::new());
|
||||||
/// ```
|
/// ```
|
||||||
macro_rules! insert {
|
macro_rules! insert {
|
||||||
($table:ident, $from:ty) => {
|
($table:ident, $from:ident) => {
|
||||||
insert!($table, $from, |x, _conn| Ok(x));
|
insert!($table, $from, |x, _conn| Ok(x));
|
||||||
};
|
};
|
||||||
($table:ident, $from:ty, |$val:ident, $conn:ident | $( $after:tt )+) => {
|
($table:ident, $from:ident, |$val:ident, $conn:ident | $( $after:tt )+) => {
|
||||||
last!($table);
|
last!($table);
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> {
|
pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> {
|
||||||
diesel::insert_into($table::table)
|
diesel::insert_into($table::table)
|
||||||
.values(new)
|
.values(new)
|
||||||
@@ -262,12 +278,14 @@ macro_rules! insert {
|
|||||||
/// ```
|
/// ```
|
||||||
macro_rules! last {
|
macro_rules! last {
|
||||||
($table:ident) => {
|
($table:ident) => {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn last(conn: &crate::Connection) -> Result<Self> {
|
pub fn last(conn: &crate::Connection) -> Result<Self> {
|
||||||
$table::table
|
$table::table
|
||||||
.order_by($table::id.desc())
|
.order_by($table::id.desc())
|
||||||
.first(conn)
|
.limit(1)
|
||||||
.map_err(Error::from)
|
.load::<Self>(conn)?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -282,12 +300,18 @@ pub fn ap_url(url: &str) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, search, Connection as Conn, CONFIG};
|
use db_conn;
|
||||||
use diesel::r2d2::ConnectionManager;
|
use diesel::r2d2::ConnectionManager;
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
use diesel::{dsl::sql_query, RunQueryDsl};
|
||||||
|
use migrations::IMPORTED_MIGRATIONS;
|
||||||
use plume_common::utils::random_hex;
|
use plume_common::utils::random_hex;
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
|
use search;
|
||||||
use std::env::temp_dir;
|
use std::env::temp_dir;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use Connection as Conn;
|
||||||
|
use CONFIG;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! part_eq {
|
macro_rules! part_eq {
|
||||||
@@ -307,7 +331,7 @@ mod tests {
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref DB_POOL: db_conn::DbPool = {
|
static ref DB_POOL: db_conn::DbPool = {
|
||||||
let pool = db_conn::DbPool::builder()
|
let pool = db_conn::DbPool::builder()
|
||||||
.connection_customizer(Box::new(db_conn::tests::TestConnectionCustomizer))
|
.connection_customizer(Box::new(db_conn::PragmaForeignKey))
|
||||||
.build(ConnectionManager::<Conn>::new(CONFIG.database_url.as_str()))
|
.build(ConnectionManager::<Conn>::new(CONFIG.database_url.as_str()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||||
@@ -331,7 +355,6 @@ mod tests {
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod api_tokens;
|
pub mod api_tokens;
|
||||||
pub mod apps;
|
pub mod apps;
|
||||||
pub mod blocklisted_emails;
|
|
||||||
pub mod blog_authors;
|
pub mod blog_authors;
|
||||||
pub mod blogs;
|
pub mod blogs;
|
||||||
pub mod comment_seers;
|
pub mod comment_seers;
|
||||||
@@ -342,7 +365,6 @@ pub mod headers;
|
|||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod likes;
|
pub mod likes;
|
||||||
pub mod lists;
|
|
||||||
pub mod medias;
|
pub mod medias;
|
||||||
pub mod mentions;
|
pub mod mentions;
|
||||||
pub mod migrations;
|
pub mod migrations;
|
||||||
@@ -353,10 +375,8 @@ pub mod post_authors;
|
|||||||
pub mod posts;
|
pub mod posts;
|
||||||
pub mod reshares;
|
pub mod reshares;
|
||||||
pub mod safe_string;
|
pub mod safe_string;
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod timeline;
|
|
||||||
pub mod users;
|
pub mod users;
|
||||||
pub use plume_rocket::PlumeRocket;
|
pub use plume_rocket::PlumeRocket;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
use crate::{
|
|
||||||
notifications::*, posts::Post, schema::likes, timeline::*, users::User, Connection, Error,
|
|
||||||
PlumeRocket, Result,
|
|
||||||
};
|
|
||||||
use activitypub::activity;
|
use activitypub::activity;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsActor, AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
Id, IntoId, PUBLIC_VISIBILITY,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
};
|
};
|
||||||
|
use posts::Post;
|
||||||
|
use schema::likes;
|
||||||
|
use users::User;
|
||||||
|
use {Connection, Error, PlumeRocket, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
pub struct Like {
|
pub struct Like {
|
||||||
@@ -97,8 +99,6 @@ impl AsObject<User, activity::Like, &PlumeRocket> for Post {
|
|||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
res.notify(&c.conn)?;
|
res.notify(&c.conn)?;
|
||||||
|
|
||||||
Timeline::add_to_all_timelines(c, &self, Kind::Like(&actor))?;
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,554 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
blogs::Blog,
|
|
||||||
schema::{blogs, list_elems, lists, users},
|
|
||||||
users::User,
|
|
||||||
Connection, Error, Result,
|
|
||||||
};
|
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
|
||||||
|
|
||||||
/// Represent what a list is supposed to store. Represented in database as an integer
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum ListType {
|
|
||||||
User,
|
|
||||||
Blog,
|
|
||||||
Word,
|
|
||||||
Prefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<i32> for ListType {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(i: i32) -> std::result::Result<Self, ()> {
|
|
||||||
match i {
|
|
||||||
0 => Ok(ListType::User),
|
|
||||||
1 => Ok(ListType::Blog),
|
|
||||||
2 => Ok(ListType::Word),
|
|
||||||
3 => Ok(ListType::Prefix),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<i32> for ListType {
|
|
||||||
fn into(self) -> i32 {
|
|
||||||
match self {
|
|
||||||
ListType::User => 0,
|
|
||||||
ListType::Blog => 1,
|
|
||||||
ListType::Word => 2,
|
|
||||||
ListType::Prefix => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
|
||||||
pub struct List {
|
|
||||||
pub id: i32,
|
|
||||||
pub name: String,
|
|
||||||
pub user_id: Option<i32>,
|
|
||||||
type_: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
|
||||||
#[table_name = "lists"]
|
|
||||||
struct NewList<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
pub user_id: Option<i32>,
|
|
||||||
type_: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! func {
|
|
||||||
(@elem User $id:expr, $value:expr) => {
|
|
||||||
NewListElem {
|
|
||||||
list_id: $id,
|
|
||||||
user_id: Some(*$value),
|
|
||||||
blog_id: None,
|
|
||||||
word: None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(@elem Blog $id:expr, $value:expr) => {
|
|
||||||
NewListElem {
|
|
||||||
list_id: $id,
|
|
||||||
user_id: None,
|
|
||||||
blog_id: Some(*$value),
|
|
||||||
word: None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(@elem Word $id:expr, $value:expr) => {
|
|
||||||
NewListElem {
|
|
||||||
list_id: $id,
|
|
||||||
user_id: None,
|
|
||||||
blog_id: None,
|
|
||||||
word: Some($value),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(@elem Prefix $id:expr, $value:expr) => {
|
|
||||||
NewListElem {
|
|
||||||
list_id: $id,
|
|
||||||
user_id: None,
|
|
||||||
blog_id: None,
|
|
||||||
word: Some($value),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(@in_type User) => { i32 };
|
|
||||||
(@in_type Blog) => { i32 };
|
|
||||||
(@in_type Word) => { &str };
|
|
||||||
(@in_type Prefix) => { &str };
|
|
||||||
(@out_type User) => { User };
|
|
||||||
(@out_type Blog) => { Blog };
|
|
||||||
(@out_type Word) => { String };
|
|
||||||
(@out_type Prefix) => { String };
|
|
||||||
|
|
||||||
(add: $fn:ident, $kind:ident) => {
|
|
||||||
pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> {
|
|
||||||
if self.kind() != ListType::$kind {
|
|
||||||
return Err(Error::InvalidValue);
|
|
||||||
}
|
|
||||||
diesel::insert_into(list_elems::table)
|
|
||||||
.values(
|
|
||||||
vals
|
|
||||||
.iter()
|
|
||||||
.map(|u| func!(@elem $kind self.id, u))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.execute(conn)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(list: $fn:ident, $kind:ident, $table:ident) => {
|
|
||||||
pub fn $fn(&self, conn: &Connection) -> Result<Vec<func!(@out_type $kind)>> {
|
|
||||||
if self.kind() != ListType::$kind {
|
|
||||||
return Err(Error::InvalidValue);
|
|
||||||
}
|
|
||||||
list_elems::table
|
|
||||||
.filter(list_elems::list_id.eq(self.id))
|
|
||||||
.inner_join($table::table)
|
|
||||||
.select($table::all_columns)
|
|
||||||
.load(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(set: $fn:ident, $kind:ident, $add:ident) => {
|
|
||||||
pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> {
|
|
||||||
if self.kind() != ListType::$kind {
|
|
||||||
return Err(Error::InvalidValue);
|
|
||||||
}
|
|
||||||
self.clear(conn)?;
|
|
||||||
self.$add(conn, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
|
||||||
struct ListElem {
|
|
||||||
pub id: i32,
|
|
||||||
pub list_id: i32,
|
|
||||||
pub user_id: Option<i32>,
|
|
||||||
pub blog_id: Option<i32>,
|
|
||||||
pub word: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
|
||||||
#[table_name = "list_elems"]
|
|
||||||
struct NewListElem<'a> {
|
|
||||||
pub list_id: i32,
|
|
||||||
pub user_id: Option<i32>,
|
|
||||||
pub blog_id: Option<i32>,
|
|
||||||
pub word: Option<&'a str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl List {
|
|
||||||
last!(lists);
|
|
||||||
get!(lists);
|
|
||||||
|
|
||||||
fn insert(conn: &Connection, val: NewList<'_>) -> Result<Self> {
|
|
||||||
diesel::insert_into(lists::table)
|
|
||||||
.values(val)
|
|
||||||
.execute(conn)?;
|
|
||||||
List::last(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_for_user(conn: &Connection, user_id: Option<i32>) -> Result<Vec<Self>> {
|
|
||||||
if let Some(user_id) = user_id {
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.eq(user_id))
|
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
} else {
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.is_null())
|
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_for_user_by_name(
|
|
||||||
conn: &Connection,
|
|
||||||
user_id: Option<i32>,
|
|
||||||
name: &str,
|
|
||||||
) -> Result<Self> {
|
|
||||||
if let Some(user_id) = user_id {
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.eq(user_id))
|
|
||||||
.filter(lists::name.eq(name))
|
|
||||||
.first(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
} else {
|
|
||||||
lists::table
|
|
||||||
.filter(lists::user_id.is_null())
|
|
||||||
.filter(lists::name.eq(name))
|
|
||||||
.first(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(conn: &Connection, name: &str, user: Option<&User>, kind: ListType) -> Result<Self> {
|
|
||||||
Self::insert(
|
|
||||||
conn,
|
|
||||||
NewList {
|
|
||||||
name,
|
|
||||||
user_id: user.map(|u| u.id),
|
|
||||||
type_: kind.into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the kind of a list
|
|
||||||
pub fn kind(&self) -> ListType {
|
|
||||||
self.type_.try_into().expect("invalid list was constructed")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return Ok(true) if the list contain the given user, Ok(false) otherwiser,
|
|
||||||
/// and Err(_) on error
|
|
||||||
pub fn contains_user(&self, conn: &Connection, user: i32) -> Result<bool> {
|
|
||||||
private::ListElem::user_in_list(conn, self, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return Ok(true) if the list contain the given blog, Ok(false) otherwiser,
|
|
||||||
/// and Err(_) on error
|
|
||||||
pub fn contains_blog(&self, conn: &Connection, blog: i32) -> Result<bool> {
|
|
||||||
private::ListElem::blog_in_list(conn, self, blog)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return Ok(true) if the list contain the given word, Ok(false) otherwiser,
|
|
||||||
/// and Err(_) on error
|
|
||||||
pub fn contains_word(&self, conn: &Connection, word: &str) -> Result<bool> {
|
|
||||||
private::ListElem::word_in_list(conn, self, word)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return Ok(true) if the list match the given prefix, Ok(false) otherwiser,
|
|
||||||
/// and Err(_) on error
|
|
||||||
pub fn contains_prefix(&self, conn: &Connection, word: &str) -> Result<bool> {
|
|
||||||
private::ListElem::prefix_in_list(conn, self, word)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert new users in a list
|
|
||||||
func! {add: add_users, User}
|
|
||||||
|
|
||||||
/// Insert new blogs in a list
|
|
||||||
func! {add: add_blogs, Blog}
|
|
||||||
|
|
||||||
/// Insert new words in a list
|
|
||||||
func! {add: add_words, Word}
|
|
||||||
|
|
||||||
/// Insert new prefixes in a list
|
|
||||||
func! {add: add_prefixes, Prefix}
|
|
||||||
|
|
||||||
/// Get all users in the list
|
|
||||||
func! {list: list_users, User, users}
|
|
||||||
|
|
||||||
/// Get all blogs in the list
|
|
||||||
func! {list: list_blogs, Blog, blogs}
|
|
||||||
|
|
||||||
/// Get all words in the list
|
|
||||||
pub fn list_words(&self, conn: &Connection) -> Result<Vec<String>> {
|
|
||||||
self.list_stringlike(conn, ListType::Word)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all prefixes in the list
|
|
||||||
pub fn list_prefixes(&self, conn: &Connection) -> Result<Vec<String>> {
|
|
||||||
self.list_stringlike(conn, ListType::Prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn list_stringlike(&self, conn: &Connection, t: ListType) -> Result<Vec<String>> {
|
|
||||||
if self.kind() != t {
|
|
||||||
return Err(Error::InvalidValue);
|
|
||||||
}
|
|
||||||
list_elems::table
|
|
||||||
.filter(list_elems::list_id.eq(self.id))
|
|
||||||
.filter(list_elems::word.is_not_null())
|
|
||||||
.select(list_elems::word)
|
|
||||||
.load::<Option<String>>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
.map(|r| r.into_iter().filter_map(|o| o).collect::<Vec<String>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&self, conn: &Connection) -> Result<()> {
|
|
||||||
diesel::delete(list_elems::table.filter(list_elems::list_id.eq(self.id)))
|
|
||||||
.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}
|
|
||||||
func! {set: set_prefixes, Prefix, add_prefixes}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod private {
|
|
||||||
pub use super::*;
|
|
||||||
use diesel::{
|
|
||||||
dsl,
|
|
||||||
sql_types::{Nullable, Text},
|
|
||||||
IntoSql, TextExpressionMethods,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl ListElem {
|
|
||||||
insert!(list_elems, NewListElem<'_>);
|
|
||||||
|
|
||||||
pub fn user_in_list(conn: &Connection, list: &List, user: i32) -> Result<bool> {
|
|
||||||
dsl::select(dsl::exists(
|
|
||||||
list_elems::table
|
|
||||||
.filter(list_elems::list_id.eq(list.id))
|
|
||||||
.filter(list_elems::user_id.eq(Some(user))),
|
|
||||||
))
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blog_in_list(conn: &Connection, list: &List, blog: i32) -> Result<bool> {
|
|
||||||
dsl::select(dsl::exists(
|
|
||||||
list_elems::table
|
|
||||||
.filter(list_elems::list_id.eq(list.id))
|
|
||||||
.filter(list_elems::blog_id.eq(Some(blog))),
|
|
||||||
))
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn word_in_list(conn: &Connection, list: &List, word: &str) -> Result<bool> {
|
|
||||||
dsl::select(dsl::exists(
|
|
||||||
list_elems::table
|
|
||||||
.filter(list_elems::list_id.eq(list.id))
|
|
||||||
.filter(list_elems::word.eq(word)),
|
|
||||||
))
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prefix_in_list(conn: &Connection, list: &List, word: &str) -> Result<bool> {
|
|
||||||
dsl::select(dsl::exists(
|
|
||||||
list_elems::table
|
|
||||||
.filter(
|
|
||||||
word.into_sql::<Nullable<Text>>()
|
|
||||||
.like(list_elems::word.concat("%")),
|
|
||||||
)
|
|
||||||
.filter(list_elems::list_id.eq(list.id)),
|
|
||||||
))
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{blogs::tests as blog_tests, tests::db};
|
|
||||||
use diesel::Connection;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_type() {
|
|
||||||
for i in 0..4 {
|
|
||||||
assert_eq!(i, Into::<i32>::into(ListType::try_from(i).unwrap()));
|
|
||||||
}
|
|
||||||
ListType::try_from(4).unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_lists() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, _) = blog_tests::fill_database(conn);
|
|
||||||
|
|
||||||
let l1 = List::new(conn, "list1", None, ListType::User).unwrap();
|
|
||||||
let l2 = List::new(conn, "list2", None, ListType::Blog).unwrap();
|
|
||||||
let l1u = List::new(conn, "list1", Some(&users[0]), ListType::Word).unwrap();
|
|
||||||
|
|
||||||
let l_eq = |l1: &List, l2: &List| {
|
|
||||||
assert_eq!(l1.id, l2.id);
|
|
||||||
assert_eq!(l1.user_id, l2.user_id);
|
|
||||||
assert_eq!(l1.name, l2.name);
|
|
||||||
assert_eq!(l1.type_, l2.type_);
|
|
||||||
};
|
|
||||||
|
|
||||||
let l1bis = List::get(conn, l1.id).unwrap();
|
|
||||||
l_eq(&l1, &l1bis);
|
|
||||||
|
|
||||||
let l_inst = List::list_for_user(conn, None).unwrap();
|
|
||||||
let l_user = List::list_for_user(conn, Some(users[0].id)).unwrap();
|
|
||||||
assert_eq!(2, l_inst.len());
|
|
||||||
assert_eq!(1, l_user.len());
|
|
||||||
assert!(l_inst.iter().all(|l| l.id != l1u.id));
|
|
||||||
|
|
||||||
l_eq(&l1u, &l_user[0]);
|
|
||||||
if l_inst[0].id == l1.id {
|
|
||||||
l_eq(&l1, &l_inst[0]);
|
|
||||||
l_eq(&l2, &l_inst[1]);
|
|
||||||
} else {
|
|
||||||
l_eq(&l1, &l_inst[1]);
|
|
||||||
l_eq(&l2, &l_inst[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
l_eq(
|
|
||||||
&l1,
|
|
||||||
&List::find_for_user_by_name(conn, l1.user_id, &l1.name).unwrap(),
|
|
||||||
);
|
|
||||||
l_eq(
|
|
||||||
&&l1u,
|
|
||||||
&List::find_for_user_by_name(conn, l1u.user_id, &l1u.name).unwrap(),
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_user_list() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blog_tests::fill_database(conn);
|
|
||||||
|
|
||||||
let l = List::new(conn, "list", None, ListType::User).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(l.kind(), ListType::User);
|
|
||||||
assert!(l.list_users(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(!l.contains_user(conn, users[0].id).unwrap());
|
|
||||||
assert!(l.add_users(conn, &[users[0].id]).is_ok());
|
|
||||||
assert!(l.contains_user(conn, users[0].id).unwrap());
|
|
||||||
|
|
||||||
assert!(l.add_users(conn, &[users[1].id]).is_ok());
|
|
||||||
assert!(l.contains_user(conn, users[0].id).unwrap());
|
|
||||||
assert!(l.contains_user(conn, users[1].id).unwrap());
|
|
||||||
assert_eq!(2, l.list_users(conn).unwrap().len());
|
|
||||||
|
|
||||||
assert!(l.set_users(conn, &[users[0].id]).is_ok());
|
|
||||||
assert!(l.contains_user(conn, users[0].id).unwrap());
|
|
||||||
assert!(!l.contains_user(conn, users[1].id).unwrap());
|
|
||||||
assert_eq!(1, l.list_users(conn).unwrap().len());
|
|
||||||
assert!(users[0] == l.list_users(conn).unwrap()[0]);
|
|
||||||
|
|
||||||
l.clear(conn).unwrap();
|
|
||||||
assert!(l.list_users(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(l.add_blogs(conn, &[blogs[0].id]).is_err());
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_blog_list() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blog_tests::fill_database(conn);
|
|
||||||
|
|
||||||
let l = List::new(conn, "list", None, ListType::Blog).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(l.kind(), ListType::Blog);
|
|
||||||
assert!(l.list_blogs(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(!l.contains_blog(conn, blogs[0].id).unwrap());
|
|
||||||
assert!(l.add_blogs(conn, &[blogs[0].id]).is_ok());
|
|
||||||
assert!(l.contains_blog(conn, blogs[0].id).unwrap());
|
|
||||||
|
|
||||||
assert!(l.add_blogs(conn, &[blogs[1].id]).is_ok());
|
|
||||||
assert!(l.contains_blog(conn, blogs[0].id).unwrap());
|
|
||||||
assert!(l.contains_blog(conn, blogs[1].id).unwrap());
|
|
||||||
assert_eq!(2, l.list_blogs(conn).unwrap().len());
|
|
||||||
|
|
||||||
assert!(l.set_blogs(conn, &[blogs[0].id]).is_ok());
|
|
||||||
assert!(l.contains_blog(conn, blogs[0].id).unwrap());
|
|
||||||
assert!(!l.contains_blog(conn, blogs[1].id).unwrap());
|
|
||||||
assert_eq!(1, l.list_blogs(conn).unwrap().len());
|
|
||||||
assert_eq!(blogs[0].id, l.list_blogs(conn).unwrap()[0].id);
|
|
||||||
|
|
||||||
l.clear(conn).unwrap();
|
|
||||||
assert!(l.list_blogs(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(l.add_users(conn, &[users[0].id]).is_err());
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_word_list() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let l = List::new(conn, "list", None, ListType::Word).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(l.kind(), ListType::Word);
|
|
||||||
assert!(l.list_words(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(!l.contains_word(conn, "plume").unwrap());
|
|
||||||
assert!(l.add_words(conn, &["plume"]).is_ok());
|
|
||||||
assert!(l.contains_word(conn, "plume").unwrap());
|
|
||||||
assert!(!l.contains_word(conn, "plumelin").unwrap());
|
|
||||||
|
|
||||||
assert!(l.add_words(conn, &["amsterdam"]).is_ok());
|
|
||||||
assert!(l.contains_word(conn, "plume").unwrap());
|
|
||||||
assert!(l.contains_word(conn, "amsterdam").unwrap());
|
|
||||||
assert_eq!(2, l.list_words(conn).unwrap().len());
|
|
||||||
|
|
||||||
assert!(l.set_words(conn, &["plume"]).is_ok());
|
|
||||||
assert!(l.contains_word(conn, "plume").unwrap());
|
|
||||||
assert!(!l.contains_word(conn, "amsterdam").unwrap());
|
|
||||||
assert_eq!(1, l.list_words(conn).unwrap().len());
|
|
||||||
assert_eq!("plume", l.list_words(conn).unwrap()[0]);
|
|
||||||
|
|
||||||
l.clear(conn).unwrap();
|
|
||||||
assert!(l.list_words(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(l.add_prefixes(conn, &["something"]).is_err());
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prefix_list() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let l = List::new(conn, "list", None, ListType::Prefix).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(l.kind(), ListType::Prefix);
|
|
||||||
assert!(l.list_prefixes(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(!l.contains_prefix(conn, "plume").unwrap());
|
|
||||||
assert!(l.add_prefixes(conn, &["plume"]).is_ok());
|
|
||||||
assert!(l.contains_prefix(conn, "plume").unwrap());
|
|
||||||
assert!(l.contains_prefix(conn, "plumelin").unwrap());
|
|
||||||
|
|
||||||
assert!(l.add_prefixes(conn, &["amsterdam"]).is_ok());
|
|
||||||
assert!(l.contains_prefix(conn, "plume").unwrap());
|
|
||||||
assert!(l.contains_prefix(conn, "amsterdam").unwrap());
|
|
||||||
assert_eq!(2, l.list_prefixes(conn).unwrap().len());
|
|
||||||
|
|
||||||
assert!(l.set_prefixes(conn, &["plume"]).is_ok());
|
|
||||||
assert!(l.contains_prefix(conn, "plume").unwrap());
|
|
||||||
assert!(!l.contains_prefix(conn, "amsterdam").unwrap());
|
|
||||||
assert_eq!(1, l.list_prefixes(conn).unwrap().len());
|
|
||||||
assert_eq!("plume", l.list_prefixes(conn).unwrap()[0]);
|
|
||||||
|
|
||||||
l.clear(conn).unwrap();
|
|
||||||
assert!(l.list_prefixes(conn).unwrap().is_empty());
|
|
||||||
|
|
||||||
assert!(l.add_words(conn, &["something"]).is_err());
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+27
-32
@@ -1,17 +1,20 @@
|
|||||||
use crate::{
|
|
||||||
ap_url, instance::Instance, safe_string::SafeString, schema::medias, users::User, Connection,
|
|
||||||
Error, PlumeRocket, Result,
|
|
||||||
};
|
|
||||||
use activitypub::object::Image;
|
use activitypub::object::Image;
|
||||||
use askama_escape::escape;
|
use askama_escape::escape;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use guid_create::GUID;
|
use guid_create::GUID;
|
||||||
|
use reqwest;
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
use plume_common::{
|
use plume_common::{
|
||||||
activity_pub::{inbox::FromId, Id},
|
activity_pub::{inbox::FromId, Id},
|
||||||
utils::MediaProcessor,
|
utils::MediaProcessor,
|
||||||
};
|
};
|
||||||
use reqwest;
|
|
||||||
use std::{fs, path::Path};
|
use instance::Instance;
|
||||||
|
use safe_string::SafeString;
|
||||||
|
use schema::medias;
|
||||||
|
use users::User;
|
||||||
|
use {ap_url, Connection, Error, PlumeRocket, Result};
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable)]
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
@@ -59,14 +62,7 @@ impl MediaCategory {
|
|||||||
impl Media {
|
impl Media {
|
||||||
insert!(medias, NewMedia);
|
insert!(medias, NewMedia);
|
||||||
get!(medias);
|
get!(medias);
|
||||||
|
list_by!(medias, for_user, owner_id as i32);
|
||||||
pub fn for_user(conn: &Connection, owner: i32) -> Result<Vec<Media>> {
|
|
||||||
medias::table
|
|
||||||
.filter(medias::owner_id.eq(owner))
|
|
||||||
.order(medias::id.desc())
|
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
|
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
|
||||||
medias::table.load::<Media>(conn).map_err(Error::from)
|
medias::table.load::<Media>(conn).map_err(Error::from)
|
||||||
@@ -79,7 +75,6 @@ impl Media {
|
|||||||
) -> Result<Vec<Media>> {
|
) -> Result<Vec<Media>> {
|
||||||
medias::table
|
medias::table
|
||||||
.filter(medias::owner_id.eq(user.id))
|
.filter(medias::owner_id.eq(user.id))
|
||||||
.order(medias::id.desc())
|
|
||||||
.offset(i64::from(min))
|
.offset(i64::from(min))
|
||||||
.limit(i64::from(max - min))
|
.limit(i64::from(max - min))
|
||||||
.load::<Media>(conn)
|
.load::<Media>(conn)
|
||||||
@@ -114,19 +109,15 @@ impl Media {
|
|||||||
Ok(match self.category() {
|
Ok(match self.category() {
|
||||||
MediaCategory::Image => SafeString::trusted(&format!(
|
MediaCategory::Image => SafeString::trusted(&format!(
|
||||||
r#"<img src="{}" alt="{}" title="{}">"#,
|
r#"<img src="{}" alt="{}" title="{}">"#,
|
||||||
url,
|
url, escape(&self.alt_text), escape(&self.alt_text)
|
||||||
escape(&self.alt_text),
|
|
||||||
escape(&self.alt_text)
|
|
||||||
)),
|
)),
|
||||||
MediaCategory::Audio => SafeString::trusted(&format!(
|
MediaCategory::Audio => SafeString::trusted(&format!(
|
||||||
r#"<div class="media-preview audio"></div><audio src="{}" title="{}" controls></audio>"#,
|
r#"<div class="media-preview audio"></div><audio src="{}" title="{}" controls></audio>"#,
|
||||||
url,
|
url, escape(&self.alt_text)
|
||||||
escape(&self.alt_text)
|
|
||||||
)),
|
)),
|
||||||
MediaCategory::Video => SafeString::trusted(&format!(
|
MediaCategory::Video => SafeString::trusted(&format!(
|
||||||
r#"<video src="{}" title="{}" controls></video>"#,
|
r#"<video src="{}" title="{}" controls></video>"#,
|
||||||
url,
|
url, escape(&self.alt_text)
|
||||||
escape(&self.alt_text)
|
|
||||||
)),
|
)),
|
||||||
MediaCategory::Unknown => SafeString::trusted(&format!(
|
MediaCategory::Unknown => SafeString::trusted(&format!(
|
||||||
r#"<a href="{}" class="media-preview unknown"></a>"#,
|
r#"<a href="{}" class="media-preview unknown"></a>"#,
|
||||||
@@ -149,12 +140,10 @@ impl Media {
|
|||||||
if self.is_remote {
|
if self.is_remote {
|
||||||
Ok(self.remote_url.clone().unwrap_or_default())
|
Ok(self.remote_url.clone().unwrap_or_default())
|
||||||
} else {
|
} else {
|
||||||
let p = Path::new(&self.file_path);
|
|
||||||
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
|
|
||||||
Ok(ap_url(&format!(
|
Ok(ap_url(&format!(
|
||||||
"{}/static/media/{}",
|
"{}/{}",
|
||||||
Instance::get_local()?.public_domain,
|
Instance::get_local()?.public_domain,
|
||||||
&filename
|
self.file_path
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,11 +194,10 @@ impl Media {
|
|||||||
.next()
|
.next()
|
||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.unwrap_or_else(|| String::from("png"));
|
.unwrap_or_else(|| String::from("png"));
|
||||||
let path = Path::new(&super::CONFIG.media_directory).join(format!(
|
let path =
|
||||||
"{}.{}",
|
Path::new("static")
|
||||||
GUID::rand().to_string(),
|
.join("media")
|
||||||
ext
|
.join(format!("{}.{}", GUID::rand().to_string(), ext));
|
||||||
));
|
|
||||||
|
|
||||||
let mut dest = fs::File::create(path.clone()).ok()?;
|
let mut dest = fs::File::create(path.clone()).ok()?;
|
||||||
reqwest::get(remote_url.as_str())
|
reqwest::get(remote_url.as_str())
|
||||||
@@ -260,11 +248,13 @@ impl Media {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{tests::db, users::tests as usersTests, Connection as Conn};
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use std::env::{current_dir, set_current_dir};
|
use std::env::{current_dir, set_current_dir};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use tests::db;
|
||||||
|
use users::tests as usersTests;
|
||||||
|
use Connection as Conn;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
|
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Media>) {
|
||||||
let mut wd = current_dir().unwrap().to_path_buf();
|
let mut wd = current_dir().unwrap().to_path_buf();
|
||||||
@@ -326,6 +316,8 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//set_owner
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete() {
|
fn delete() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
@@ -354,11 +346,13 @@ pub(crate) mod tests {
|
|||||||
assert!(!Path::new(&path).exists());
|
assert!(!Path::new(&path).exists());
|
||||||
|
|
||||||
clean(conn);
|
clean(conn);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
||||||
fn set_owner() {
|
fn set_owner() {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
@@ -402,6 +396,7 @@ pub(crate) mod tests {
|
|||||||
.any(|m| m.id == media.id));
|
.any(|m| m.id == media.id));
|
||||||
|
|
||||||
clean(conn);
|
clean(conn);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
use crate::{
|
|
||||||
comments::Comment, notifications::*, posts::Post, schema::mentions, users::User, Connection,
|
|
||||||
Error, PlumeRocket, Result,
|
|
||||||
};
|
|
||||||
use activitypub::link;
|
use activitypub::link;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use comments::Comment;
|
||||||
|
use notifications::*;
|
||||||
use plume_common::activity_pub::inbox::AsActor;
|
use plume_common::activity_pub::inbox::AsActor;
|
||||||
|
use posts::Post;
|
||||||
|
use schema::mentions;
|
||||||
|
use users::User;
|
||||||
|
use PlumeRocket;
|
||||||
|
use {Connection, Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
pub struct Mention {
|
pub struct Mention {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
use crate::{Connection, Error, Result};
|
use Connection;
|
||||||
|
use Error;
|
||||||
|
use Result;
|
||||||
|
|
||||||
use diesel::connection::{Connection as Conn, SimpleConnection};
|
use diesel::connection::{Connection as Conn, SimpleConnection};
|
||||||
use migrations_internals::{setup_database, MigrationConnection};
|
use migrations_internals::{setup_database, MigrationConnection};
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[allow(dead_code)] //variants might not be constructed if not required by current migrations
|
#[allow(dead_code)] //variants might not be constructed if not required by current migrations
|
||||||
enum Action {
|
enum Action {
|
||||||
Sql(&'static str),
|
Sql(&'static str),
|
||||||
Function(&'static dyn Fn(&Connection, &Path) -> Result<()>),
|
Function(&'static Fn(&Connection, &Path) -> Result<()>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use crate::{
|
|
||||||
comments::Comment,
|
|
||||||
follows::Follow,
|
|
||||||
likes::Like,
|
|
||||||
mentions::Mention,
|
|
||||||
posts::Post,
|
|
||||||
reshares::Reshare,
|
|
||||||
schema::{follows, notifications},
|
|
||||||
users::User,
|
|
||||||
Connection, Error, Result,
|
|
||||||
};
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use comments::Comment;
|
||||||
|
use follows::Follow;
|
||||||
|
use likes::Like;
|
||||||
|
use mentions::Mention;
|
||||||
|
use posts::Post;
|
||||||
|
use reshares::Reshare;
|
||||||
|
use schema::notifications;
|
||||||
|
use users::User;
|
||||||
|
use {Connection, Error, Result};
|
||||||
|
|
||||||
pub mod notification_kind {
|
pub mod notification_kind {
|
||||||
pub const COMMENT: &str = "COMMENT";
|
pub const COMMENT: &str = "COMMENT";
|
||||||
@@ -65,16 +64,6 @@ impl Notification {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_followed_by(conn: &Connection, user: &User) -> Result<Vec<Notification>> {
|
|
||||||
notifications::table
|
|
||||||
.inner_join(follows::table.on(notifications::object_id.eq(follows::id)))
|
|
||||||
.filter(notifications::kind.eq(notification_kind::FOLLOW))
|
|
||||||
.filter(follows::follower_id.eq(user.id))
|
|
||||||
.select(notifications::all_columns)
|
|
||||||
.load::<Notification>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count_for_user(conn: &Connection, user: &User) -> Result<i64> {
|
pub fn count_for_user(conn: &Connection, user: &User) -> Result<i64> {
|
||||||
notifications::table
|
notifications::table
|
||||||
.filter(notifications::user_id.eq(user.id))
|
.filter(notifications::user_id.eq(user.id))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::{schema::password_reset_requests, Connection, Error, Result};
|
|
||||||
use chrono::{offset::Utc, Duration, NaiveDateTime};
|
use chrono::{offset::Utc, Duration, NaiveDateTime};
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
use schema::password_reset_requests;
|
||||||
|
use {Connection, Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable)]
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
pub struct PasswordResetRequest {
|
pub struct PasswordResetRequest {
|
||||||
@@ -74,8 +75,9 @@ impl PasswordResetRequest {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{tests::db, users::tests as user_tests};
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use tests::db;
|
||||||
|
use users::tests as user_tests;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_insert_and_find_password_reset_request() {
|
fn test_insert_and_find_password_reset_request() {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ pub use self::module::PlumeRocket;
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
mod module {
|
mod module {
|
||||||
use crate::{db_conn::DbConn, search, users};
|
use crate::db_conn::DbConn;
|
||||||
|
use crate::search;
|
||||||
|
use crate::users;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
request::{self, FlashMessage, FromRequest, Request},
|
request::{self, FlashMessage, FromRequest, Request},
|
||||||
Outcome, State,
|
Outcome, State,
|
||||||
@@ -27,9 +29,9 @@ mod module {
|
|||||||
let conn = request.guard::<DbConn>()?;
|
let conn = request.guard::<DbConn>()?;
|
||||||
let intl = request.guard::<rocket_i18n::I18n>()?;
|
let intl = request.guard::<rocket_i18n::I18n>()?;
|
||||||
let user = request.guard::<users::User>().succeeded();
|
let user = request.guard::<users::User>().succeeded();
|
||||||
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
|
let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
|
||||||
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
|
let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
|
||||||
let flash_msg = request.guard::<FlashMessage<'_, '_>>().succeeded();
|
let flash_msg = request.guard::<FlashMessage>().succeeded();
|
||||||
Outcome::Success(PlumeRocket {
|
Outcome::Success(PlumeRocket {
|
||||||
conn,
|
conn,
|
||||||
intl,
|
intl,
|
||||||
@@ -44,7 +46,9 @@ mod module {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod module {
|
mod module {
|
||||||
use crate::{db_conn::DbConn, search, users};
|
use crate::db_conn::DbConn;
|
||||||
|
use crate::search;
|
||||||
|
use crate::users;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
request::{self, FromRequest, Request},
|
request::{self, FromRequest, Request},
|
||||||
Outcome, State,
|
Outcome, State,
|
||||||
@@ -66,8 +70,8 @@ mod module {
|
|||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
|
||||||
let conn = request.guard::<DbConn>()?;
|
let conn = request.guard::<DbConn>()?;
|
||||||
let user = request.guard::<users::User>().succeeded();
|
let user = request.guard::<users::User>().succeeded();
|
||||||
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
|
let worker = request.guard::<State<Arc<ScheduledThreadPool>>>()?;
|
||||||
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
|
let searcher = request.guard::<State<Arc<search::Searcher>>>()?;
|
||||||
Outcome::Success(PlumeRocket {
|
Outcome::Success(PlumeRocket {
|
||||||
conn,
|
conn,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use crate::{posts::Post, schema::post_authors, users::User, Error, Result};
|
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use posts::Post;
|
||||||
|
use schema::post_authors;
|
||||||
|
use users::User;
|
||||||
|
use {Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, Associations)]
|
#[derive(Clone, Queryable, Identifiable, Associations)]
|
||||||
#[belongs_to(Post)]
|
#[belongs_to(Post)]
|
||||||
#[belongs_to(User, foreign_key = "author_id")]
|
#[belongs_to(User, foreign_key = "author_id")]
|
||||||
|
|||||||
+98
-27
@@ -1,8 +1,3 @@
|
|||||||
use crate::{
|
|
||||||
ap_url, blogs::Blog, instance::Instance, medias::Media, mentions::Mention, post_authors::*,
|
|
||||||
safe_string::SafeString, schema::posts, search::Searcher, tags::*, timeline::*, users::User,
|
|
||||||
Connection, Error, PlumeRocket, Result, CONFIG,
|
|
||||||
};
|
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
activity::{Create, Delete, Update},
|
activity::{Create, Delete, Update},
|
||||||
link,
|
link,
|
||||||
@@ -12,6 +7,13 @@ use activitypub::{
|
|||||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use heck::{CamelCase, KebabCase};
|
use heck::{CamelCase, KebabCase};
|
||||||
|
use serde_json;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use blogs::Blog;
|
||||||
|
use instance::Instance;
|
||||||
|
use medias::Media;
|
||||||
|
use mentions::Mention;
|
||||||
use plume_common::{
|
use plume_common::{
|
||||||
activity_pub::{
|
activity_pub::{
|
||||||
inbox::{AsObject, FromId},
|
inbox::{AsObject, FromId},
|
||||||
@@ -19,8 +21,13 @@ use plume_common::{
|
|||||||
},
|
},
|
||||||
utils::md_to_html,
|
utils::md_to_html,
|
||||||
};
|
};
|
||||||
use serde_json;
|
use post_authors::*;
|
||||||
use std::collections::HashSet;
|
use safe_string::SafeString;
|
||||||
|
use schema::posts;
|
||||||
|
use search::Searcher;
|
||||||
|
use tags::*;
|
||||||
|
use users::User;
|
||||||
|
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
|
||||||
|
|
||||||
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
||||||
|
|
||||||
@@ -103,7 +110,7 @@ impl Post {
|
|||||||
tag: String,
|
tag: String,
|
||||||
(min, max): (i32, i32),
|
(min, max): (i32, i32),
|
||||||
) -> Result<Vec<Post>> {
|
) -> Result<Vec<Post>> {
|
||||||
use crate::schema::tags;
|
use schema::tags;
|
||||||
|
|
||||||
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
||||||
posts::table
|
posts::table
|
||||||
@@ -117,7 +124,7 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_for_tag(conn: &Connection, tag: String) -> Result<i64> {
|
pub fn count_for_tag(conn: &Connection, tag: String) -> Result<i64> {
|
||||||
use crate::schema::tags;
|
use schema::tags;
|
||||||
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
||||||
posts::table
|
posts::table
|
||||||
.filter(posts::id.eq_any(ids))
|
.filter(posts::id.eq_any(ids))
|
||||||
@@ -131,8 +138,8 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||||
use crate::schema::post_authors;
|
use schema::post_authors;
|
||||||
use crate::schema::users;
|
use schema::users;
|
||||||
let local_authors = users::table
|
let local_authors = users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
.select(users::id);
|
.select(users::id);
|
||||||
@@ -175,12 +182,21 @@ impl Post {
|
|||||||
query.get_results::<Post>(conn).map_err(Error::from)
|
query.get_results::<Post>(conn).map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_recents(conn: &Connection, limit: i64) -> Result<Vec<Post>> {
|
||||||
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
|
.filter(posts::published.eq(true))
|
||||||
|
.limit(limit)
|
||||||
|
.load::<Post>(conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_recents_for_author(
|
pub fn get_recents_for_author(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
author: &User,
|
author: &User,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<Post>> {
|
) -> Result<Vec<Post>> {
|
||||||
use crate::schema::post_authors;
|
use schema::post_authors;
|
||||||
|
|
||||||
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
||||||
posts::table
|
posts::table
|
||||||
@@ -230,8 +246,62 @@ impl Post {
|
|||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Give a page of all the recent posts known to this instance (= federated timeline)
|
||||||
|
pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Post>> {
|
||||||
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
|
.filter(posts::published.eq(true))
|
||||||
|
.offset(min.into())
|
||||||
|
.limit((max - min).into())
|
||||||
|
.load::<Post>(conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Give a page of posts from a specific instance
|
||||||
|
pub fn get_instance_page(
|
||||||
|
conn: &Connection,
|
||||||
|
instance_id: i32,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Result<Vec<Post>> {
|
||||||
|
use schema::blogs;
|
||||||
|
|
||||||
|
let blog_ids = blogs::table
|
||||||
|
.filter(blogs::instance_id.eq(instance_id))
|
||||||
|
.select(blogs::id);
|
||||||
|
|
||||||
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
|
.filter(posts::published.eq(true))
|
||||||
|
.filter(posts::blog_id.eq_any(blog_ids))
|
||||||
|
.offset(min.into())
|
||||||
|
.limit((max - min).into())
|
||||||
|
.load::<Post>(conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Give a page of customized user feed, based on a list of followed users
|
||||||
|
pub fn user_feed_page(
|
||||||
|
conn: &Connection,
|
||||||
|
followed: Vec<i32>,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Result<Vec<Post>> {
|
||||||
|
use schema::post_authors;
|
||||||
|
let post_ids = post_authors::table
|
||||||
|
.filter(post_authors::author_id.eq_any(followed))
|
||||||
|
.select(post_authors::post_id);
|
||||||
|
|
||||||
|
posts::table
|
||||||
|
.order(posts::creation_date.desc())
|
||||||
|
.filter(posts::published.eq(true))
|
||||||
|
.filter(posts::id.eq_any(post_ids))
|
||||||
|
.offset(min.into())
|
||||||
|
.limit((max - min).into())
|
||||||
|
.load::<Post>(conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn drafts_by_author(conn: &Connection, author: &User) -> Result<Vec<Post>> {
|
pub fn drafts_by_author(conn: &Connection, author: &User) -> Result<Vec<Post>> {
|
||||||
use crate::schema::post_authors;
|
use schema::post_authors;
|
||||||
|
|
||||||
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
||||||
posts::table
|
posts::table
|
||||||
@@ -243,8 +313,8 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
|
pub fn get_authors(&self, conn: &Connection) -> Result<Vec<User>> {
|
||||||
use crate::schema::post_authors;
|
use schema::post_authors;
|
||||||
use crate::schema::users;
|
use schema::users;
|
||||||
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
|
let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id);
|
||||||
users::table
|
users::table
|
||||||
.filter(users::id.eq_any(author_list))
|
.filter(users::id.eq_any(author_list))
|
||||||
@@ -253,7 +323,7 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result<bool> {
|
pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result<bool> {
|
||||||
use crate::schema::post_authors;
|
use schema::post_authors;
|
||||||
Ok(PostAuthor::belonging_to(self)
|
Ok(PostAuthor::belonging_to(self)
|
||||||
.filter(post_authors::author_id.eq(author_id))
|
.filter(post_authors::author_id.eq(author_id))
|
||||||
.count()
|
.count()
|
||||||
@@ -262,15 +332,18 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
|
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
|
||||||
use crate::schema::blogs;
|
use schema::blogs;
|
||||||
blogs::table
|
blogs::table
|
||||||
.filter(blogs::id.eq(self.blog_id))
|
.filter(blogs::id.eq(self.blog_id))
|
||||||
.first(conn)
|
.limit(1)
|
||||||
.map_err(Error::from)
|
.load::<Blog>(conn)?
|
||||||
|
.into_iter()
|
||||||
|
.nth(0)
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
|
pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
|
||||||
use crate::schema::likes;
|
use schema::likes;
|
||||||
likes::table
|
likes::table
|
||||||
.filter(likes::post_id.eq(self.id))
|
.filter(likes::post_id.eq(self.id))
|
||||||
.count()
|
.count()
|
||||||
@@ -279,7 +352,7 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_reshares(&self, conn: &Connection) -> Result<i64> {
|
pub fn count_reshares(&self, conn: &Connection) -> Result<i64> {
|
||||||
use crate::schema::reshares;
|
use schema::reshares;
|
||||||
reshares::table
|
reshares::table
|
||||||
.filter(reshares::post_id.eq(self.id))
|
.filter(reshares::post_id.eq(self.id))
|
||||||
.count()
|
.count()
|
||||||
@@ -624,7 +697,7 @@ impl FromId<PlumeRocket> for Post {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
|
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() {
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
serde_json::from_value::<link::Mention>(tag.clone())
|
serde_json::from_value::<link::Mention>(tag.clone())
|
||||||
.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
|
.map(|m| Mention::from_activity(conn, &m, post.id, true, true))
|
||||||
@@ -644,9 +717,6 @@ impl FromId<PlumeRocket> for Post {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timeline::add_to_all_timelines(c, &post, Kind::Original)?;
|
|
||||||
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,7 +787,7 @@ impl FromId<PlumeRocket> for PostUpdate {
|
|||||||
.ok()
|
.ok()
|
||||||
.map(|x| x.content),
|
.map(|x| x.content),
|
||||||
license: updated.custom_props.license_string().ok(),
|
license: updated.custom_props.license_string().ok(),
|
||||||
tags: updated.object.object_props.tag,
|
tags: updated.object.object_props.tag.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,7 +868,7 @@ impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
|
|||||||
|
|
||||||
impl IntoId for Post {
|
impl IntoId for Post {
|
||||||
fn into_id(self) -> Id {
|
fn into_id(self) -> Id {
|
||||||
Id::new(self.ap_url)
|
Id::new(self.ap_url.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,6 +928,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unexpected result"),
|
_ => panic!("Unexpected result"),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
use crate::{
|
|
||||||
notifications::*, posts::Post, schema::reshares, timeline::*, users::User, Connection, Error,
|
|
||||||
PlumeRocket, Result,
|
|
||||||
};
|
|
||||||
use activitypub::activity::{Announce, Undo};
|
use activitypub::activity::{Announce, Undo};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{AsActor, AsObject, FromId},
|
inbox::{AsActor, AsObject, FromId},
|
||||||
Id, IntoId, PUBLIC_VISIBILITY,
|
Id, IntoId, PUBLIC_VISIBILITY,
|
||||||
};
|
};
|
||||||
|
use posts::Post;
|
||||||
|
use schema::reshares;
|
||||||
|
use users::User;
|
||||||
|
use {Connection, Error, PlumeRocket, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable)]
|
#[derive(Clone, Queryable, Identifiable)]
|
||||||
pub struct Reshare {
|
pub struct Reshare {
|
||||||
@@ -122,8 +124,6 @@ impl AsObject<User, Announce, &PlumeRocket> for Post {
|
|||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
reshare.notify(conn)?;
|
reshare.notify(conn)?;
|
||||||
|
|
||||||
Timeline::add_to_all_timelines(c, &self, Kind::Reshare(&actor))?;
|
|
||||||
Ok(reshare)
|
Ok(reshare)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,41 +31,7 @@ lazy_static! {
|
|||||||
.add_tag_attributes("label", ["for"].iter())
|
.add_tag_attributes("label", ["for"].iter())
|
||||||
.add_tag_attributes("input", ["type", "checked"].iter())
|
.add_tag_attributes("input", ["type", "checked"].iter())
|
||||||
.add_allowed_classes("input", ["cw-checkbox"].iter())
|
.add_allowed_classes("input", ["cw-checkbox"].iter())
|
||||||
.add_allowed_classes(
|
.add_allowed_classes("span", ["cw-container", "cw-text"].iter())
|
||||||
"span",
|
|
||||||
[
|
|
||||||
"cw-container",
|
|
||||||
"cw-text",
|
|
||||||
//Scope classes for the syntax highlighting.
|
|
||||||
"attribute-name",
|
|
||||||
"comment",
|
|
||||||
"constant",
|
|
||||||
"control",
|
|
||||||
"declaration",
|
|
||||||
"entity",
|
|
||||||
"function",
|
|
||||||
"invalid",
|
|
||||||
"keyword",
|
|
||||||
"language",
|
|
||||||
"modifier",
|
|
||||||
"name",
|
|
||||||
"numeric",
|
|
||||||
"operator",
|
|
||||||
"parameter",
|
|
||||||
"punctuation",
|
|
||||||
"source",
|
|
||||||
"storage",
|
|
||||||
"string",
|
|
||||||
"support",
|
|
||||||
"tag",
|
|
||||||
"type",
|
|
||||||
"variable",
|
|
||||||
]
|
|
||||||
.iter(),
|
|
||||||
)
|
|
||||||
// Related to https://github.com/Plume-org/Plume/issues/637
|
|
||||||
.add_allowed_classes("sup", ["footnote-reference", "footnote-definition-label"].iter())
|
|
||||||
.add_allowed_classes("div", ["footnote-definition"].iter())
|
|
||||||
.attribute_filter(|elem, att, val| match (elem, att) {
|
.attribute_filter(|elem, att, val| match (elem, att) {
|
||||||
("input", "type") => Some("checkbox".into()),
|
("input", "type") => Some("checkbox".into()),
|
||||||
("input", "checked") => Some("checked".into()),
|
("input", "checked") => Some("checked".into()),
|
||||||
@@ -82,7 +48,7 @@ lazy_static! {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url_add_prefix(url: &str) -> Option<Cow<'_, str>> {
|
fn url_add_prefix(url: &str) -> Option<Cow<str>> {
|
||||||
if url.starts_with('#') && !url.starts_with("#postcontent-") {
|
if url.starts_with('#') && !url.starts_with("#postcontent-") {
|
||||||
//if start with an #
|
//if start with an #
|
||||||
let mut new_url = "#postcontent-".to_owned(); //change to valid id
|
let mut new_url = "#postcontent-".to_owned(); //change to valid id
|
||||||
@@ -139,7 +105,7 @@ struct SafeStringVisitor;
|
|||||||
impl<'de> Visitor<'de> for SafeStringVisitor {
|
impl<'de> Visitor<'de> for SafeStringVisitor {
|
||||||
type Value = SafeString;
|
type Value = SafeString;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
formatter.write_str("a string")
|
formatter.write_str("a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +147,7 @@ where
|
|||||||
DB: diesel::backend::Backend,
|
DB: diesel::backend::Backend,
|
||||||
str: ToSql<diesel::sql_types::Text, DB>,
|
str: ToSql<diesel::sql_types::Text, DB>,
|
||||||
{
|
{
|
||||||
fn to_sql<W: Write>(&self, out: &mut Output<'_, W, DB>) -> serialize::Result {
|
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
|
||||||
str::to_sql(&self.value, out)
|
str::to_sql(&self.value, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +159,7 @@ impl Borrow<str> for SafeString {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Display for SafeString {
|
impl Display for SafeString {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", self.value)
|
write!(f, "{}", self.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ table! {
|
|||||||
summary_html -> Text,
|
summary_html -> Text,
|
||||||
icon_id -> Nullable<Int4>,
|
icon_id -> Nullable<Int4>,
|
||||||
banner_id -> Nullable<Int4>,
|
banner_id -> Nullable<Int4>,
|
||||||
theme -> Nullable<Varchar>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,15 +72,6 @@ table! {
|
|||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
table! {
|
|
||||||
email_blocklist(id){
|
|
||||||
id -> Int4,
|
|
||||||
email_address -> VarChar,
|
|
||||||
note -> Text,
|
|
||||||
notify_user -> Bool,
|
|
||||||
notification_text -> Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
follows (id) {
|
follows (id) {
|
||||||
@@ -119,26 +109,6 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
|
||||||
list_elems (id) {
|
|
||||||
id -> Int4,
|
|
||||||
list_id -> Int4,
|
|
||||||
user_id -> Nullable<Int4>,
|
|
||||||
blog_id -> Nullable<Int4>,
|
|
||||||
word -> Nullable<Varchar>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
lists (id) {
|
|
||||||
id -> Int4,
|
|
||||||
name -> Varchar,
|
|
||||||
user_id -> Nullable<Int4>,
|
|
||||||
#[sql_name = "type"]
|
|
||||||
type_ -> Int4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
medias (id) {
|
medias (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@@ -224,23 +194,6 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
|
||||||
timeline (id) {
|
|
||||||
id -> Int4,
|
|
||||||
post_id -> Int4,
|
|
||||||
timeline_id -> Int4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
timeline_definition (id) {
|
|
||||||
id -> Int4,
|
|
||||||
user_id -> Nullable<Int4>,
|
|
||||||
name -> Varchar,
|
|
||||||
query -> Varchar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (id) {
|
users (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@@ -248,6 +201,7 @@ table! {
|
|||||||
display_name -> Varchar,
|
display_name -> Varchar,
|
||||||
outbox_url -> Varchar,
|
outbox_url -> Varchar,
|
||||||
inbox_url -> Varchar,
|
inbox_url -> Varchar,
|
||||||
|
is_admin -> Bool,
|
||||||
summary -> Text,
|
summary -> Text,
|
||||||
email -> Nullable<Text>,
|
email -> Nullable<Text>,
|
||||||
hashed_password -> Nullable<Text>,
|
hashed_password -> Nullable<Text>,
|
||||||
@@ -262,9 +216,6 @@ table! {
|
|||||||
last_fetched_date -> Timestamp,
|
last_fetched_date -> Timestamp,
|
||||||
fqn -> Text,
|
fqn -> Text,
|
||||||
summary_html -> Text,
|
summary_html -> Text,
|
||||||
role -> Int4,
|
|
||||||
preferred_theme -> Nullable<Varchar>,
|
|
||||||
hide_custom_css -> Bool,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,10 +230,6 @@ joinable!(comments -> posts (post_id));
|
|||||||
joinable!(comments -> users (author_id));
|
joinable!(comments -> users (author_id));
|
||||||
joinable!(likes -> posts (post_id));
|
joinable!(likes -> posts (post_id));
|
||||||
joinable!(likes -> users (user_id));
|
joinable!(likes -> users (user_id));
|
||||||
joinable!(list_elems -> blogs (blog_id));
|
|
||||||
joinable!(list_elems -> lists (list_id));
|
|
||||||
joinable!(list_elems -> users (user_id));
|
|
||||||
joinable!(lists -> users (user_id));
|
|
||||||
joinable!(mentions -> comments (comment_id));
|
joinable!(mentions -> comments (comment_id));
|
||||||
joinable!(mentions -> posts (post_id));
|
joinable!(mentions -> posts (post_id));
|
||||||
joinable!(mentions -> users (mentioned_id));
|
joinable!(mentions -> users (mentioned_id));
|
||||||
@@ -294,9 +241,6 @@ joinable!(posts -> medias (cover_id));
|
|||||||
joinable!(reshares -> posts (post_id));
|
joinable!(reshares -> posts (post_id));
|
||||||
joinable!(reshares -> users (user_id));
|
joinable!(reshares -> users (user_id));
|
||||||
joinable!(tags -> posts (post_id));
|
joinable!(tags -> posts (post_id));
|
||||||
joinable!(timeline -> posts (post_id));
|
|
||||||
joinable!(timeline -> timeline_definition (timeline_id));
|
|
||||||
joinable!(timeline_definition -> users (user_id));
|
|
||||||
joinable!(users -> instances (instance_id));
|
joinable!(users -> instances (instance_id));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
@@ -309,8 +253,6 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
follows,
|
follows,
|
||||||
instances,
|
instances,
|
||||||
likes,
|
likes,
|
||||||
list_elems,
|
|
||||||
lists,
|
|
||||||
medias,
|
medias,
|
||||||
mentions,
|
mentions,
|
||||||
notifications,
|
notifications,
|
||||||
@@ -319,7 +261,5 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
posts,
|
posts,
|
||||||
reshares,
|
reshares,
|
||||||
tags,
|
tags,
|
||||||
timeline,
|
|
||||||
timeline_definition,
|
|
||||||
users,
|
users,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,20 +8,18 @@ pub use self::searcher::*;
|
|||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::{Query, Searcher};
|
use super::{Query, Searcher};
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use plume_common::utils::random_hex;
|
|
||||||
use std::env::temp_dir;
|
use std::env::temp_dir;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use blogs::tests::fill_database;
|
||||||
blogs::tests::fill_database,
|
use plume_common::utils::random_hex;
|
||||||
post_authors::*,
|
use post_authors::*;
|
||||||
posts::{NewPost, Post},
|
use posts::{NewPost, Post};
|
||||||
safe_string::SafeString,
|
use safe_string::SafeString;
|
||||||
tests::db,
|
use tests::db;
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) fn get_searcher() -> Searcher {
|
pub(crate) fn get_searcher() -> Searcher {
|
||||||
let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
|
let dir = temp_dir().join("plume-test");
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
Searcher::open(&dir)
|
Searcher::open(&dir)
|
||||||
} else {
|
} else {
|
||||||
@@ -98,10 +96,11 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open() {
|
fn open() {
|
||||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
|
||||||
{
|
{
|
||||||
Searcher::create(&dir).unwrap();
|
get_searcher()
|
||||||
}
|
}; //make sure $tmp/plume-test-tantivy exist
|
||||||
|
|
||||||
|
let dir = temp_dir().join("plume-test");
|
||||||
Searcher::open(&dir).unwrap();
|
Searcher::open(&dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +175,7 @@ pub(crate) mod tests {
|
|||||||
assert!(searcher
|
assert!(searcher
|
||||||
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
|
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
|
||||||
.is_empty());
|
.is_empty());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::search::searcher::Searcher;
|
|
||||||
use chrono::{naive::NaiveDate, offset::Utc, Datelike};
|
use chrono::{naive::NaiveDate, offset::Utc, Datelike};
|
||||||
|
use search::searcher::Searcher;
|
||||||
use std::{cmp, ops::Bound};
|
use std::{cmp, ops::Bound};
|
||||||
use tantivy::{query::*, schema::*, Term};
|
use tantivy::{query::*, schema::*, Term};
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ impl PlumeQuery {
|
|||||||
|
|
||||||
/// Convert this Query to a Tantivy Query
|
/// Convert this Query to a Tantivy Query
|
||||||
pub fn into_query(self) -> BooleanQuery {
|
pub fn into_query(self) -> BooleanQuery {
|
||||||
let mut result: Vec<(Occur, Box<dyn Query>)> = Vec::new();
|
let mut result: Vec<(Occur, Box<Query>)> = Vec::new();
|
||||||
gen_to_query!(self, result; normal: title, subtitle, content, tag;
|
gen_to_query!(self, result; normal: title, subtitle, content, tag;
|
||||||
oneoff: instance, author, blog, lang, license);
|
oneoff: instance, author, blog, lang, license);
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ impl PlumeQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// map a token and it's field to a query
|
// map a token and it's field to a query
|
||||||
fn token_to_query(token: &str, field_name: &str) -> Box<dyn Query> {
|
fn token_to_query(token: &str, field_name: &str) -> Box<Query> {
|
||||||
let token = token.to_lowercase();
|
let token = token.to_lowercase();
|
||||||
let token = token.as_str();
|
let token = token.as_str();
|
||||||
let field = Searcher::schema().get_field(field_name).unwrap();
|
let field = Searcher::schema().get_field(field_name).unwrap();
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use crate::{
|
use instance::Instance;
|
||||||
instance::Instance,
|
use posts::Post;
|
||||||
posts::Post,
|
use schema::posts;
|
||||||
schema::posts,
|
use tags::Tag;
|
||||||
search::{query::PlumeQuery, tokenizer},
|
use Connection;
|
||||||
tags::Tag,
|
|
||||||
Connection, Result,
|
|
||||||
};
|
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -16,6 +14,10 @@ use tantivy::{
|
|||||||
};
|
};
|
||||||
use whatlang::{detect as detect_lang, Lang};
|
use whatlang::{detect as detect_lang, Lang};
|
||||||
|
|
||||||
|
use super::tokenizer;
|
||||||
|
use search::query::PlumeQuery;
|
||||||
|
use Result;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SearcherError {
|
pub enum SearcherError {
|
||||||
IndexCreationError,
|
IndexCreationError,
|
||||||
@@ -70,7 +72,7 @@ impl Searcher {
|
|||||||
schema_builder.build()
|
schema_builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create(path: &dyn AsRef<Path>) -> Result<Self> {
|
pub fn create(path: &AsRef<Path>) -> Result<Self> {
|
||||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||||
|
|
||||||
let content_tokenizer = SimpleTokenizer
|
let content_tokenizer = SimpleTokenizer
|
||||||
@@ -109,7 +111,7 @@ impl Searcher {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(path: &dyn AsRef<Path>) -> Result<Self> {
|
pub fn open(path: &AsRef<Path>) -> Result<Self> {
|
||||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||||
|
|
||||||
let content_tokenizer = SimpleTokenizer
|
let content_tokenizer = SimpleTokenizer
|
||||||
@@ -146,10 +148,6 @@ impl Searcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_document(&self, conn: &Connection, post: &Post) -> Result<()> {
|
pub fn add_document(&self, conn: &Connection, post: &Post) -> Result<()> {
|
||||||
if !post.published {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let schema = self.index.schema();
|
let schema = self.index.schema();
|
||||||
|
|
||||||
let post_id = schema.get_field("post_id").unwrap();
|
let post_id = schema.get_field("post_id").unwrap();
|
||||||
@@ -173,7 +171,7 @@ impl Searcher {
|
|||||||
post_id => i64::from(post.id),
|
post_id => i64::from(post.id),
|
||||||
author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "),
|
author => post.get_authors(conn)?.into_iter().map(|u| u.fqn).join(" "),
|
||||||
creation_date => i64::from(post.creation_date.num_days_from_ce()),
|
creation_date => i64::from(post.creation_date.num_days_from_ce()),
|
||||||
instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain,
|
instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain.clone(),
|
||||||
tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "),
|
tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "),
|
||||||
blog_name => post.get_blog(conn)?.title,
|
blog_name => post.get_blog(conn)?.title,
|
||||||
content => post.content.get().clone(),
|
content => post.content.get().clone(),
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use crate::{ap_url, instance::Instance, schema::tags, Connection, Error, Result};
|
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use instance::Instance;
|
||||||
use plume_common::activity_pub::Hashtag;
|
use plume_common::activity_pub::Hashtag;
|
||||||
|
use schema::tags;
|
||||||
|
use {ap_url, Connection, Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Identifiable, Queryable)]
|
#[derive(Clone, Identifiable, Queryable)]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
|
|||||||
@@ -1,846 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
lists::List,
|
|
||||||
posts::Post,
|
|
||||||
schema::{posts, timeline, timeline_definition},
|
|
||||||
Connection, Error, PlumeRocket, Result,
|
|
||||||
};
|
|
||||||
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
pub(crate) mod query;
|
|
||||||
|
|
||||||
pub use self::query::Kind;
|
|
||||||
use self::query::{QueryError, TimelineQuery};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Queryable, Identifiable, AsChangeset)]
|
|
||||||
#[table_name = "timeline_definition"]
|
|
||||||
pub struct Timeline {
|
|
||||||
pub id: i32,
|
|
||||||
pub user_id: Option<i32>,
|
|
||||||
pub name: String,
|
|
||||||
pub query: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
|
||||||
#[table_name = "timeline_definition"]
|
|
||||||
pub struct NewTimeline {
|
|
||||||
user_id: Option<i32>,
|
|
||||||
name: String,
|
|
||||||
query: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
|
||||||
#[table_name = "timeline"]
|
|
||||||
struct TimelineEntry {
|
|
||||||
pub post_id: i32,
|
|
||||||
pub timeline_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Timeline {
|
|
||||||
insert!(timeline_definition, NewTimeline);
|
|
||||||
get!(timeline_definition);
|
|
||||||
|
|
||||||
pub fn find_for_user_by_name(
|
|
||||||
conn: &Connection,
|
|
||||||
user_id: Option<i32>,
|
|
||||||
name: &str,
|
|
||||||
) -> Result<Self> {
|
|
||||||
if let Some(user_id) = user_id {
|
|
||||||
timeline_definition::table
|
|
||||||
.filter(timeline_definition::user_id.eq(user_id))
|
|
||||||
.filter(timeline_definition::name.eq(name))
|
|
||||||
.first(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
} else {
|
|
||||||
timeline_definition::table
|
|
||||||
.filter(timeline_definition::user_id.is_null())
|
|
||||||
.filter(timeline_definition::name.eq(name))
|
|
||||||
.first(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_for_user(conn: &Connection, user_id: Option<i32>) -> Result<Vec<Self>> {
|
|
||||||
if let Some(user_id) = user_id {
|
|
||||||
timeline_definition::table
|
|
||||||
.filter(timeline_definition::user_id.eq(user_id))
|
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
} else {
|
|
||||||
timeline_definition::table
|
|
||||||
.filter(timeline_definition::user_id.is_null())
|
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `list_for_user`, but also includes instance timelines if `user_id` is `Some`.
|
|
||||||
pub fn list_all_for_user(conn: &Connection, user_id: Option<i32>) -> Result<Vec<Self>> {
|
|
||||||
if let Some(user_id) = user_id {
|
|
||||||
timeline_definition::table
|
|
||||||
.filter(
|
|
||||||
timeline_definition::user_id
|
|
||||||
.eq(user_id)
|
|
||||||
.or(timeline_definition::user_id.is_null()),
|
|
||||||
)
|
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
} else {
|
|
||||||
timeline_definition::table
|
|
||||||
.filter(timeline_definition::user_id.is_null())
|
|
||||||
.load::<Self>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_for_user(
|
|
||||||
conn: &Connection,
|
|
||||||
user_id: i32,
|
|
||||||
name: String,
|
|
||||||
query_string: String,
|
|
||||||
) -> Result<Timeline> {
|
|
||||||
{
|
|
||||||
let query = TimelineQuery::parse(&query_string)?; // verify the query is valid
|
|
||||||
if let Some(err) =
|
|
||||||
query
|
|
||||||
.list_used_lists()
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|(name, kind)| {
|
|
||||||
let list = List::find_for_user_by_name(conn, Some(user_id), &name)
|
|
||||||
.map(|l| l.kind() == kind);
|
|
||||||
match list {
|
|
||||||
Ok(true) => None,
|
|
||||||
Ok(false) => Some(Error::TimelineQuery(QueryError::RuntimeError(
|
|
||||||
format!("list '{}' has the wrong type for this usage", name),
|
|
||||||
))),
|
|
||||||
Err(_) => Some(Error::TimelineQuery(QueryError::RuntimeError(
|
|
||||||
format!("list '{}' was not found", name),
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
{
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::insert(
|
|
||||||
conn,
|
|
||||||
NewTimeline {
|
|
||||||
user_id: Some(user_id),
|
|
||||||
name,
|
|
||||||
query: query_string,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_for_instance(
|
|
||||||
conn: &Connection,
|
|
||||||
name: String,
|
|
||||||
query_string: String,
|
|
||||||
) -> Result<Timeline> {
|
|
||||||
{
|
|
||||||
let query = TimelineQuery::parse(&query_string)?; // verify the query is valid
|
|
||||||
if let Some(err) =
|
|
||||||
query
|
|
||||||
.list_used_lists()
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|(name, kind)| {
|
|
||||||
let list = List::find_for_user_by_name(conn, None, &name)
|
|
||||||
.map(|l| l.kind() == kind);
|
|
||||||
match list {
|
|
||||||
Ok(true) => None,
|
|
||||||
Ok(false) => Some(Error::TimelineQuery(QueryError::RuntimeError(
|
|
||||||
format!("list '{}' has the wrong type for this usage", name),
|
|
||||||
))),
|
|
||||||
Err(_) => Some(Error::TimelineQuery(QueryError::RuntimeError(
|
|
||||||
format!("list '{}' was not found", name),
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
{
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::insert(
|
|
||||||
conn,
|
|
||||||
NewTimeline {
|
|
||||||
user_id: None,
|
|
||||||
name,
|
|
||||||
query: query_string,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&self, conn: &Connection) -> Result<Self> {
|
|
||||||
diesel::update(self).set(self).execute(conn)?;
|
|
||||||
let timeline = Self::get(conn, self.id)?;
|
|
||||||
Ok(timeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection) -> Result<()> {
|
|
||||||
diesel::delete(self)
|
|
||||||
.execute(conn)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_latest(&self, conn: &Connection, count: i32) -> Result<Vec<Post>> {
|
|
||||||
self.get_page(conn, (0, count))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Post>> {
|
|
||||||
timeline::table
|
|
||||||
.filter(timeline::timeline_id.eq(self.id))
|
|
||||||
.inner_join(posts::table)
|
|
||||||
.order(posts::creation_date.desc())
|
|
||||||
.offset(min.into())
|
|
||||||
.limit((max - min).into())
|
|
||||||
.select(posts::all_columns)
|
|
||||||
.load::<Post>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count_posts(&self, conn: &Connection) -> Result<i64> {
|
|
||||||
timeline::table
|
|
||||||
.filter(timeline::timeline_id.eq(self.id))
|
|
||||||
.inner_join(posts::table)
|
|
||||||
.count()
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> {
|
|
||||||
let timelines = timeline_definition::table
|
|
||||||
.load::<Self>(rocket.conn.deref())
|
|
||||||
.map_err(Error::from)?;
|
|
||||||
|
|
||||||
for t in timelines {
|
|
||||||
if t.matches(rocket, post, kind)? {
|
|
||||||
t.add_post(&rocket.conn, post)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_post(&self, conn: &Connection, post: &Post) -> Result<()> {
|
|
||||||
diesel::insert_into(timeline::table)
|
|
||||||
.values(TimelineEntry {
|
|
||||||
post_id: post.id,
|
|
||||||
timeline_id: self.id,
|
|
||||||
})
|
|
||||||
.execute(conn)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> {
|
|
||||||
let query = TimelineQuery::parse(&self.query)?;
|
|
||||||
query.matches(rocket, self, post, kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{
|
|
||||||
blogs::tests as blogTests,
|
|
||||||
follows::*,
|
|
||||||
lists::ListType,
|
|
||||||
post_authors::{NewPostAuthor, PostAuthor},
|
|
||||||
posts::NewPost,
|
|
||||||
safe_string::SafeString,
|
|
||||||
tags::Tag,
|
|
||||||
tests::{db, rockets},
|
|
||||||
users::tests as userTests,
|
|
||||||
};
|
|
||||||
use diesel::Connection;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_timeline() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let users = userTests::fill_database(conn);
|
|
||||||
|
|
||||||
let mut tl1_u1 = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"my timeline".to_owned(),
|
|
||||||
"all".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
List::new(conn, "languages I speak", Some(&users[1]), ListType::Prefix).unwrap();
|
|
||||||
let tl2_u1 = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"another timeline".to_owned(),
|
|
||||||
"followed".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let tl1_u2 = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[1].id,
|
|
||||||
"english posts".to_owned(),
|
|
||||||
"lang in \"languages I speak\"".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let tl1_instance = Timeline::new_for_instance(
|
|
||||||
conn,
|
|
||||||
"english posts".to_owned(),
|
|
||||||
"license in [cc]".to_owned(),
|
|
||||||
)
|
|
||||||
.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")
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tl1_instance,
|
|
||||||
Timeline::find_for_user_by_name(conn, None, "english posts").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().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();
|
|
||||||
assert_eq!(3, tl_instance.len()); // there are also the local and federated feed by default
|
|
||||||
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 tl_u2 = Timeline::list_for_user(conn, Some(users[1].id)).unwrap();
|
|
||||||
assert_eq!(2, tl_u2.len()); // same here
|
|
||||||
assert!(tl_u2
|
|
||||||
.iter()
|
|
||||||
.fold(false, |res, tl| { res || *tl == new_tl1_u2 }));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_timeline_creation_error() {
|
|
||||||
let conn = &db();
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let users = userTests::fill_database(conn);
|
|
||||||
|
|
||||||
assert!(Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"my timeline".to_owned(),
|
|
||||||
"invalid keyword".to_owned(),
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
assert!(Timeline::new_for_instance(
|
|
||||||
conn,
|
|
||||||
"my timeline".to_owned(),
|
|
||||||
"invalid keyword".to_owned(),
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
assert!(Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"my timeline".to_owned(),
|
|
||||||
"author in non_existant_list".to_owned(),
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
assert!(Timeline::new_for_instance(
|
|
||||||
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();
|
|
||||||
|
|
||||||
assert!(Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"my timeline".to_owned(),
|
|
||||||
"blog in friends".to_owned(),
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
assert!(Timeline::new_for_instance(
|
|
||||||
conn,
|
|
||||||
"my timeline".to_owned(),
|
|
||||||
"not author in idk".to_owned(),
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_match() {
|
|
||||||
let r = &rockets();
|
|
||||||
let conn = &r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blogTests::fill_database(conn);
|
|
||||||
|
|
||||||
let gnu_tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"GNU timeline".to_owned(),
|
|
||||||
"license in [AGPL, LGPL, GPL]".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "slug".to_string(),
|
|
||||||
title: "About Linux".to_string(),
|
|
||||||
content: SafeString::new("you must say GNU/Linux, not Linux!!!"),
|
|
||||||
published: true,
|
|
||||||
license: "GPL".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(gnu_tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
|
|
||||||
let non_free_post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "slug2".to_string(),
|
|
||||||
title: "Private is bad".to_string(),
|
|
||||||
content: SafeString::new("so is Microsoft"),
|
|
||||||
published: true,
|
|
||||||
license: "all right reserved".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
source: "so is Microsoft".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!gnu_tl.matches(r, &non_free_post, Kind::Original).unwrap());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_complex_match() {
|
|
||||||
let r = &rockets();
|
|
||||||
let conn = &r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blogTests::fill_database(conn);
|
|
||||||
Follow::insert(
|
|
||||||
conn,
|
|
||||||
NewFollow {
|
|
||||||
follower_id: users[0].id,
|
|
||||||
following_id: users[1].id,
|
|
||||||
ap_url: String::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.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();
|
|
||||||
|
|
||||||
let my_tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"My timeline".to_owned(),
|
|
||||||
"blog in fav_blogs and not has_cover or local and followed exclude likes"
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "about-linux".to_string(),
|
|
||||||
title: "About Linux".to_string(),
|
|
||||||
content: SafeString::new("you must say GNU/Linux, not Linux!!!"),
|
|
||||||
published: true,
|
|
||||||
license: "GPL".to_string(),
|
|
||||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(my_tl.matches(r, &post, Kind::Original).unwrap()); // matches because of "blog in fav_blogs" (and there is no cover)
|
|
||||||
|
|
||||||
let post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[1].id,
|
|
||||||
slug: "about-linux-2".to_string(),
|
|
||||||
title: "About Linux (2)".to_string(),
|
|
||||||
content: SafeString::new(
|
|
||||||
"Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better.",
|
|
||||||
),
|
|
||||||
published: true,
|
|
||||||
license: "GPL".to_string(),
|
|
||||||
source: "Actually, GNU+Linux, GNU×Linux, or GNU¿Linux are better.".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!my_tl.matches(r, &post, Kind::Like(&users[1])).unwrap());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_add_to_all_timelines() {
|
|
||||||
let r = &rockets();
|
|
||||||
let conn = &r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blogTests::fill_database(conn);
|
|
||||||
|
|
||||||
let gnu_tl = Timeline::new_for_user(
|
|
||||||
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,
|
|
||||||
users[0].id,
|
|
||||||
"Stallman disapproved timeline".to_owned(),
|
|
||||||
"not license in [AGPL, LGPL, GPL]".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "slug".to_string(),
|
|
||||||
title: "About Linux".to_string(),
|
|
||||||
content: SafeString::new("you must say GNU/Linux, not Linux!!!"),
|
|
||||||
published: true,
|
|
||||||
license: "GPL".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let non_free_post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "slug2".to_string(),
|
|
||||||
title: "Private is bad".to_string(),
|
|
||||||
content: SafeString::new("so is Microsoft"),
|
|
||||||
published: true,
|
|
||||||
license: "all right reserved".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
source: "so is Microsoft".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Timeline::add_to_all_timelines(r, &gnu_post, Kind::Original).unwrap();
|
|
||||||
Timeline::add_to_all_timelines(r, &non_free_post, Kind::Original).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();
|
|
||||||
assert_eq!(res.len(), 1);
|
|
||||||
assert_eq!(res[0].id, non_free_post.id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_matches_lists_direct() {
|
|
||||||
let r = &rockets();
|
|
||||||
let conn = &r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blogTests::fill_database(conn);
|
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "slug".to_string(),
|
|
||||||
title: "About Linux".to_string(),
|
|
||||||
content: SafeString::new("you must say GNU/Linux, not Linux!!!"),
|
|
||||||
published: true,
|
|
||||||
license: "GPL".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
gnu_post
|
|
||||||
.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()])
|
|
||||||
.unwrap();
|
|
||||||
PostAuthor::insert(
|
|
||||||
conn,
|
|
||||||
NewPostAuthor {
|
|
||||||
post_id: gnu_post.id,
|
|
||||||
author_id: blogs[0].list_authors(conn).unwrap()[0].id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"blog timeline".to_owned(),
|
|
||||||
format!("blog in [{}]", blogs[0].fqn),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"blog timeline".to_owned(),
|
|
||||||
"blog in [no_one@nowhere]".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"author timeline".to_owned(),
|
|
||||||
format!(
|
|
||||||
"author in [{}]",
|
|
||||||
blogs[0].list_authors(conn).unwrap()[0].fqn
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"author timeline".to_owned(),
|
|
||||||
format!("author in [{}]", users[2].fqn),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"author timeline".to_owned(),
|
|
||||||
format!(
|
|
||||||
"author in [{}] include likes exclude reshares",
|
|
||||||
users[2].fqn
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Reshare(&users[2])).unwrap());
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Like(&users[2])).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"tag timeline".to_owned(),
|
|
||||||
"tags in [free]".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"tag timeline".to_owned(),
|
|
||||||
"tags in [private]".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"english timeline".to_owned(),
|
|
||||||
"lang in [en]".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"franco-italian timeline".to_owned(),
|
|
||||||
"lang in [fr, it]".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[test]
|
|
||||||
fn test_matches_lists_saved() {
|
|
||||||
let r = &rockets();
|
|
||||||
let conn = &r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blogTests::fill_database(conn);
|
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "slug".to_string(),
|
|
||||||
title: "About Linux".to_string(),
|
|
||||||
content: SafeString::new("you must say GNU/Linux, not Linux!!!"),
|
|
||||||
published: true,
|
|
||||||
license: "GPL".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "".to_string(),
|
|
||||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
gnu_post.update_tags(conn, vec![Tag::build_activity("free".to_owned()).unwrap()]).unwrap();
|
|
||||||
PostAuthor::insert(conn, NewPostAuthor {post_id: gnu_post.id, author_id: blogs[0].list_authors(conn).unwrap()[0].id}).unwrap();
|
|
||||||
|
|
||||||
unimplemented!();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_matches_keyword() {
|
|
||||||
let r = &rockets();
|
|
||||||
let conn = &r.conn;
|
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
|
||||||
let (users, blogs) = blogTests::fill_database(conn);
|
|
||||||
|
|
||||||
let gnu_post = Post::insert(
|
|
||||||
conn,
|
|
||||||
NewPost {
|
|
||||||
blog_id: blogs[0].id,
|
|
||||||
slug: "slug".to_string(),
|
|
||||||
title: "About Linux".to_string(),
|
|
||||||
content: SafeString::new("you must say GNU/Linux, not Linux!!!"),
|
|
||||||
published: true,
|
|
||||||
license: "GPL".to_string(),
|
|
||||||
ap_url: "".to_string(),
|
|
||||||
creation_date: None,
|
|
||||||
subtitle: "Stallman is our god".to_string(),
|
|
||||||
source: "you must say GNU/Linux, not Linux!!!".to_string(),
|
|
||||||
cover_id: None,
|
|
||||||
},
|
|
||||||
&r.searcher,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"Linux title".to_owned(),
|
|
||||||
"title contains Linux".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"Microsoft title".to_owned(),
|
|
||||||
"title contains Microsoft".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"Linux subtitle".to_owned(),
|
|
||||||
"subtitle contains Stallman".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"Microsoft subtitle".to_owned(),
|
|
||||||
"subtitle contains Nadella".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"Linux content".to_owned(),
|
|
||||||
"content contains Linux".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
let tl = Timeline::new_for_user(
|
|
||||||
conn,
|
|
||||||
users[0].id,
|
|
||||||
"Microsoft content".to_owned(),
|
|
||||||
"subtitle contains Windows".to_owned(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!tl.matches(r, &gnu_post, Kind::Original).unwrap());
|
|
||||||
tl.delete(conn).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,886 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
blogs::Blog,
|
|
||||||
lists::{self, ListType},
|
|
||||||
posts::Post,
|
|
||||||
tags::Tag,
|
|
||||||
timeline::Timeline,
|
|
||||||
users::User,
|
|
||||||
PlumeRocket, Result,
|
|
||||||
};
|
|
||||||
use plume_common::activity_pub::inbox::AsActor;
|
|
||||||
use whatlang::{self, Lang};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum QueryError {
|
|
||||||
SyntaxError(usize, usize, String),
|
|
||||||
UnexpectedEndOfQuery,
|
|
||||||
RuntimeError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::option::NoneError> for QueryError {
|
|
||||||
fn from(_: std::option::NoneError) -> Self {
|
|
||||||
QueryError::UnexpectedEndOfQuery
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type QueryResult<T> = std::result::Result<T, QueryError>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum Kind<'a> {
|
|
||||||
Original,
|
|
||||||
Reshare(&'a User),
|
|
||||||
Like(&'a User),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
enum Token<'a> {
|
|
||||||
LParent(usize),
|
|
||||||
RParent(usize),
|
|
||||||
LBracket(usize),
|
|
||||||
RBracket(usize),
|
|
||||||
Comma(usize),
|
|
||||||
Word(usize, usize, &'a str),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Token<'a> {
|
|
||||||
fn get_text(&self) -> &'a str {
|
|
||||||
match self {
|
|
||||||
Token::Word(_, _, s) => s,
|
|
||||||
Token::LParent(_) => "(",
|
|
||||||
Token::RParent(_) => ")",
|
|
||||||
Token::LBracket(_) => "[",
|
|
||||||
Token::RBracket(_) => "]",
|
|
||||||
Token::Comma(_) => ",",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_pos(&self) -> (usize, usize) {
|
|
||||||
match self {
|
|
||||||
Token::Word(a, b, _) => (*a, *b),
|
|
||||||
Token::LParent(a)
|
|
||||||
| Token::RParent(a)
|
|
||||||
| Token::LBracket(a)
|
|
||||||
| Token::RBracket(a)
|
|
||||||
| Token::Comma(a) => (*a, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_error<T>(&self, token: Token<'_>) -> QueryResult<T> {
|
|
||||||
let (b, e) = self.get_pos();
|
|
||||||
let message = format!(
|
|
||||||
"Syntax Error: Expected {}, got {}",
|
|
||||||
token.to_string(),
|
|
||||||
self.to_string()
|
|
||||||
);
|
|
||||||
Err(QueryError::SyntaxError(b, e, message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ToString for Token<'a> {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
if let Token::Word(0, 0, v) = self {
|
|
||||||
return (*v).to_string();
|
|
||||||
}
|
|
||||||
format!(
|
|
||||||
"'{}'",
|
|
||||||
match self {
|
|
||||||
Token::Word(_, _, v) => v,
|
|
||||||
Token::LParent(_) => "(",
|
|
||||||
Token::RParent(_) => ")",
|
|
||||||
Token::LBracket(_) => "[",
|
|
||||||
Token::RBracket(_) => "]",
|
|
||||||
Token::Comma(_) => ",",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! gen_tokenizer {
|
|
||||||
( ($c:ident,$i:ident), $state:ident, $quote:ident; $([$char:tt, $variant:tt]),*) => {
|
|
||||||
match $c {
|
|
||||||
space if !*$quote && space.is_whitespace() => match $state.take() {
|
|
||||||
Some(v) => vec![v],
|
|
||||||
None => vec![],
|
|
||||||
},
|
|
||||||
$(
|
|
||||||
$char if !*$quote => match $state.take() {
|
|
||||||
Some(v) => vec![v, Token::$variant($i)],
|
|
||||||
None => vec![Token::$variant($i)],
|
|
||||||
},
|
|
||||||
)*
|
|
||||||
'"' => {
|
|
||||||
*$quote = !*$quote;
|
|
||||||
vec![]
|
|
||||||
},
|
|
||||||
_ => match $state.take() {
|
|
||||||
Some(Token::Word(b, l, _)) => {
|
|
||||||
*$state = Some(Token::Word(b, l+1, &""));
|
|
||||||
vec![]
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
*$state = Some(Token::Word($i,1,&""));
|
|
||||||
vec![]
|
|
||||||
},
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lex(stream: &str) -> Vec<Token<'_>> {
|
|
||||||
stream
|
|
||||||
.chars()
|
|
||||||
.chain(" ".chars()) // force a last whitespace to empty scan's state
|
|
||||||
.zip(0..)
|
|
||||||
.scan((None, false), |(state, quote), (c, i)| {
|
|
||||||
Some(gen_tokenizer!((c,i), state, quote;
|
|
||||||
['(', LParent], [')', RParent],
|
|
||||||
['[', LBracket], [']', RBracket],
|
|
||||||
[',', Comma]))
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.map(|t| {
|
|
||||||
if let Token::Word(b, e, _) = t {
|
|
||||||
Token::Word(b, e, &stream[b..b + e])
|
|
||||||
} else {
|
|
||||||
t
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Private internals of TimelineQuery
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum TQ<'a> {
|
|
||||||
Or(Vec<TQ<'a>>),
|
|
||||||
And(Vec<TQ<'a>>),
|
|
||||||
Arg(Arg<'a>, bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TQ<'a> {
|
|
||||||
fn matches(
|
|
||||||
&self,
|
|
||||||
rocket: &PlumeRocket,
|
|
||||||
timeline: &Timeline,
|
|
||||||
post: &Post,
|
|
||||||
kind: Kind<'_>,
|
|
||||||
) -> Result<bool> {
|
|
||||||
match self {
|
|
||||||
TQ::Or(inner) => inner.iter().try_fold(false, |s, e| {
|
|
||||||
e.matches(rocket, timeline, post, kind).map(|r| s || r)
|
|
||||||
}),
|
|
||||||
TQ::And(inner) => inner.iter().try_fold(true, |s, e| {
|
|
||||||
e.matches(rocket, timeline, post, kind).map(|r| s && r)
|
|
||||||
}),
|
|
||||||
TQ::Arg(inner, invert) => Ok(inner.matches(rocket, timeline, post, kind)? ^ invert),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_used_lists(&self) -> Vec<(String, ListType)> {
|
|
||||||
match self {
|
|
||||||
TQ::Or(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(),
|
|
||||||
TQ::And(inner) => inner.iter().flat_map(TQ::list_used_lists).collect(),
|
|
||||||
TQ::Arg(Arg::In(typ, List::List(name)), _) => vec![(
|
|
||||||
(*name).to_string(),
|
|
||||||
match typ {
|
|
||||||
WithList::Blog => ListType::Blog,
|
|
||||||
WithList::Author { .. } => ListType::User,
|
|
||||||
WithList::License => ListType::Word,
|
|
||||||
WithList::Tags => ListType::Word,
|
|
||||||
WithList::Lang => ListType::Prefix,
|
|
||||||
},
|
|
||||||
)],
|
|
||||||
TQ::Arg(_, _) => vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum Arg<'a> {
|
|
||||||
In(WithList, List<'a>),
|
|
||||||
Contains(WithContains, &'a str),
|
|
||||||
Boolean(Bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Arg<'a> {
|
|
||||||
pub fn matches(
|
|
||||||
&self,
|
|
||||||
rocket: &PlumeRocket,
|
|
||||||
timeline: &Timeline,
|
|
||||||
post: &Post,
|
|
||||||
kind: Kind<'_>,
|
|
||||||
) -> Result<bool> {
|
|
||||||
match self {
|
|
||||||
Arg::In(t, l) => t.matches(rocket, timeline, post, l, kind),
|
|
||||||
Arg::Contains(t, v) => t.matches(post, v),
|
|
||||||
Arg::Boolean(t) => t.matches(rocket, timeline, post, kind),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum WithList {
|
|
||||||
Blog,
|
|
||||||
Author { boosts: bool, likes: bool },
|
|
||||||
License,
|
|
||||||
Tags,
|
|
||||||
Lang,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WithList {
|
|
||||||
pub fn matches(
|
|
||||||
&self,
|
|
||||||
rocket: &PlumeRocket,
|
|
||||||
timeline: &Timeline,
|
|
||||||
post: &Post,
|
|
||||||
list: &List<'_>,
|
|
||||||
kind: Kind<'_>,
|
|
||||||
) -> Result<bool> {
|
|
||||||
match list {
|
|
||||||
List::List(name) => {
|
|
||||||
let list =
|
|
||||||
lists::List::find_for_user_by_name(&rocket.conn, timeline.user_id, &name)?;
|
|
||||||
match (self, list.kind()) {
|
|
||||||
(WithList::Blog, ListType::Blog) => {
|
|
||||||
list.contains_blog(&rocket.conn, post.blog_id)
|
|
||||||
}
|
|
||||||
(WithList::Author { boosts, likes }, ListType::User) => match kind {
|
|
||||||
Kind::Original => Ok(list
|
|
||||||
.list_users(&rocket.conn)?
|
|
||||||
.iter()
|
|
||||||
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
|
|
||||||
Kind::Reshare(u) => {
|
|
||||||
if *boosts {
|
|
||||||
list.contains_user(&rocket.conn, u.id)
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::Like(u) => {
|
|
||||||
if *likes {
|
|
||||||
list.contains_user(&rocket.conn, u.id)
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(WithList::License, ListType::Word) => {
|
|
||||||
list.contains_word(&rocket.conn, &post.license)
|
|
||||||
}
|
|
||||||
(WithList::Tags, ListType::Word) => {
|
|
||||||
let tags = Tag::for_post(&rocket.conn, post.id)?;
|
|
||||||
Ok(list
|
|
||||||
.list_words(&rocket.conn)?
|
|
||||||
.iter()
|
|
||||||
.any(|s| tags.iter().any(|t| s == &t.tag)))
|
|
||||||
}
|
|
||||||
(WithList::Lang, ListType::Prefix) => {
|
|
||||||
let lang = whatlang::detect(post.content.get())
|
|
||||||
.and_then(|i| {
|
|
||||||
if i.is_reliable() {
|
|
||||||
Some(i.lang())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(Lang::Eng)
|
|
||||||
.name();
|
|
||||||
list.contains_prefix(&rocket.conn, lang)
|
|
||||||
}
|
|
||||||
(_, _) => Err(QueryError::RuntimeError(format!(
|
|
||||||
"The list '{}' is of the wrong type for this usage",
|
|
||||||
name
|
|
||||||
))
|
|
||||||
.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List::Array(list) => match self {
|
|
||||||
WithList::Blog => Ok(list
|
|
||||||
.iter()
|
|
||||||
.filter_map(|b| Blog::find_by_fqn(rocket, b).ok())
|
|
||||||
.any(|b| b.id == post.blog_id)),
|
|
||||||
WithList::Author { boosts, likes } => match kind {
|
|
||||||
Kind::Original => Ok(list
|
|
||||||
.iter()
|
|
||||||
.filter_map(|a| User::find_by_fqn(rocket, a).ok())
|
|
||||||
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
|
|
||||||
Kind::Reshare(u) => {
|
|
||||||
if *boosts {
|
|
||||||
Ok(list.iter().any(|user| &u.fqn == user))
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::Like(u) => {
|
|
||||||
if *likes {
|
|
||||||
Ok(list.iter().any(|user| &u.fqn == user))
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
WithList::License => Ok(list.iter().any(|s| s == &post.license)),
|
|
||||||
WithList::Tags => {
|
|
||||||
let tags = Tag::for_post(&rocket.conn, post.id)?;
|
|
||||||
Ok(list.iter().any(|s| tags.iter().any(|t| s == &t.tag)))
|
|
||||||
}
|
|
||||||
WithList::Lang => {
|
|
||||||
let lang = whatlang::detect(post.content.get())
|
|
||||||
.and_then(|i| {
|
|
||||||
if i.is_reliable() {
|
|
||||||
Some(i.lang())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(Lang::Eng)
|
|
||||||
.name()
|
|
||||||
.to_lowercase();
|
|
||||||
Ok(list.iter().any(|s| lang.starts_with(&s.to_lowercase())))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum WithContains {
|
|
||||||
Title,
|
|
||||||
Subtitle,
|
|
||||||
Content,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WithContains {
|
|
||||||
pub fn matches(&self, post: &Post, value: &str) -> Result<bool> {
|
|
||||||
match self {
|
|
||||||
WithContains::Title => Ok(post.title.contains(value)),
|
|
||||||
WithContains::Subtitle => Ok(post.subtitle.contains(value)),
|
|
||||||
WithContains::Content => Ok(post.content.contains(value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum Bool {
|
|
||||||
Followed { boosts: bool, likes: bool },
|
|
||||||
HasCover,
|
|
||||||
Local,
|
|
||||||
All,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bool {
|
|
||||||
pub fn matches(
|
|
||||||
&self,
|
|
||||||
rocket: &PlumeRocket,
|
|
||||||
timeline: &Timeline,
|
|
||||||
post: &Post,
|
|
||||||
kind: Kind<'_>,
|
|
||||||
) -> Result<bool> {
|
|
||||||
match self {
|
|
||||||
Bool::Followed { boosts, likes } => {
|
|
||||||
if timeline.user_id.is_none() {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
let user = timeline.user_id.unwrap();
|
|
||||||
match kind {
|
|
||||||
Kind::Original => post
|
|
||||||
.get_authors(&rocket.conn)?
|
|
||||||
.iter()
|
|
||||||
.try_fold(false, |s, a| {
|
|
||||||
a.is_followed_by(&rocket.conn, user).map(|r| s || r)
|
|
||||||
}),
|
|
||||||
Kind::Reshare(u) => {
|
|
||||||
if *boosts {
|
|
||||||
u.is_followed_by(&rocket.conn, user)
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::Like(u) => {
|
|
||||||
if *likes {
|
|
||||||
u.is_followed_by(&rocket.conn, user)
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Bool::HasCover => Ok(post.cover_id.is_some()),
|
|
||||||
Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local()),
|
|
||||||
Bool::All => Ok(true),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum List<'a> {
|
|
||||||
List(&'a str),
|
|
||||||
Array(Vec<&'a str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_s<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
let (left, token) = parse_a(&stream)?;
|
|
||||||
res.push(token);
|
|
||||||
stream = left;
|
|
||||||
while !stream.is_empty() {
|
|
||||||
match stream[0] {
|
|
||||||
Token::Word(_, _, and) if and == "or" => {}
|
|
||||||
_ => break,
|
|
||||||
}
|
|
||||||
let (left, token) = parse_a(&stream[1..])?;
|
|
||||||
res.push(token);
|
|
||||||
stream = left;
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.len() == 1 {
|
|
||||||
Ok((stream, res.remove(0)))
|
|
||||||
} else {
|
|
||||||
Ok((stream, TQ::Or(res)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_a<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
let (left, token) = parse_b(&stream)?;
|
|
||||||
res.push(token);
|
|
||||||
stream = left;
|
|
||||||
while !stream.is_empty() {
|
|
||||||
match stream[0] {
|
|
||||||
Token::Word(_, _, and) if and == "and" => {}
|
|
||||||
_ => break,
|
|
||||||
}
|
|
||||||
let (left, token) = parse_b(&stream[1..])?;
|
|
||||||
res.push(token);
|
|
||||||
stream = left;
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.len() == 1 {
|
|
||||||
Ok((stream, res.remove(0)))
|
|
||||||
} else {
|
|
||||||
Ok((stream, TQ::And(res)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_b<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
|
|
||||||
match stream.get(0) {
|
|
||||||
Some(Token::LParent(_)) => {
|
|
||||||
let (left, token) = parse_s(&stream[1..])?;
|
|
||||||
match left.get(0) {
|
|
||||||
Some(Token::RParent(_)) => Ok((&left[1..], token)),
|
|
||||||
Some(t) => t.get_error(Token::RParent(0)),
|
|
||||||
None => None?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => parse_c(stream),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_c<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], TQ<'a>)> {
|
|
||||||
match stream.get(0) {
|
|
||||||
Some(Token::Word(_, _, not)) if not == &"not" => {
|
|
||||||
let (left, token) = parse_d(&stream[1..])?;
|
|
||||||
Ok((left, TQ::Arg(token, true)))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let (left, token) = parse_d(stream)?;
|
|
||||||
Ok((left, TQ::Arg(token, false)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_d<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Arg<'a>)> {
|
|
||||||
match stream.get(0).map(Token::get_text)? {
|
|
||||||
s @ "blog" | s @ "author" | s @ "license" | s @ "tags" | s @ "lang" => {
|
|
||||||
match stream.get(1)? {
|
|
||||||
Token::Word(_, _, r#in) if r#in == &"in" => {
|
|
||||||
let (mut left, list) = parse_l(&stream[2..])?;
|
|
||||||
let kind = match s {
|
|
||||||
"blog" => WithList::Blog,
|
|
||||||
"author" => {
|
|
||||||
let mut boosts = true;
|
|
||||||
let mut likes = false;
|
|
||||||
while let Some(Token::Word(s, e, clude)) = left.get(0) {
|
|
||||||
if *clude != "include" && *clude != "exclude" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match (*clude, left.get(1).map(Token::get_text)?) {
|
|
||||||
("include", "reshares") | ("include", "reshare") => {
|
|
||||||
boosts = true
|
|
||||||
}
|
|
||||||
("exclude", "reshares") | ("exclude", "reshare") => {
|
|
||||||
boosts = false
|
|
||||||
}
|
|
||||||
("include", "likes") | ("include", "like") => likes = true,
|
|
||||||
("exclude", "likes") | ("exclude", "like") => likes = false,
|
|
||||||
(_, w) => {
|
|
||||||
return Token::Word(*s, *e, w).get_error(Token::Word(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"one of 'likes' or 'reshares'",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
left = &left[2..];
|
|
||||||
}
|
|
||||||
WithList::Author { boosts, likes }
|
|
||||||
}
|
|
||||||
"license" => WithList::License,
|
|
||||||
"tags" => WithList::Tags,
|
|
||||||
"lang" => WithList::Lang,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
Ok((left, Arg::In(kind, list)))
|
|
||||||
}
|
|
||||||
t => t.get_error(Token::Word(0, 0, "'in'")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s @ "title" | s @ "subtitle" | s @ "content" => match (stream.get(1)?, stream.get(2)?) {
|
|
||||||
(Token::Word(_, _, contains), Token::Word(_, _, w)) if contains == &"contains" => Ok((
|
|
||||||
&stream[3..],
|
|
||||||
Arg::Contains(
|
|
||||||
match s {
|
|
||||||
"title" => WithContains::Title,
|
|
||||||
"subtitle" => WithContains::Subtitle,
|
|
||||||
"content" => WithContains::Content,
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
w,
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
(Token::Word(_, _, contains), t) if contains == &"contains" => {
|
|
||||||
t.get_error(Token::Word(0, 0, "any word"))
|
|
||||||
}
|
|
||||||
(t, _) => t.get_error(Token::Word(0, 0, "'contains'")),
|
|
||||||
},
|
|
||||||
s @ "followed" | s @ "has_cover" | s @ "local" | s @ "all" => match s {
|
|
||||||
"followed" => {
|
|
||||||
let mut boosts = true;
|
|
||||||
let mut likes = false;
|
|
||||||
while let Some(Token::Word(s, e, clude)) = stream.get(1) {
|
|
||||||
if *clude != "include" && *clude != "exclude" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match (*clude, stream.get(2).map(Token::get_text)?) {
|
|
||||||
("include", "reshares") | ("include", "reshare") => boosts = true,
|
|
||||||
("exclude", "reshares") | ("exclude", "reshare") => boosts = false,
|
|
||||||
("include", "likes") | ("include", "like") => likes = true,
|
|
||||||
("exclude", "likes") | ("exclude", "like") => likes = false,
|
|
||||||
(_, w) => {
|
|
||||||
return Token::Word(*s, *e, w).get_error(Token::Word(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"one of 'likes' or 'boosts'",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stream = &stream[2..];
|
|
||||||
}
|
|
||||||
Ok((&stream[1..], Arg::Boolean(Bool::Followed { boosts, likes })))
|
|
||||||
}
|
|
||||||
"has_cover" => Ok((&stream[1..], Arg::Boolean(Bool::HasCover))),
|
|
||||||
"local" => Ok((&stream[1..], Arg::Boolean(Bool::Local))),
|
|
||||||
"all" => Ok((&stream[1..], Arg::Boolean(Bool::All))),
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
_ => stream.get(0)?.get_error(Token::Word(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"one of 'blog', 'author', 'license', 'tags', 'lang', \
|
|
||||||
'title', 'subtitle', 'content', 'followed', 'has_cover', 'local' or 'all'",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_l<'a, 'b>(stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], List<'a>)> {
|
|
||||||
match stream.get(0)? {
|
|
||||||
Token::LBracket(_) => {
|
|
||||||
let (left, list) = parse_m(&stream[1..])?;
|
|
||||||
match left.get(0)? {
|
|
||||||
Token::RBracket(_) => Ok((&left[1..], List::Array(list))),
|
|
||||||
t => t.get_error(Token::Word(0, 0, "one of ']' or ','")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Token::Word(_, _, list) => Ok((&stream[1..], List::List(list))),
|
|
||||||
t => t.get_error(Token::Word(0, 0, "one of [list, of, words] or list_name")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_m<'a, 'b>(mut stream: &'b [Token<'a>]) -> QueryResult<(&'b [Token<'a>], Vec<&'a str>)> {
|
|
||||||
let mut res: Vec<&str> = Vec::new();
|
|
||||||
res.push(match stream.get(0)? {
|
|
||||||
Token::Word(_, _, w) => w,
|
|
||||||
t => return t.get_error(Token::Word(0, 0, "any word")),
|
|
||||||
});
|
|
||||||
stream = &stream[1..];
|
|
||||||
while let Token::Comma(_) = stream[0] {
|
|
||||||
res.push(match stream.get(1)? {
|
|
||||||
Token::Word(_, _, w) => w,
|
|
||||||
t => return t.get_error(Token::Word(0, 0, "any word")),
|
|
||||||
});
|
|
||||||
stream = &stream[2..];
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((stream, res))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TimelineQuery<'a>(TQ<'a>);
|
|
||||||
|
|
||||||
impl<'a> TimelineQuery<'a> {
|
|
||||||
pub fn parse(query: &'a str) -> QueryResult<Self> {
|
|
||||||
parse_s(&lex(query))
|
|
||||||
.and_then(|(left, res)| {
|
|
||||||
if left.is_empty() {
|
|
||||||
Ok(res)
|
|
||||||
} else {
|
|
||||||
left[0].get_error(Token::Word(0, 0, "on of 'or' or 'and'"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(TimelineQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(
|
|
||||||
&self,
|
|
||||||
rocket: &PlumeRocket,
|
|
||||||
timeline: &Timeline,
|
|
||||||
post: &Post,
|
|
||||||
kind: Kind<'_>,
|
|
||||||
) -> Result<bool> {
|
|
||||||
self.0.matches(rocket, timeline, post, kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_used_lists(&self) -> Vec<(String, ListType)> {
|
|
||||||
self.0.list_used_lists()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lexer() {
|
|
||||||
assert_eq!(
|
|
||||||
lex("()[ ],two words \"something quoted with , and [\""),
|
|
||||||
vec![
|
|
||||||
Token::LParent(0),
|
|
||||||
Token::RParent(1),
|
|
||||||
Token::LBracket(2),
|
|
||||||
Token::RBracket(4),
|
|
||||||
Token::Comma(5),
|
|
||||||
Token::Word(6, 3, "two"),
|
|
||||||
Token::Word(10, 5, "words"),
|
|
||||||
Token::Word(17, 29, "something quoted with , and ["),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parser() {
|
|
||||||
let q = TimelineQuery::parse(r#"lang in [fr, en] and (license in my_fav_lic or not followed) or title contains "Plume is amazing""#)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
q.0,
|
|
||||||
TQ::Or(vec![
|
|
||||||
TQ::And(vec![
|
|
||||||
TQ::Arg(
|
|
||||||
Arg::In(WithList::Lang, List::Array(vec!["fr", "en"]),),
|
|
||||||
false
|
|
||||||
),
|
|
||||||
TQ::Or(vec![
|
|
||||||
TQ::Arg(Arg::In(WithList::License, List::List("my_fav_lic"),), false),
|
|
||||||
TQ::Arg(
|
|
||||||
Arg::Boolean(Bool::Followed {
|
|
||||||
boosts: true,
|
|
||||||
likes: false
|
|
||||||
}),
|
|
||||||
true
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
TQ::Arg(
|
|
||||||
Arg::Contains(WithContains::Title, "Plume is amazing",),
|
|
||||||
false
|
|
||||||
),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
let lists = TimelineQuery::parse(
|
|
||||||
r#"blog in a or author in b include likes or license in c or tags in d or lang in e "#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
lists.0,
|
|
||||||
TQ::Or(vec![
|
|
||||||
TQ::Arg(Arg::In(WithList::Blog, List::List("a"),), false),
|
|
||||||
TQ::Arg(
|
|
||||||
Arg::In(
|
|
||||||
WithList::Author {
|
|
||||||
boosts: true,
|
|
||||||
likes: true
|
|
||||||
},
|
|
||||||
List::List("b"),
|
|
||||||
),
|
|
||||||
false
|
|
||||||
),
|
|
||||||
TQ::Arg(Arg::In(WithList::License, List::List("c"),), false),
|
|
||||||
TQ::Arg(Arg::In(WithList::Tags, List::List("d"),), false),
|
|
||||||
TQ::Arg(Arg::In(WithList::Lang, List::List("e"),), false),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
let contains = TimelineQuery::parse(
|
|
||||||
r#"title contains a or subtitle contains b or content contains c"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
contains.0,
|
|
||||||
TQ::Or(vec![
|
|
||||||
TQ::Arg(Arg::Contains(WithContains::Title, "a"), false),
|
|
||||||
TQ::Arg(Arg::Contains(WithContains::Subtitle, "b"), false),
|
|
||||||
TQ::Arg(Arg::Contains(WithContains::Content, "c"), false),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
let booleans = TimelineQuery::parse(
|
|
||||||
r#"followed include like exclude reshares and has_cover and local and all"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
booleans.0,
|
|
||||||
TQ::And(vec![
|
|
||||||
TQ::Arg(
|
|
||||||
Arg::Boolean(Bool::Followed {
|
|
||||||
boosts: false,
|
|
||||||
likes: true
|
|
||||||
}),
|
|
||||||
false
|
|
||||||
),
|
|
||||||
TQ::Arg(Arg::Boolean(Bool::HasCover), false),
|
|
||||||
TQ::Arg(Arg::Boolean(Bool::Local), false),
|
|
||||||
TQ::Arg(Arg::Boolean(Bool::All), false),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rejection_parser() {
|
|
||||||
let missing_and_or = TimelineQuery::parse(r#"followed or has_cover local"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
missing_and_or,
|
|
||||||
QueryError::SyntaxError(
|
|
||||||
22,
|
|
||||||
5,
|
|
||||||
"Syntax Error: Expected on of 'or' or 'and', got 'local'".to_owned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let unbalanced_parent =
|
|
||||||
TimelineQuery::parse(r#"followed and (has_cover or local"#).unwrap_err();
|
|
||||||
assert_eq!(unbalanced_parent, QueryError::UnexpectedEndOfQuery);
|
|
||||||
|
|
||||||
let missing_and_or_in_par =
|
|
||||||
TimelineQuery::parse(r#"(title contains "abc def" followed)"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
missing_and_or_in_par,
|
|
||||||
QueryError::SyntaxError(
|
|
||||||
26,
|
|
||||||
8,
|
|
||||||
"Syntax Error: Expected ')', got 'followed'".to_owned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let expect_in = TimelineQuery::parse(r#"lang contains abc"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
expect_in,
|
|
||||||
QueryError::SyntaxError(
|
|
||||||
5,
|
|
||||||
8,
|
|
||||||
"Syntax Error: Expected 'in', got 'contains'".to_owned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let expect_contains = TimelineQuery::parse(r#"title in abc"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
expect_contains,
|
|
||||||
QueryError::SyntaxError(
|
|
||||||
6,
|
|
||||||
2,
|
|
||||||
"Syntax Error: Expected 'contains', got 'in'".to_owned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let expect_keyword = TimelineQuery::parse(r#"not_a_field contains something"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
expect_keyword,
|
|
||||||
QueryError::SyntaxError(
|
|
||||||
0,
|
|
||||||
11,
|
|
||||||
"Syntax Error: Expected one of 'blog', \
|
|
||||||
'author', 'license', 'tags', 'lang', 'title', 'subtitle', 'content', 'followed', 'has_cover', \
|
|
||||||
'local' or 'all', got 'not_a_field'"
|
|
||||||
.to_owned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let expect_bracket_or_comma = TimelineQuery::parse(r#"lang in [en ["#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
expect_bracket_or_comma,
|
|
||||||
QueryError::SyntaxError(
|
|
||||||
12,
|
|
||||||
1,
|
|
||||||
"Syntax Error: Expected one of ']' or ',', \
|
|
||||||
got '['"
|
|
||||||
.to_owned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let expect_bracket = TimelineQuery::parse(r#"lang in )abc"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
expect_bracket,
|
|
||||||
QueryError::SyntaxError(
|
|
||||||
8,
|
|
||||||
1,
|
|
||||||
"Syntax Error: Expected one of [list, of, words] or list_name, \
|
|
||||||
got ')'"
|
|
||||||
.to_owned()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let expect_word = TimelineQuery::parse(r#"title contains ,"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
expect_word,
|
|
||||||
QueryError::SyntaxError(15, 1, "Syntax Error: Expected any word, got ','".to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
let got_bracket = TimelineQuery::parse(r#"lang in []"#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
got_bracket,
|
|
||||||
QueryError::SyntaxError(9, 1, "Syntax Error: Expected any word, got ']'".to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
let got_par = TimelineQuery::parse(r#"lang in [a, ("#).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
got_par,
|
|
||||||
QueryError::SyntaxError(12, 1, "Syntax Error: Expected any word, got '('".to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_list_used_lists() {
|
|
||||||
let q = TimelineQuery::parse(r#"lang in [fr, en] and blog in blogs or author in my_fav_authors or tags in hashtag and lang in spoken or license in copyleft"#)
|
|
||||||
.unwrap();
|
|
||||||
let used_lists = q.list_used_lists();
|
|
||||||
assert_eq!(
|
|
||||||
used_lists,
|
|
||||||
vec![
|
|
||||||
("blogs".to_owned(), ListType::Blog),
|
|
||||||
("my_fav_authors".to_owned(), ListType::User),
|
|
||||||
("hashtag".to_owned(), ListType::Word),
|
|
||||||
("spoken".to_owned(), ListType::Prefix),
|
|
||||||
("copyleft".to_owned(), ListType::Word),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+173
-222
@@ -1,34 +1,26 @@
|
|||||||
use crate::{
|
|
||||||
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, search::Searcher, timeline::Timeline, Connection,
|
|
||||||
Error, PlumeRocket, Result, ITEMS_PER_PAGE,
|
|
||||||
};
|
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
activity::Delete,
|
activity::Delete,
|
||||||
actor::Person,
|
actor::Person,
|
||||||
collection::{OrderedCollection, OrderedCollectionPage},
|
collection::OrderedCollection,
|
||||||
object::{Image, Tombstone},
|
object::{Image, Tombstone},
|
||||||
Activity, CustomObject, Endpoint,
|
Activity, CustomObject, Endpoint,
|
||||||
};
|
};
|
||||||
use bcrypt;
|
use bcrypt;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use openssl::{
|
use openssl::{
|
||||||
hash::MessageDigest,
|
hash::MessageDigest,
|
||||||
pkey::{PKey, Private},
|
pkey::{PKey, Private},
|
||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sign,
|
sign,
|
||||||
};
|
};
|
||||||
use plume_common::{
|
use plume_common::activity_pub::{
|
||||||
activity_pub::{
|
ap_accept_header,
|
||||||
ap_accept_header,
|
inbox::{AsActor, AsObject, FromId},
|
||||||
inbox::{AsActor, AsObject, FromId},
|
sign::{gen_keypair, Signer},
|
||||||
sign::{gen_keypair, Signer},
|
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
|
||||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
|
|
||||||
},
|
|
||||||
utils,
|
|
||||||
};
|
};
|
||||||
|
use plume_common::utils;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
header::{HeaderValue, ACCEPT},
|
header::{HeaderValue, ACCEPT},
|
||||||
ClientBuilder,
|
ClientBuilder,
|
||||||
@@ -45,13 +37,19 @@ use std::{
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
pub type CustomPerson = CustomObject<ApSignature, Person>;
|
use blogs::Blog;
|
||||||
|
use db_conn::DbConn;
|
||||||
|
use follows::Follow;
|
||||||
|
use instance::*;
|
||||||
|
use medias::Media;
|
||||||
|
use post_authors::PostAuthor;
|
||||||
|
use posts::Post;
|
||||||
|
use safe_string::SafeString;
|
||||||
|
use schema::users;
|
||||||
|
use search::Searcher;
|
||||||
|
use {ap_url, Connection, Error, PlumeRocket, Result};
|
||||||
|
|
||||||
pub enum Role {
|
pub type CustomPerson = CustomObject<ApSignature, Person>;
|
||||||
Admin = 0,
|
|
||||||
Moderator = 1,
|
|
||||||
Normal = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
|
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
@@ -60,6 +58,7 @@ pub struct User {
|
|||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub outbox_url: String,
|
pub outbox_url: String,
|
||||||
pub inbox_url: String,
|
pub inbox_url: String,
|
||||||
|
pub is_admin: bool,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub hashed_password: Option<String>,
|
pub hashed_password: Option<String>,
|
||||||
@@ -74,12 +73,6 @@ pub struct User {
|
|||||||
pub last_fetched_date: NaiveDateTime,
|
pub last_fetched_date: NaiveDateTime,
|
||||||
pub fqn: String,
|
pub fqn: String,
|
||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
/// 0 = admin
|
|
||||||
/// 1 = moderator
|
|
||||||
/// anything else = normal user
|
|
||||||
pub role: i32,
|
|
||||||
pub preferred_theme: Option<String>,
|
|
||||||
pub hide_custom_css: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Insertable)]
|
#[derive(Default, Insertable)]
|
||||||
@@ -89,6 +82,7 @@ pub struct NewUser {
|
|||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub outbox_url: String,
|
pub outbox_url: String,
|
||||||
pub inbox_url: String,
|
pub inbox_url: String,
|
||||||
|
pub is_admin: bool,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub hashed_password: Option<String>,
|
pub hashed_password: Option<String>,
|
||||||
@@ -100,28 +94,50 @@ pub struct NewUser {
|
|||||||
pub followers_endpoint: String,
|
pub followers_endpoint: String,
|
||||||
pub avatar_id: Option<i32>,
|
pub avatar_id: Option<i32>,
|
||||||
pub summary_html: SafeString,
|
pub summary_html: SafeString,
|
||||||
pub role: i32,
|
|
||||||
pub fqn: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const AUTH_COOKIE: &str = "user_id";
|
pub const AUTH_COOKIE: &str = "user_id";
|
||||||
const USER_PREFIX: &str = "@";
|
const USER_PREFIX: &str = "@";
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
insert!(users, NewUser);
|
insert!(users, NewUser, |inserted, conn| {
|
||||||
|
let instance = inserted.get_instance(conn)?;
|
||||||
|
if inserted.outbox_url.is_empty() {
|
||||||
|
inserted.outbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "outbox");
|
||||||
|
}
|
||||||
|
|
||||||
|
if inserted.inbox_url.is_empty() {
|
||||||
|
inserted.inbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "inbox");
|
||||||
|
}
|
||||||
|
|
||||||
|
if inserted.ap_url.is_empty() {
|
||||||
|
inserted.ap_url = instance.compute_box(USER_PREFIX, &inserted.username, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if inserted.shared_inbox_url.is_none() {
|
||||||
|
inserted.shared_inbox_url = Some(ap_url(&format!("{}/inbox", instance.public_domain)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if inserted.followers_endpoint.is_empty() {
|
||||||
|
inserted.followers_endpoint =
|
||||||
|
instance.compute_box(USER_PREFIX, &inserted.username, "followers");
|
||||||
|
}
|
||||||
|
|
||||||
|
if inserted.fqn.is_empty() {
|
||||||
|
if instance.local {
|
||||||
|
inserted.fqn = inserted.username.clone();
|
||||||
|
} else {
|
||||||
|
inserted.fqn = format!("{}@{}", inserted.username, instance.public_domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inserted.save_changes(conn).map_err(Error::from)
|
||||||
|
});
|
||||||
get!(users);
|
get!(users);
|
||||||
find_by!(users, find_by_email, email as &str);
|
find_by!(users, find_by_email, email as &str);
|
||||||
find_by!(users, find_by_name, username as &str, instance_id as i32);
|
find_by!(users, find_by_name, username as &str, instance_id as i32);
|
||||||
find_by!(users, find_by_ap_url, ap_url as &str);
|
find_by!(users, find_by_ap_url, ap_url as &str);
|
||||||
|
|
||||||
pub fn is_moderator(&self) -> bool {
|
|
||||||
self.role == Role::Admin as i32 || self.role == Role::Moderator as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_admin(&self) -> bool {
|
|
||||||
self.role == Role::Admin as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn one_by_instance(conn: &Connection) -> Result<Vec<User>> {
|
pub fn one_by_instance(conn: &Connection) -> Result<Vec<User>> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq_any(users::table.select(users::instance_id).distinct()))
|
.filter(users::instance_id.eq_any(users::table.select(users::instance_id).distinct()))
|
||||||
@@ -130,7 +146,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
||||||
use crate::schema::post_authors;
|
use schema::post_authors;
|
||||||
|
|
||||||
for blog in Blog::find_for_author(conn, self)?
|
for blog in Blog::find_for_author(conn, self)?
|
||||||
.iter()
|
.iter()
|
||||||
@@ -160,10 +176,6 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for notif in Notification::find_followed_by(conn, self)? {
|
|
||||||
notif.delete(conn)?
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::delete(self)
|
diesel::delete(self)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -174,14 +186,46 @@ impl User {
|
|||||||
Instance::get(conn, self.instance_id)
|
Instance::get(conn, self.instance_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_role(&self, conn: &Connection, new_role: Role) -> Result<()> {
|
pub fn grant_admin_rights(&self, conn: &Connection) -> Result<()> {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set(users::role.eq(new_role as i32))
|
.set(users::is_admin.eq(true))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn revoke_admin_rights(&self, conn: &Connection) -> Result<()> {
|
||||||
|
diesel::update(self)
|
||||||
|
.set(users::is_admin.eq(false))
|
||||||
|
.execute(conn)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
&self,
|
||||||
|
conn: &Connection,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
summary: String,
|
||||||
|
) -> Result<User> {
|
||||||
|
diesel::update(self)
|
||||||
|
.set((
|
||||||
|
users::display_name.eq(name),
|
||||||
|
users::email.eq(email),
|
||||||
|
users::summary_html.eq(utils::md_to_html(
|
||||||
|
&summary,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
Some(Media::get_media_processor(conn, vec![self])),
|
||||||
|
)
|
||||||
|
.0),
|
||||||
|
users::summary.eq(summary),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
User::get(conn, self.id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn count_local(conn: &Connection) -> Result<i64> {
|
pub fn count_local(conn: &Connection) -> Result<i64> {
|
||||||
users::table
|
users::table
|
||||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
@@ -193,8 +237,10 @@ impl User {
|
|||||||
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
|
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
|
||||||
let from_db = users::table
|
let from_db = users::table
|
||||||
.filter(users::fqn.eq(fqn))
|
.filter(users::fqn.eq(fqn))
|
||||||
.first(&*c.conn)
|
.limit(1)
|
||||||
.optional()?;
|
.load::<User>(&*c.conn)?
|
||||||
|
.into_iter()
|
||||||
|
.next();
|
||||||
if let Some(from_db) = from_db {
|
if let Some(from_db) = from_db {
|
||||||
Ok(from_db)
|
Ok(from_db)
|
||||||
} else {
|
} else {
|
||||||
@@ -314,77 +360,16 @@ impl User {
|
|||||||
.load::<User>(conn)
|
.load::<User>(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
|
||||||
let mut coll = OrderedCollection::default();
|
|
||||||
let first = &format!("{}?page=1", &self.outbox_url);
|
|
||||||
let last = &format!(
|
|
||||||
"{}?page={}",
|
|
||||||
&self.outbox_url,
|
|
||||||
self.get_activities_count(&conn) / i64::from(ITEMS_PER_PAGE) + 1
|
|
||||||
);
|
|
||||||
coll.collection_props.set_first_link(Id::new(first))?;
|
|
||||||
coll.collection_props.set_last_link(Id::new(last))?;
|
|
||||||
coll.collection_props
|
|
||||||
.set_total_items_u64(self.get_activities_count(&conn) as u64)?;
|
|
||||||
Ok(ActivityStream::new(coll))
|
|
||||||
}
|
|
||||||
pub fn outbox_page(
|
|
||||||
&self,
|
|
||||||
conn: &Connection,
|
|
||||||
(min, max): (i32, i32),
|
|
||||||
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
|
||||||
let acts = self.get_activities_page(conn, (min, max))?;
|
|
||||||
let n_acts = self.get_activities_count(&conn);
|
|
||||||
let mut coll = OrderedCollectionPage::default();
|
|
||||||
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
|
|
||||||
coll.collection_page_props.set_next_link(Id::new(&format!(
|
|
||||||
"{}?page={}",
|
|
||||||
&self.outbox_url,
|
|
||||||
min / ITEMS_PER_PAGE + 2
|
|
||||||
)))?;
|
|
||||||
}
|
|
||||||
if min > 0 {
|
|
||||||
coll.collection_page_props.set_prev_link(Id::new(&format!(
|
|
||||||
"{}?page={}",
|
|
||||||
&self.outbox_url,
|
|
||||||
min / ITEMS_PER_PAGE
|
|
||||||
)))?;
|
|
||||||
}
|
|
||||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
|
||||||
coll.collection_page_props
|
|
||||||
.set_part_of_link(Id::new(&self.outbox_url))?;
|
|
||||||
Ok(ActivityStream::new(coll))
|
|
||||||
}
|
|
||||||
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
|
|
||||||
let mut res = ClientBuilder::new()
|
|
||||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
|
||||||
.build()?
|
|
||||||
.get(url)
|
|
||||||
.header(
|
|
||||||
ACCEPT,
|
|
||||||
HeaderValue::from_str(
|
|
||||||
&ap_accept_header()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", "),
|
|
||||||
)?,
|
|
||||||
)
|
|
||||||
.send()?;
|
|
||||||
let text = &res.text()?;
|
|
||||||
let json: serde_json::Value = serde_json::from_str(text)?;
|
|
||||||
let items = json["items"]
|
|
||||||
.as_array()
|
|
||||||
.unwrap_or(&vec![])
|
|
||||||
.iter()
|
|
||||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
|
||||||
.collect::<Vec<T>>();
|
|
||||||
|
|
||||||
let next = match json.get("next") {
|
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
||||||
Some(x) => Some(x.as_str().unwrap().to_owned()),
|
let acts = self.get_activities(conn)?;
|
||||||
None => None,
|
let n_acts = acts.len();
|
||||||
};
|
let mut coll = OrderedCollection::default();
|
||||||
Ok((items, next))
|
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||||
|
coll.collection_props.set_total_items_u64(n_acts as u64)?;
|
||||||
|
Ok(ActivityStream::new(coll))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
|
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
|
||||||
let mut res = ClientBuilder::new()
|
let mut res = ClientBuilder::new()
|
||||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||||
@@ -402,32 +387,12 @@ impl User {
|
|||||||
.send()?;
|
.send()?;
|
||||||
let text = &res.text()?;
|
let text = &res.text()?;
|
||||||
let json: serde_json::Value = serde_json::from_str(text)?;
|
let json: serde_json::Value = serde_json::from_str(text)?;
|
||||||
if let Some(first) = json.get("first") {
|
Ok(json["items"]
|
||||||
let mut items: Vec<T> = Vec::new();
|
.as_array()
|
||||||
let mut next = first.as_str().unwrap().to_owned();
|
.unwrap_or(&vec![])
|
||||||
while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) {
|
.iter()
|
||||||
if page.is_empty() {
|
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||||
break;
|
.collect::<Vec<T>>())
|
||||||
}
|
|
||||||
items.extend(page.drain(..));
|
|
||||||
if let Some(n) = nxt {
|
|
||||||
if n == next {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
next = n;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(items)
|
|
||||||
} else {
|
|
||||||
Ok(json["items"]
|
|
||||||
.as_array()
|
|
||||||
.unwrap_or(&vec![])
|
|
||||||
.iter()
|
|
||||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
|
||||||
.collect::<Vec<T>>())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
|
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
|
||||||
@@ -454,31 +419,14 @@ impl User {
|
|||||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||||
.collect::<Vec<String>>())
|
.collect::<Vec<String>>())
|
||||||
}
|
}
|
||||||
fn get_activities_count(&self, conn: &Connection) -> i64 {
|
|
||||||
use crate::schema::post_authors;
|
fn get_activities(&self, conn: &Connection) -> Result<Vec<serde_json::Value>> {
|
||||||
use crate::schema::posts;
|
use schema::post_authors;
|
||||||
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
|
use schema::posts;
|
||||||
posts::table
|
|
||||||
.filter(posts::published.eq(true))
|
|
||||||
.filter(posts::id.eq_any(posts_by_self))
|
|
||||||
.count()
|
|
||||||
.first(conn)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
fn get_activities_page(
|
|
||||||
&self,
|
|
||||||
conn: &Connection,
|
|
||||||
(min, max): (i32, i32),
|
|
||||||
) -> Result<Vec<serde_json::Value>> {
|
|
||||||
use crate::schema::post_authors;
|
|
||||||
use crate::schema::posts;
|
|
||||||
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
|
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
|
||||||
let posts = posts::table
|
let posts = posts::table
|
||||||
.filter(posts::published.eq(true))
|
.filter(posts::published.eq(true))
|
||||||
.filter(posts::id.eq_any(posts_by_self))
|
.filter(posts::id.eq_any(posts_by_self))
|
||||||
.order(posts::creation_date.desc())
|
|
||||||
.offset(min.into())
|
|
||||||
.limit((max - min).into())
|
|
||||||
.load::<Post>(conn)?;
|
.load::<Post>(conn)?;
|
||||||
Ok(posts
|
Ok(posts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -491,7 +439,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_followers(&self, conn: &Connection) -> Result<Vec<User>> {
|
pub fn get_followers(&self, conn: &Connection) -> Result<Vec<User>> {
|
||||||
use crate::schema::follows;
|
use schema::follows;
|
||||||
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
||||||
users::table
|
users::table
|
||||||
.filter(users::id.eq_any(follows))
|
.filter(users::id.eq_any(follows))
|
||||||
@@ -500,7 +448,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_followers(&self, conn: &Connection) -> Result<i64> {
|
pub fn count_followers(&self, conn: &Connection) -> Result<i64> {
|
||||||
use crate::schema::follows;
|
use schema::follows;
|
||||||
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
||||||
users::table
|
users::table
|
||||||
.filter(users::id.eq_any(follows))
|
.filter(users::id.eq_any(follows))
|
||||||
@@ -514,7 +462,7 @@ impl User {
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
(min, max): (i32, i32),
|
(min, max): (i32, i32),
|
||||||
) -> Result<Vec<User>> {
|
) -> Result<Vec<User>> {
|
||||||
use crate::schema::follows;
|
use schema::follows;
|
||||||
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
||||||
users::table
|
users::table
|
||||||
.filter(users::id.eq_any(follows))
|
.filter(users::id.eq_any(follows))
|
||||||
@@ -525,7 +473,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_followed(&self, conn: &Connection) -> Result<Vec<User>> {
|
pub fn get_followed(&self, conn: &Connection) -> Result<Vec<User>> {
|
||||||
use crate::schema::follows::dsl::*;
|
use schema::follows::dsl::*;
|
||||||
let f = follows.filter(follower_id.eq(self.id)).select(following_id);
|
let f = follows.filter(follower_id.eq(self.id)).select(following_id);
|
||||||
users::table
|
users::table
|
||||||
.filter(users::id.eq_any(f))
|
.filter(users::id.eq_any(f))
|
||||||
@@ -534,7 +482,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_followed(&self, conn: &Connection) -> Result<i64> {
|
pub fn count_followed(&self, conn: &Connection) -> Result<i64> {
|
||||||
use crate::schema::follows;
|
use schema::follows;
|
||||||
follows::table
|
follows::table
|
||||||
.filter(follows::follower_id.eq(self.id))
|
.filter(follows::follower_id.eq(self.id))
|
||||||
.count()
|
.count()
|
||||||
@@ -547,7 +495,7 @@ impl User {
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
(min, max): (i32, i32),
|
(min, max): (i32, i32),
|
||||||
) -> Result<Vec<User>> {
|
) -> Result<Vec<User>> {
|
||||||
use crate::schema::follows;
|
use schema::follows;
|
||||||
let follows = follows::table
|
let follows = follows::table
|
||||||
.filter(follows::follower_id.eq(self.id))
|
.filter(follows::follower_id.eq(self.id))
|
||||||
.select(follows::following_id)
|
.select(follows::following_id)
|
||||||
@@ -560,7 +508,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> Result<bool> {
|
pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> Result<bool> {
|
||||||
use crate::schema::follows;
|
use schema::follows;
|
||||||
follows::table
|
follows::table
|
||||||
.filter(follows::follower_id.eq(other_id))
|
.filter(follows::follower_id.eq(other_id))
|
||||||
.filter(follows::following_id.eq(self.id))
|
.filter(follows::following_id.eq(self.id))
|
||||||
@@ -571,7 +519,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_following(&self, conn: &Connection, other_id: i32) -> Result<bool> {
|
pub fn is_following(&self, conn: &Connection, other_id: i32) -> Result<bool> {
|
||||||
use crate::schema::follows;
|
use schema::follows;
|
||||||
follows::table
|
follows::table
|
||||||
.filter(follows::follower_id.eq(self.id))
|
.filter(follows::follower_id.eq(self.id))
|
||||||
.filter(follows::following_id.eq(other_id))
|
.filter(follows::following_id.eq(other_id))
|
||||||
@@ -582,7 +530,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_liked(&self, conn: &Connection, post: &Post) -> Result<bool> {
|
pub fn has_liked(&self, conn: &Connection, post: &Post) -> Result<bool> {
|
||||||
use crate::schema::likes;
|
use schema::likes;
|
||||||
likes::table
|
likes::table
|
||||||
.filter(likes::post_id.eq(post.id))
|
.filter(likes::post_id.eq(post.id))
|
||||||
.filter(likes::user_id.eq(self.id))
|
.filter(likes::user_id.eq(self.id))
|
||||||
@@ -593,7 +541,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_reshared(&self, conn: &Connection, post: &Post) -> Result<bool> {
|
pub fn has_reshared(&self, conn: &Connection, post: &Post) -> Result<bool> {
|
||||||
use crate::schema::reshares;
|
use schema::reshares;
|
||||||
reshares::table
|
reshares::table
|
||||||
.filter(reshares::post_id.eq(post.id))
|
.filter(reshares::post_id.eq(post.id))
|
||||||
.filter(reshares::user_id.eq(self.id))
|
.filter(reshares::user_id.eq(self.id))
|
||||||
@@ -604,7 +552,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> Result<bool> {
|
pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> Result<bool> {
|
||||||
use crate::schema::blog_authors;
|
use schema::blog_authors;
|
||||||
blog_authors::table
|
blog_authors::table
|
||||||
.filter(blog_authors::author_id.eq(self.id))
|
.filter(blog_authors::author_id.eq(self.id))
|
||||||
.filter(blog_authors::blog_id.eq(blog.id))
|
.filter(blog_authors::blog_id.eq(blog.id))
|
||||||
@@ -720,7 +668,7 @@ impl User {
|
|||||||
pub fn avatar_url(&self, conn: &Connection) -> String {
|
pub fn avatar_url(&self, conn: &Connection) -> String {
|
||||||
self.avatar_id
|
self.avatar_id
|
||||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
.and_then(|id| Media::get(conn, id).and_then(|m| m.url()).ok())
|
||||||
.unwrap_or_else(|| "/static/images/default-avatar.png".to_string())
|
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
||||||
@@ -804,7 +752,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
|
|||||||
|
|
||||||
impl IntoId for User {
|
impl IntoId for User {
|
||||||
fn into_id(self) -> Id {
|
fn into_id(self) -> Id {
|
||||||
Id::new(self.ap_url)
|
Id::new(self.ap_url.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,12 +793,6 @@ impl FromId<PlumeRocket> for User {
|
|||||||
return Err(Error::InvalidValue);
|
return Err(Error::InvalidValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fqn = if instance.local {
|
|
||||||
username.clone()
|
|
||||||
} else {
|
|
||||||
format!("{}@{}", username, instance.public_domain)
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = User::insert(
|
let user = User::insert(
|
||||||
&c.conn,
|
&c.conn,
|
||||||
NewUser {
|
NewUser {
|
||||||
@@ -862,7 +804,7 @@ impl FromId<PlumeRocket> for User {
|
|||||||
username,
|
username,
|
||||||
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||||
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||||
role: 2,
|
is_admin: false,
|
||||||
summary: acct
|
summary: acct
|
||||||
.object
|
.object
|
||||||
.object_props
|
.object_props
|
||||||
@@ -891,7 +833,6 @@ impl FromId<PlumeRocket> for User {
|
|||||||
.and_then(|e| e.shared_inbox_string())
|
.and_then(|e| e.shared_inbox_string())
|
||||||
.ok(),
|
.ok(),
|
||||||
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
|
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
|
||||||
fqn,
|
|
||||||
avatar_id: None,
|
avatar_id: None,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
@@ -979,58 +920,40 @@ impl NewUser {
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
username: String,
|
username: String,
|
||||||
display_name: String,
|
display_name: String,
|
||||||
role: Role,
|
is_admin: bool,
|
||||||
summary: &str,
|
summary: &str,
|
||||||
email: String,
|
email: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<User> {
|
) -> Result<User> {
|
||||||
let (pub_key, priv_key) = gen_keypair();
|
let (pub_key, priv_key) = gen_keypair();
|
||||||
let instance = Instance::get_local()?;
|
User::insert(
|
||||||
let blocklisted = BlocklistedEmail::matches_blocklist(conn, &email)?;
|
|
||||||
if let Some(x) = blocklisted {
|
|
||||||
return Err(Error::Blocklisted(x.notify_user, x.notification_text));
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = User::insert(
|
|
||||||
conn,
|
conn,
|
||||||
NewUser {
|
NewUser {
|
||||||
username: username.clone(),
|
username,
|
||||||
display_name,
|
display_name,
|
||||||
role: role as i32,
|
is_admin,
|
||||||
summary: summary.to_owned(),
|
summary: summary.to_owned(),
|
||||||
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
|
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
|
||||||
email: Some(email),
|
email: Some(email),
|
||||||
hashed_password: Some(password),
|
hashed_password: Some(password),
|
||||||
instance_id: instance.id,
|
instance_id: Instance::get_local()?.id,
|
||||||
|
ap_url: String::new(),
|
||||||
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
|
||||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||||
outbox_url: instance.compute_box(USER_PREFIX, &username, "outbox"),
|
..NewUser::default()
|
||||||
inbox_url: instance.compute_box(USER_PREFIX, &username, "inbox"),
|
|
||||||
ap_url: instance.compute_box(USER_PREFIX, &username, ""),
|
|
||||||
shared_inbox_url: Some(ap_url(&format!("{}/inbox", &instance.public_domain))),
|
|
||||||
followers_endpoint: instance.compute_box(USER_PREFIX, &username, "followers"),
|
|
||||||
fqn: username,
|
|
||||||
avatar_id: None,
|
|
||||||
},
|
},
|
||||||
)?;
|
)
|
||||||
|
|
||||||
// create default timeline
|
|
||||||
Timeline::new_for_user(conn, res.id, "My feed".into(), "followed".into())?;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
|
||||||
instance::{tests as instance_tests, Instance},
|
|
||||||
search::tests::get_searcher,
|
|
||||||
tests::{db, rockets},
|
|
||||||
Connection as Conn,
|
|
||||||
};
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use instance::{tests as instance_tests, Instance};
|
||||||
|
use search::tests::get_searcher;
|
||||||
|
use tests::{db, rockets};
|
||||||
|
use Connection as Conn;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> Vec<User> {
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<User> {
|
||||||
instance_tests::fill_database(conn);
|
instance_tests::fill_database(conn);
|
||||||
@@ -1038,7 +961,7 @@ pub(crate) mod tests {
|
|||||||
conn,
|
conn,
|
||||||
"admin".to_owned(),
|
"admin".to_owned(),
|
||||||
"The admin".to_owned(),
|
"The admin".to_owned(),
|
||||||
Role::Admin,
|
true,
|
||||||
"Hello there, I'm the admin",
|
"Hello there, I'm the admin",
|
||||||
"admin@example.com".to_owned(),
|
"admin@example.com".to_owned(),
|
||||||
"invalid_admin_password".to_owned(),
|
"invalid_admin_password".to_owned(),
|
||||||
@@ -1048,7 +971,7 @@ pub(crate) mod tests {
|
|||||||
conn,
|
conn,
|
||||||
"user".to_owned(),
|
"user".to_owned(),
|
||||||
"Some user".to_owned(),
|
"Some user".to_owned(),
|
||||||
Role::Normal,
|
false,
|
||||||
"Hello there, I'm no one",
|
"Hello there, I'm no one",
|
||||||
"user@example.com".to_owned(),
|
"user@example.com".to_owned(),
|
||||||
"invalid_user_password".to_owned(),
|
"invalid_user_password".to_owned(),
|
||||||
@@ -1058,7 +981,7 @@ pub(crate) mod tests {
|
|||||||
conn,
|
conn,
|
||||||
"other".to_owned(),
|
"other".to_owned(),
|
||||||
"Another user".to_owned(),
|
"Another user".to_owned(),
|
||||||
Role::Normal,
|
false,
|
||||||
"Hello there, I'm someone else",
|
"Hello there, I'm someone else",
|
||||||
"other@example.com".to_owned(),
|
"other@example.com".to_owned(),
|
||||||
"invalid_other_password".to_owned(),
|
"invalid_other_password".to_owned(),
|
||||||
@@ -1077,12 +1000,13 @@ pub(crate) mod tests {
|
|||||||
conn,
|
conn,
|
||||||
"test".to_owned(),
|
"test".to_owned(),
|
||||||
"test user".to_owned(),
|
"test user".to_owned(),
|
||||||
Role::Normal,
|
false,
|
||||||
"Hello I'm a test",
|
"Hello I'm a test",
|
||||||
"test@example.com".to_owned(),
|
"test@example.com".to_owned(),
|
||||||
User::hash_pass("test_password").unwrap(),
|
User::hash_pass("test_password").unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test_user.id,
|
test_user.id,
|
||||||
User::find_by_name(conn, "test", Instance::get_local().unwrap().id)
|
User::find_by_name(conn, "test", Instance::get_local().unwrap().id)
|
||||||
@@ -1110,6 +1034,7 @@ pub(crate) mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.id
|
.id
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1123,6 +1048,7 @@ pub(crate) mod tests {
|
|||||||
assert!(User::get(conn, inserted[0].id).is_ok());
|
assert!(User::get(conn, inserted[0].id).is_ok());
|
||||||
inserted[0].delete(conn, &get_searcher()).unwrap();
|
inserted[0].delete(conn, &get_searcher()).unwrap();
|
||||||
assert!(User::get(conn, inserted[0].id).is_err());
|
assert!(User::get(conn, inserted[0].id).is_err());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1139,12 +1065,34 @@ pub(crate) mod tests {
|
|||||||
local_inst
|
local_inst
|
||||||
.main_admin(conn)
|
.main_admin(conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_role(conn, Role::Normal)
|
.revoke_admin_rights(conn)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
inserted[0].set_role(conn, Role::Admin).unwrap();
|
inserted[0].grant_admin_rights(conn).unwrap();
|
||||||
assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
|
assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update() {
|
||||||
|
let conn = &db();
|
||||||
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
|
let inserted = fill_database(conn);
|
||||||
|
let updated = inserted[0]
|
||||||
|
.update(
|
||||||
|
conn,
|
||||||
|
"new name".to_owned(),
|
||||||
|
"em@il".to_owned(),
|
||||||
|
"<p>summary</p><script></script>".to_owned(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(updated.display_name, "new name");
|
||||||
|
assert_eq!(updated.email.unwrap(), "em@il");
|
||||||
|
assert_eq!(updated.summary_html.get(), "<p>summary</p>");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1158,7 +1106,7 @@ pub(crate) mod tests {
|
|||||||
conn,
|
conn,
|
||||||
"test".to_owned(),
|
"test".to_owned(),
|
||||||
"test user".to_owned(),
|
"test user".to_owned(),
|
||||||
Role::Normal,
|
false,
|
||||||
"Hello I'm a test",
|
"Hello I'm a test",
|
||||||
"test@example.com".to_owned(),
|
"test@example.com".to_owned(),
|
||||||
User::hash_pass("test_password").unwrap(),
|
User::hash_pass("test_password").unwrap(),
|
||||||
@@ -1167,6 +1115,7 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
assert!(test_user.auth("test_password"));
|
assert!(test_user.auth("test_password"));
|
||||||
assert!(!test_user.auth("other_password"));
|
assert!(!test_user.auth("other_password"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1196,6 +1145,7 @@ pub(crate) mod tests {
|
|||||||
.len() as i64,
|
.len() as i64,
|
||||||
User::count_local(conn).unwrap()
|
User::count_local(conn).unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1223,6 +1173,7 @@ pub(crate) mod tests {
|
|||||||
assert_eq!(user.avatar_url(conn), users[0].avatar_url(conn));
|
assert_eq!(user.avatar_url(conn), users[0].avatar_url(conn));
|
||||||
assert_eq!(user.fqn, users[0].fqn);
|
assert_eq!(user.fqn, users[0].fqn);
|
||||||
assert_eq!(user.summary_html, users[0].summary_html);
|
assert_eq!(user.summary_html, users[0].summary_html);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
extern crate diesel;
|
||||||
|
extern crate plume_common;
|
||||||
|
extern crate plume_models;
|
||||||
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use plume_common::utils::random_hex;
|
use plume_common::utils::random_hex;
|
||||||
use plume_models::migrations::IMPORTED_MIGRATIONS;
|
use plume_models::migrations::IMPORTED_MIGRATIONS;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user