Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c73f993d0 | |||
| 777cbaa69d |
-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
|
||||
Rocket.toml
|
||||
!.gitkeep
|
||||
static
|
||||
static/media
|
||||
docker-compose.yml
|
||||
*.db
|
||||
*.sqlite
|
||||
@@ -16,5 +16,8 @@ docker-compose.yml
|
||||
tags.*
|
||||
!tags.rs
|
||||
search_index
|
||||
main.css
|
||||
*.wasm
|
||||
*.js
|
||||
.buildconfig
|
||||
__pycache__
|
||||
|
||||
Generated
+1013
-1234
File diff suppressed because it is too large
Load Diff
+9
-10
@@ -8,7 +8,6 @@ repository = "https://github.com/Plume-org/Plume"
|
||||
activitypub = "0.1.3"
|
||||
askama_escape = "0.1"
|
||||
atom_syndication = "0.6"
|
||||
clap = "2.33"
|
||||
colored = "1.8"
|
||||
dotenv = "0.14"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
@@ -16,22 +15,22 @@ gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
lettre = "0.9.2"
|
||||
lettre_email = "0.9.2"
|
||||
num_cpus = "1.10"
|
||||
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||
num_cpus = "1.0"
|
||||
rocket = "0.4.0"
|
||||
rocket_contrib = { version = "0.4.0", features = ["json"] }
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
rpassword = "4.0"
|
||||
runtime-fmt = "0.4.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
rpassword = "3.0"
|
||||
runtime-fmt = "0.3.0"
|
||||
scheduled-thread-pool = "0.2.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_qs = "0.5"
|
||||
serde_qs = "0.4"
|
||||
shrinkwraprs = "0.2.1"
|
||||
validator = "0.8"
|
||||
validator_derive = "0.8"
|
||||
webfinger = "0.4.1"
|
||||
webfinger = "0.3.1"
|
||||
|
||||
[[bin]]
|
||||
name = "plume"
|
||||
@@ -65,7 +64,7 @@ path = "plume-models"
|
||||
|
||||
[dependencies.rocket_csrf]
|
||||
git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
||||
rev = "29910f2829e7e590a540da3804336577b48c7b31"
|
||||
rev = "4a72ea2ec716cb0b26188fb00bccf2ef7d1e031c"
|
||||
|
||||
[build-dependencies]
|
||||
ructe = "0.6.2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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
|
||||
</h1>
|
||||
<p align="center">
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
* {
|
||||
font-family: monospace;
|
||||
}
|
||||
@@ -1,19 +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;
|
||||
@@ -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 @@
|
||||
extern crate rsass;
|
||||
extern crate ructe;
|
||||
use ructe::Ructe;
|
||||
use ructe::*;
|
||||
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 {
|
||||
//"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() {
|
||||
Ructe::from_env()
|
||||
.expect("This must be run with cargo")
|
||||
.compile_templates("templates")
|
||||
.expect("compile templates");
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("templates");
|
||||
compile_templates(&in_dir, &out_dir).expect("compile templates");
|
||||
|
||||
compile_themes().expect("Theme compilation error");
|
||||
recursive_copy(&Path::new("assets").join("icons"), &Path::new("static"))
|
||||
.expect("Couldn't copy icons");
|
||||
recursive_copy(&Path::new("assets").join("images"), &Path::new("static"))
|
||||
.expect("Couldn't copy images");
|
||||
create_dir_all(&Path::new("static").join("media")).expect("Couldn't init media directory");
|
||||
println!("cargo:rerun-if-changed=static/css/_article.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_forms.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_global.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_header.scss");
|
||||
println!("cargo:rerun-if-changed=static/css/_variables.scss");
|
||||
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];
|
||||
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
|
||||
@@ -66,91 +74,3 @@ fn main() {
|
||||
|
||||
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;
|
||||
@@ -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 [%]';
|
||||
@@ -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;
|
||||
@@ -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 [%]';
|
||||
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
dotenv = "0.14"
|
||||
rpassword = "4.0"
|
||||
rpassword = "3.0"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
|
||||
@@ -25,11 +25,7 @@ fn main() {
|
||||
.subcommand(users::command());
|
||||
let matches = app.clone().get_matches();
|
||||
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => println!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
dotenv::dotenv().expect("error while reading .env");
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str());
|
||||
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
|
||||
|
||||
|
||||
+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>) {
|
||||
let path = args.value_of("path");
|
||||
let path = match path {
|
||||
Some(path) => Path::new(path).join("search_index"),
|
||||
None => Path::new(&CONFIG.search_index).to_path_buf(),
|
||||
};
|
||||
let path = args.value_of("path").unwrap_or(".");
|
||||
let path = Path::new(path).join("search_index");
|
||||
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap());
|
||||
|
||||
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>) {
|
||||
let path = match args.value_of("path") {
|
||||
None => Path::new(&CONFIG.search_index),
|
||||
Some(x) => Path::new(x),
|
||||
};
|
||||
let meta = Path::new(path).join(".tantivy-meta.lock");
|
||||
let path = args.value_of("path").unwrap_or(".");
|
||||
let meta = Path::new(path).join("search_index/.tantivy-meta.lock");
|
||||
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();
|
||||
}
|
||||
|
||||
+1
-17
@@ -52,12 +52,6 @@ pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
.long("admin")
|
||||
.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"),
|
||||
)
|
||||
.subcommand(
|
||||
@@ -100,17 +94,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
.value_of("display-name")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| super::ask_for("Display name"));
|
||||
|
||||
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 email = args
|
||||
.value_of("email")
|
||||
@@ -129,7 +113,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
conn,
|
||||
username,
|
||||
display_name,
|
||||
role,
|
||||
admin,
|
||||
&bio,
|
||||
email,
|
||||
User::hash_pass(&password).expect("Couldn't hash password"),
|
||||
|
||||
@@ -5,13 +5,13 @@ authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
activitypub = "0.1.1"
|
||||
activitystreams-derive = "0.1.1"
|
||||
activitystreams-derive = "0.1.0"
|
||||
activitystreams-traits = "0.1.0"
|
||||
array_tool = "1.0"
|
||||
base64 = "0.10"
|
||||
heck = "0.3.0"
|
||||
hex = "0.3"
|
||||
hyper = "0.12.33"
|
||||
hyper = "0.12.28"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
reqwest = "0.9"
|
||||
@@ -19,7 +19,7 @@ serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shrinkwraprs = "0.2.1"
|
||||
tokio = "0.1.22"
|
||||
tokio = "0.1.21"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(custom_attribute, associated_type_defaults)]
|
||||
|
||||
extern crate activitypub;
|
||||
#[macro_use]
|
||||
|
||||
@@ -97,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>(
|
||||
evt: Event<'a>,
|
||||
|
||||
@@ -4,11 +4,9 @@ version = "0.3.0"
|
||||
authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
stdweb = "=0.4.18"
|
||||
stdweb-internal-runtime = "=0.1.4"
|
||||
stdweb = "=0.4.14"
|
||||
stdweb-internal-runtime = "=0.1.3"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
gettext-macros = { 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"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
+25
-191
@@ -1,6 +1,3 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::sync::Mutex;
|
||||
use stdweb::{
|
||||
unstable::{TryFrom, TryInto},
|
||||
web::{event::*, html_element::*, *},
|
||||
@@ -19,26 +16,17 @@ macro_rules! mv {
|
||||
fn get_elt_value(id: &'static str) -> String {
|
||||
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.raw_value()).unwrap_or_else(|_| {
|
||||
textarea
|
||||
.map(|t| t.value())
|
||||
.unwrap_or_else(|_| select.unwrap().raw_value())
|
||||
})
|
||||
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
||||
inp.map(|i| i.raw_value())
|
||||
.unwrap_or_else(|_| textarea.unwrap().value())
|
||||
}
|
||||
|
||||
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();
|
||||
let textarea: Result<TextAreaElement, _> = 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()))
|
||||
})
|
||||
.unwrap_or_else(|_| textarea.unwrap().set_value(val.as_ref()))
|
||||
}
|
||||
|
||||
fn no_return(evt: KeyDownEvent) {
|
||||
@@ -74,148 +62,7 @@ impl From<stdweb::private::ConversionError> for EditorError {
|
||||
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,
|
||||
@@ -253,40 +100,29 @@ fn filter_paste(elt: &HtmlElement) {
|
||||
}
|
||||
|
||||
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
|
||||
if window()
|
||||
.local_storage()
|
||||
.get("basic-editor")
|
||||
.map(|x| x == "true")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
|
||||
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
|
||||
let editor_button = document().create_element("a")?;
|
||||
js! { @{&editor_button}.href = "#"; }
|
||||
editor_button.add_event_listener(|_: ClickEvent| {
|
||||
window().local_storage().remove("basic-editor");
|
||||
window().history().go(0).ok(); // refresh
|
||||
});
|
||||
editor_button.append_child(
|
||||
&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());
|
||||
if let Some(basic_editor) = window().local_storage().get("basic-editor") {
|
||||
if basic_editor == "true" {
|
||||
if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
|
||||
if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
|
||||
let editor_button = document().create_element("a")?;
|
||||
js! { @{&editor_button}.href = "#"; }
|
||||
editor_button.add_event_listener(|_: ClickEvent| {
|
||||
window().local_storage().remove("basic-editor");
|
||||
window().history().go(0).ok(); // refresh
|
||||
});
|
||||
editor_button.append_child(
|
||||
&document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
|
||||
);
|
||||
editor.insert_before(&editor_button, &title_label).ok();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
init_editor()
|
||||
}
|
||||
|
||||
// If we didn't returned above
|
||||
init_editor()
|
||||
}
|
||||
|
||||
fn init_editor() -> Result<(), EditorError> {
|
||||
@@ -334,7 +170,6 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
}).ok();
|
||||
};
|
||||
}), 0);
|
||||
autosave_debounce();
|
||||
}));
|
||||
|
||||
document().get_element_by_id("publish")?.add_event_listener(
|
||||
@@ -470,7 +305,6 @@ fn init_popup(
|
||||
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();
|
||||
};
|
||||
|
||||
+5
-11
@@ -8,8 +8,7 @@ extern crate gettext_macros;
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate stdweb;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
use stdweb::web::{event::*, *};
|
||||
|
||||
init_i18n!(
|
||||
@@ -47,15 +46,10 @@ lazy_static! {
|
||||
let catalogs = include_i18n!();
|
||||
let lang = js! { return navigator.language }.into_string().unwrap();
|
||||
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
||||
|
||||
let english_position = catalogs
|
||||
.iter()
|
||||
.position(|(language_code, _)| *language_code == "en")
|
||||
.unwrap();
|
||||
catalogs
|
||||
.iter()
|
||||
.find(|(l, _)| l == &lang)
|
||||
.unwrap_or(&catalogs[english_position])
|
||||
.unwrap_or(&catalogs[0])
|
||||
.clone()
|
||||
.1
|
||||
};
|
||||
@@ -69,19 +63,19 @@ fn main() {
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Toggle menu on mobile devices
|
||||
/// Toggle menu on mobile device
|
||||
///
|
||||
/// It should normally be working fine even without this code
|
||||
/// But :focus-within is not yet supported by Webkit/Blink
|
||||
fn menu() {
|
||||
if let Some(button) = document().get_element_by_id("menu") {
|
||||
if let Some(menu) = document().get_element_by_id("content") {
|
||||
button.add_event_listener(|_: TouchEnd| {
|
||||
button.add_event_listener(|_: ClickEvent| {
|
||||
document()
|
||||
.get_element_by_id("menu")
|
||||
.map(|menu| menu.class_list().add("show"));
|
||||
});
|
||||
menu.add_event_listener(|_: TouchEnd| {
|
||||
menu.add_event_listener(|_: ClickEvent| {
|
||||
document()
|
||||
.get_element_by_id("menu")
|
||||
.map(|menu| menu.class_list().remove("show"));
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = ["Plume contributors"]
|
||||
activitypub = "0.1.1"
|
||||
ammonia = "2.1.1"
|
||||
askama_escape = "0.1"
|
||||
bcrypt = "0.5"
|
||||
bcrypt = "0.4"
|
||||
guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
itertools = "0.8.0"
|
||||
@@ -17,14 +17,13 @@ openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
reqwest = "0.9"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
scheduled-thread-pool = "0.2.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
tantivy = "0.10.1"
|
||||
url = "2.1"
|
||||
walkdir = "2.2"
|
||||
webfinger = "0.4.1"
|
||||
tantivy = "0.9.1"
|
||||
url = "1.7"
|
||||
webfinger = "0.3.1"
|
||||
whatlang = "0.7.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
diesel-derive-newtype = "0.1.2"
|
||||
@@ -50,5 +49,5 @@ path = "../plume-macro"
|
||||
diesel_migrations = "1.3.0"
|
||||
|
||||
[features]
|
||||
postgres = ["diesel/postgres", "plume-macro/postgres" ]
|
||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
|
||||
postgres = ["diesel/postgres", "plume-macro/postgres"]
|
||||
sqlite = ["diesel/sqlite", "plume-macro/sqlite"]
|
||||
|
||||
@@ -14,26 +14,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> {
|
||||
let user = request.guard::<User>()?;
|
||||
if user.is_admin() {
|
||||
if user.is_admin {
|
||||
Outcome::Success(Admin(user))
|
||||
} else {
|
||||
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, ()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-60
@@ -1,11 +1,6 @@
|
||||
use activitypub::{
|
||||
actor::Group,
|
||||
collection::{OrderedCollection, OrderedCollectionPage},
|
||||
object::Image,
|
||||
CustomObject,
|
||||
};
|
||||
use activitypub::{actor::Group, collection::OrderedCollection, object::Image, CustomObject};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
@@ -27,7 +22,7 @@ use safe_string::SafeString;
|
||||
use schema::blogs;
|
||||
use search::Searcher;
|
||||
use users::User;
|
||||
use {ap_url, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE};
|
||||
use {Connection, Error, PlumeRocket, Result};
|
||||
|
||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||
|
||||
@@ -49,7 +44,6 @@ pub struct Blog {
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
pub theme: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Insertable)]
|
||||
@@ -67,7 +61,6 @@ pub struct NewBlog {
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
pub theme: Option<String>,
|
||||
}
|
||||
|
||||
const BLOG_PREFIX: &str = "~";
|
||||
@@ -140,8 +133,10 @@ impl Blog {
|
||||
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
|
||||
let from_db = blogs::table
|
||||
.filter(blogs::fqn.eq(fqn))
|
||||
.first(&*c.conn)
|
||||
.optional()?;
|
||||
.limit(1)
|
||||
.load::<Blog>(&*c.conn)?
|
||||
.into_iter()
|
||||
.next();
|
||||
if let Some(from_db) = from_db {
|
||||
Ok(from_db)
|
||||
} else {
|
||||
@@ -150,7 +145,7 @@ impl Blog {
|
||||
}
|
||||
|
||||
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
|
||||
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||
resolve(acct.to_owned(), true)?
|
||||
.links
|
||||
.into_iter()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
@@ -225,49 +220,12 @@ impl Blog {
|
||||
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
|
||||
coll.collection_props
|
||||
.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))
|
||||
}
|
||||
|
||||
fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
|
||||
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>> {
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
@@ -314,7 +272,7 @@ impl Blog {
|
||||
pub fn icon_url(&self, conn: &Connection) -> String {
|
||||
self.icon_id
|
||||
.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> {
|
||||
@@ -434,7 +392,6 @@ impl FromId<PlumeRocket> for Blog {
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
theme: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -612,8 +569,9 @@ pub(crate) mod tests {
|
||||
Instance::get_local().unwrap().id
|
||||
);
|
||||
// TODO add tests for remote instance
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -713,8 +671,9 @@ pub(crate) mod tests {
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -737,8 +696,9 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Blog::find_by_fqn(&r, "SomeName").unwrap().id, blog.id);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -760,8 +720,9 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blog.fqn, "SomeName");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -772,8 +733,9 @@ pub(crate) mod tests {
|
||||
|
||||
blogs[0].delete(conn, &get_searcher()).unwrap();
|
||||
assert!(Blog::get(conn, blogs[0].id).is_err());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -842,8 +804,9 @@ pub(crate) mod tests {
|
||||
assert!(Blog::get(conn, blog[1].id).is_err());
|
||||
user[1].delete(conn, &searcher).unwrap();
|
||||
assert!(Blog::get(conn, blog[0].id).is_err());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -904,6 +867,6 @@ pub(crate) mod tests {
|
||||
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +436,8 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ pub struct Config {
|
||||
pub search_index: String,
|
||||
pub rocket: Result<RocketConfig, RocketError>,
|
||||
pub logo: LogoConfig,
|
||||
pub default_theme: String,
|
||||
pub media_directory: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -201,8 +199,5 @@ lazy_static! {
|
||||
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
|
||||
rocket: get_rocket_config(),
|
||||
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()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,8 +235,7 @@ mod tests {
|
||||
)
|
||||
.expect("Couldn't insert new follow");
|
||||
assert_eq!(follow.ap_url, String::from("https://some.url/"));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -169,6 +170,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -210,6 +212,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -252,8 +255,9 @@ pub(crate) mod tests {
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -278,6 +282,7 @@ pub(crate) mod tests {
|
||||
"type": "Delete",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -331,6 +336,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -356,6 +362,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -394,6 +401,7 @@ pub(crate) mod tests {
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -432,6 +440,7 @@ pub(crate) mod tests {
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -470,6 +479,7 @@ pub(crate) mod tests {
|
||||
"type": "Undo",
|
||||
});
|
||||
assert!(super::inbox(&r, ok_act).is_ok());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -499,6 +509,7 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
super::inbox(&r, act).unwrap();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use std::iter::Iterator;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use ap_url;
|
||||
@@ -7,7 +8,7 @@ use medias::Media;
|
||||
use plume_common::utils::md_to_html;
|
||||
use safe_string::SafeString;
|
||||
use schema::{instances, users};
|
||||
use users::{Role, User};
|
||||
use users::User;
|
||||
use {Connection, Error, Result};
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
@@ -60,8 +61,11 @@ impl Instance {
|
||||
pub fn get_local_uncached(conn: &Connection) -> Result<Instance> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(true))
|
||||
.first(conn)
|
||||
.map_err(Error::from)
|
||||
.limit(1)
|
||||
.load::<Instance>(conn)?
|
||||
.into_iter()
|
||||
.nth(0)
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
pub fn cache_local(conn: &Connection) {
|
||||
@@ -113,7 +117,7 @@ impl Instance {
|
||||
pub fn has_admin(&self, conn: &Connection) -> Result<bool> {
|
||||
users::table
|
||||
.filter(users::instance_id.eq(self.id))
|
||||
.filter(users::role.eq(Role::Admin as i32))
|
||||
.filter(users::is_admin.eq(true))
|
||||
.load::<User>(conn)
|
||||
.map_err(Error::from)
|
||||
.map(|r| !r.is_empty())
|
||||
@@ -122,8 +126,9 @@ impl Instance {
|
||||
pub fn main_admin(&self, conn: &Connection) -> Result<User> {
|
||||
users::table
|
||||
.filter(users::instance_id.eq(self.id))
|
||||
.filter(users::role.eq(Role::Admin as i32))
|
||||
.first(conn)
|
||||
.filter(users::is_admin.eq(true))
|
||||
.limit(1)
|
||||
.get_result::<User>(conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
@@ -144,7 +149,6 @@ impl Instance {
|
||||
open_registrations: bool,
|
||||
short_description: SafeString,
|
||||
long_description: SafeString,
|
||||
default_license: String,
|
||||
) -> Result<()> {
|
||||
let (sd, _, _) = md_to_html(
|
||||
short_description.as_ref(),
|
||||
@@ -166,7 +170,6 @@ impl Instance {
|
||||
instances::long_description.eq(long_description),
|
||||
instances::short_description_html.eq(sd),
|
||||
instances::long_description_html.eq(ld),
|
||||
instances::default_license.eq(default_license),
|
||||
))
|
||||
.execute(conn)
|
||||
.map(|_| ())
|
||||
@@ -183,60 +186,6 @@ impl Instance {
|
||||
.get_result(conn)
|
||||
.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)]
|
||||
@@ -338,6 +287,7 @@ pub(crate) mod tests {
|
||||
res.short_description_html.get(),
|
||||
&inserted.short_description_html
|
||||
);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -398,6 +348,7 @@ pub(crate) mod tests {
|
||||
assert!(last_domaine <= page[0].public_domain);
|
||||
last_domaine = page[0].public_domain.clone();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -460,6 +411,7 @@ pub(crate) mod tests {
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -476,7 +428,6 @@ pub(crate) mod tests {
|
||||
false,
|
||||
SafeString::new("[short](#link)"),
|
||||
SafeString::new("[long_description](/with_link)"),
|
||||
"CC-BY-SAO".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let inst = Instance::get(conn, inst.id).unwrap();
|
||||
@@ -495,7 +446,7 @@ pub(crate) mod tests {
|
||||
inst.short_description_html,
|
||||
SafeString::new("<p><a href=\"#link\">short</a></p>\n")
|
||||
);
|
||||
assert_eq!(inst.default_license, "CC-BY-SAO".to_owned());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
+20
-23
@@ -1,7 +1,6 @@
|
||||
#![feature(try_trait)]
|
||||
#![feature(never_type)]
|
||||
//#![feature(register_attr)]
|
||||
//#![feature(register_tool)]
|
||||
#![feature(custom_attribute)]
|
||||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate activitypub;
|
||||
@@ -34,7 +33,6 @@ extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate tantivy;
|
||||
extern crate url;
|
||||
extern crate walkdir;
|
||||
extern crate webfinger;
|
||||
extern crate whatlang;
|
||||
|
||||
@@ -64,7 +62,6 @@ pub enum Error {
|
||||
SerDe,
|
||||
Search(search::SearcherError),
|
||||
Signature,
|
||||
TimelineQuery(timeline::query::QueryError),
|
||||
Unauthorized,
|
||||
Url,
|
||||
Webfinger,
|
||||
@@ -76,7 +73,7 @@ impl From<bcrypt::BcryptError> for Error {
|
||||
Error::Signature
|
||||
}
|
||||
}
|
||||
pub const ITEMS_PER_PAGE: i32 = 12;
|
||||
|
||||
impl From<openssl::error::ErrorStack> for Error {
|
||||
fn from(_: openssl::error::ErrorStack) -> Self {
|
||||
Error::Signature
|
||||
@@ -140,12 +137,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 {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Error::Io(err)
|
||||
@@ -182,8 +173,11 @@ macro_rules! find_by {
|
||||
pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result<Self> {
|
||||
$table::table
|
||||
$(.filter($table::$col.eq($col)))+
|
||||
.first(conn)
|
||||
.map_err(Error::from)
|
||||
.limit(1)
|
||||
.load::<Self>(conn)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -229,8 +223,11 @@ macro_rules! get {
|
||||
pub fn get(conn: &crate::Connection, id: i32) -> Result<Self> {
|
||||
$table::table
|
||||
.filter($table::id.eq(id))
|
||||
.first(conn)
|
||||
.map_err(Error::from)
|
||||
.limit(1)
|
||||
.load::<Self>(conn)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -254,7 +251,6 @@ macro_rules! insert {
|
||||
($table:ident, $from:ident, |$val:ident, $conn:ident | $( $after:tt )+) => {
|
||||
last!($table);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> {
|
||||
diesel::insert_into($table::table)
|
||||
.values(new)
|
||||
@@ -281,12 +277,14 @@ macro_rules! insert {
|
||||
/// ```
|
||||
macro_rules! last {
|
||||
($table:ident) => {
|
||||
#[allow(dead_code)]
|
||||
pub fn last(conn: &crate::Connection) -> Result<Self> {
|
||||
$table::table
|
||||
.order_by($table::id.desc())
|
||||
.first(conn)
|
||||
.map_err(Error::from)
|
||||
.limit(1)
|
||||
.load::<Self>(conn)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -303,6 +301,8 @@ pub fn ap_url(url: &str) -> String {
|
||||
mod tests {
|
||||
use db_conn;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use diesel::{dsl::sql_query, RunQueryDsl};
|
||||
use migrations::IMPORTED_MIGRATIONS;
|
||||
use plume_common::utils::random_hex;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
@@ -330,7 +330,7 @@ mod tests {
|
||||
lazy_static! {
|
||||
static ref DB_POOL: db_conn::DbPool = {
|
||||
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()))
|
||||
.unwrap();
|
||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||
@@ -364,7 +364,6 @@ pub mod headers;
|
||||
pub mod inbox;
|
||||
pub mod instance;
|
||||
pub mod likes;
|
||||
pub mod lists;
|
||||
pub mod medias;
|
||||
pub mod mentions;
|
||||
pub mod migrations;
|
||||
@@ -375,10 +374,8 @@ pub mod post_authors;
|
||||
pub mod posts;
|
||||
pub mod reshares;
|
||||
pub mod safe_string;
|
||||
#[allow(unused_imports)]
|
||||
pub mod schema;
|
||||
pub mod search;
|
||||
pub mod tags;
|
||||
pub mod timeline;
|
||||
pub mod users;
|
||||
pub use plume_rocket::PlumeRocket;
|
||||
|
||||
@@ -9,7 +9,6 @@ use plume_common::activity_pub::{
|
||||
};
|
||||
use posts::Post;
|
||||
use schema::likes;
|
||||
use timeline::*;
|
||||
use users::User;
|
||||
use {Connection, Error, PlumeRocket, Result};
|
||||
|
||||
@@ -100,8 +99,6 @@ impl AsObject<User, activity::Like, &PlumeRocket> for Post {
|
||||
},
|
||||
)?;
|
||||
res.notify(&c.conn)?;
|
||||
|
||||
Timeline::add_to_all_timelines(c, &self, Kind::Like(&actor))?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,554 +0,0 @@
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use blogs::Blog;
|
||||
use schema::{blogs, list_elems, lists, users};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use users::User;
|
||||
use {Connection, Error, Result};
|
||||
|
||||
/// 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 blogs::tests as blog_tests;
|
||||
use diesel::Connection;
|
||||
use tests::db;
|
||||
|
||||
#[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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
+15
-25
@@ -62,14 +62,7 @@ impl MediaCategory {
|
||||
impl Media {
|
||||
insert!(medias, NewMedia);
|
||||
get!(medias);
|
||||
|
||||
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)
|
||||
}
|
||||
list_by!(medias, for_user, owner_id as i32);
|
||||
|
||||
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
|
||||
medias::table.load::<Media>(conn).map_err(Error::from)
|
||||
@@ -82,7 +75,6 @@ impl Media {
|
||||
) -> Result<Vec<Media>> {
|
||||
medias::table
|
||||
.filter(medias::owner_id.eq(user.id))
|
||||
.order(medias::id.desc())
|
||||
.offset(i64::from(min))
|
||||
.limit(i64::from(max - min))
|
||||
.load::<Media>(conn)
|
||||
@@ -117,19 +109,15 @@ impl Media {
|
||||
Ok(match self.category() {
|
||||
MediaCategory::Image => SafeString::trusted(&format!(
|
||||
r#"<img src="{}" alt="{}" title="{}">"#,
|
||||
url,
|
||||
escape(&self.alt_text),
|
||||
escape(&self.alt_text)
|
||||
url, escape(&self.alt_text), escape(&self.alt_text)
|
||||
)),
|
||||
MediaCategory::Audio => SafeString::trusted(&format!(
|
||||
r#"<div class="media-preview audio"></div><audio src="{}" title="{}" controls></audio>"#,
|
||||
url,
|
||||
escape(&self.alt_text)
|
||||
url, escape(&self.alt_text)
|
||||
)),
|
||||
MediaCategory::Video => SafeString::trusted(&format!(
|
||||
r#"<video src="{}" title="{}" controls></video>"#,
|
||||
url,
|
||||
escape(&self.alt_text)
|
||||
url, escape(&self.alt_text)
|
||||
)),
|
||||
MediaCategory::Unknown => SafeString::trusted(&format!(
|
||||
r#"<a href="{}" class="media-preview unknown"></a>"#,
|
||||
@@ -152,12 +140,10 @@ impl Media {
|
||||
if self.is_remote {
|
||||
Ok(self.remote_url.clone().unwrap_or_default())
|
||||
} else {
|
||||
let p = Path::new(&self.file_path);
|
||||
let filename: String = p.file_name().unwrap().to_str().unwrap().to_owned();
|
||||
Ok(ap_url(&format!(
|
||||
"{}/static/media/{}",
|
||||
"{}/{}",
|
||||
Instance::get_local()?.public_domain,
|
||||
&filename
|
||||
self.file_path
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -208,11 +194,10 @@ impl Media {
|
||||
.next()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| String::from("png"));
|
||||
let path = Path::new(&super::CONFIG.media_directory).join(format!(
|
||||
"{}.{}",
|
||||
GUID::rand().to_string(),
|
||||
ext
|
||||
));
|
||||
let path =
|
||||
Path::new("static")
|
||||
.join("media")
|
||||
.join(format!("{}.{}", GUID::rand().to_string(), ext));
|
||||
|
||||
let mut dest = fs::File::create(path.clone()).ok()?;
|
||||
reqwest::get(remote_url.as_str())
|
||||
@@ -331,6 +316,8 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
//set_owner
|
||||
|
||||
#[test]
|
||||
fn delete() {
|
||||
let conn = &db();
|
||||
@@ -359,11 +346,13 @@ pub(crate) mod tests {
|
||||
assert!(!Path::new(&path).exists());
|
||||
|
||||
clean(conn);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn set_owner() {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
@@ -407,6 +396,7 @@ pub(crate) mod tests {
|
||||
.any(|m| m.id == media.id));
|
||||
|
||||
clean(conn);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::path::Path;
|
||||
#[allow(dead_code)] //variants might not be constructed if not required by current migrations
|
||||
enum Action {
|
||||
Sql(&'static str),
|
||||
Function(&'static dyn Fn(&Connection, &Path) -> Result<()>),
|
||||
Function(&'static Fn(&Connection, &Path) -> Result<()>),
|
||||
}
|
||||
|
||||
impl Action {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use comments::Comment;
|
||||
use follows::Follow;
|
||||
@@ -7,7 +7,7 @@ use likes::Like;
|
||||
use mentions::Mention;
|
||||
use posts::Post;
|
||||
use reshares::Reshare;
|
||||
use schema::{follows, notifications};
|
||||
use schema::notifications;
|
||||
use users::User;
|
||||
use {Connection, Error, Result};
|
||||
|
||||
@@ -64,16 +64,6 @@ impl Notification {
|
||||
.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> {
|
||||
notifications::table
|
||||
.filter(notifications::user_id.eq(user.id))
|
||||
|
||||
@@ -26,7 +26,6 @@ use safe_string::SafeString;
|
||||
use schema::posts;
|
||||
use search::Searcher;
|
||||
use tags::*;
|
||||
use timeline::*;
|
||||
use users::User;
|
||||
use {ap_url, Connection, Error, PlumeRocket, Result, CONFIG};
|
||||
|
||||
@@ -183,6 +182,15 @@ impl Post {
|
||||
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(
|
||||
conn: &Connection,
|
||||
author: &User,
|
||||
@@ -238,6 +246,60 @@ impl Post {
|
||||
.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>> {
|
||||
use schema::post_authors;
|
||||
|
||||
@@ -273,8 +335,11 @@ impl Post {
|
||||
use schema::blogs;
|
||||
blogs::table
|
||||
.filter(blogs::id.eq(self.blog_id))
|
||||
.first(conn)
|
||||
.map_err(Error::from)
|
||||
.limit(1)
|
||||
.load::<Blog>(conn)?
|
||||
.into_iter()
|
||||
.nth(0)
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
|
||||
@@ -652,9 +717,6 @@ impl FromId<PlumeRocket> for Post {
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Timeline::add_to_all_timelines(c, &post, Kind::Original)?;
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
}
|
||||
@@ -866,6 +928,7 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Unexpected result"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use plume_common::activity_pub::{
|
||||
};
|
||||
use posts::Post;
|
||||
use schema::reshares;
|
||||
use timeline::*;
|
||||
use users::User;
|
||||
use {Connection, Error, PlumeRocket, Result};
|
||||
|
||||
@@ -125,8 +124,6 @@ impl AsObject<User, Announce, &PlumeRocket> for Post {
|
||||
},
|
||||
)?;
|
||||
reshare.notify(conn)?;
|
||||
|
||||
Timeline::add_to_all_timelines(c, &self, Kind::Reshare(&actor))?;
|
||||
Ok(reshare)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,6 @@ lazy_static! {
|
||||
.add_tag_attributes("label", ["for"].iter())
|
||||
.add_tag_attributes("input", ["type", "checked"].iter())
|
||||
.add_allowed_classes("input", ["cw-checkbox"].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())
|
||||
.add_allowed_classes("span", ["cw-container", "cw-text"].iter())
|
||||
.attribute_filter(|elem, att, val| match (elem, att) {
|
||||
("input", "type") => Some("checkbox".into()),
|
||||
|
||||
@@ -47,7 +47,6 @@ table! {
|
||||
summary_html -> Text,
|
||||
icon_id -> Nullable<Int4>,
|
||||
banner_id -> Nullable<Int4>,
|
||||
theme -> Nullable<Varchar>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,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! {
|
||||
medias (id) {
|
||||
id -> Int4,
|
||||
@@ -215,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! {
|
||||
users (id) {
|
||||
id -> Int4,
|
||||
@@ -239,6 +201,7 @@ table! {
|
||||
display_name -> Varchar,
|
||||
outbox_url -> Varchar,
|
||||
inbox_url -> Varchar,
|
||||
is_admin -> Bool,
|
||||
summary -> Text,
|
||||
email -> Nullable<Text>,
|
||||
hashed_password -> Nullable<Text>,
|
||||
@@ -253,9 +216,6 @@ table! {
|
||||
last_fetched_date -> Timestamp,
|
||||
fqn -> Text,
|
||||
summary_html -> Text,
|
||||
role -> Int4,
|
||||
preferred_theme -> Nullable<Varchar>,
|
||||
hide_custom_css -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,10 +230,6 @@ joinable!(comments -> posts (post_id));
|
||||
joinable!(comments -> users (author_id));
|
||||
joinable!(likes -> posts (post_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 -> posts (post_id));
|
||||
joinable!(mentions -> users (mentioned_id));
|
||||
@@ -285,9 +241,6 @@ joinable!(posts -> medias (cover_id));
|
||||
joinable!(reshares -> posts (post_id));
|
||||
joinable!(reshares -> users (user_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));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
@@ -300,8 +253,6 @@ allow_tables_to_appear_in_same_query!(
|
||||
follows,
|
||||
instances,
|
||||
likes,
|
||||
list_elems,
|
||||
lists,
|
||||
medias,
|
||||
mentions,
|
||||
notifications,
|
||||
@@ -310,7 +261,5 @@ allow_tables_to_appear_in_same_query!(
|
||||
posts,
|
||||
reshares,
|
||||
tags,
|
||||
timeline,
|
||||
timeline_definition,
|
||||
users,
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(crate) mod tests {
|
||||
use tests::db;
|
||||
|
||||
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() {
|
||||
Searcher::open(&dir)
|
||||
} else {
|
||||
@@ -96,10 +96,11 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -174,6 +175,7 @@ pub(crate) mod tests {
|
||||
assert!(searcher
|
||||
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
|
||||
.is_empty());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ impl PlumeQuery {
|
||||
|
||||
/// Convert this Query to a Tantivy Query
|
||||
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;
|
||||
oneoff: instance, author, blog, lang, license);
|
||||
|
||||
@@ -279,7 +279,7 @@ impl PlumeQuery {
|
||||
}
|
||||
|
||||
// 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.as_str();
|
||||
let field = Searcher::schema().get_field(field_name).unwrap();
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Searcher {
|
||||
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 content_tokenizer = SimpleTokenizer
|
||||
@@ -111,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 content_tokenizer = SimpleTokenizer
|
||||
@@ -148,10 +148,6 @@ impl Searcher {
|
||||
}
|
||||
|
||||
pub fn add_document(&self, conn: &Connection, post: &Post) -> Result<()> {
|
||||
if !post.published {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let schema = self.index.schema();
|
||||
|
||||
let post_id = schema.get_field("post_id").unwrap();
|
||||
|
||||
@@ -1,844 +0,0 @@
|
||||
use diesel::{self, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use lists::List;
|
||||
use posts::Post;
|
||||
use schema::{posts, timeline, timeline_definition};
|
||||
use std::ops::Deref;
|
||||
use {Connection, Error, PlumeRocket, Result};
|
||||
|
||||
pub(crate) mod query;
|
||||
|
||||
use self::query::{QueryError, TimelineQuery};
|
||||
|
||||
pub use self::query::Kind;
|
||||
|
||||
#[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),
|
||||
))),
|
||||
}
|
||||
})
|
||||
{
|
||||
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),
|
||||
))),
|
||||
}
|
||||
})
|
||||
{
|
||||
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 blogs::tests as blogTests;
|
||||
use diesel::Connection;
|
||||
use follows::*;
|
||||
use lists::ListType;
|
||||
use post_authors::{NewPostAuthor, PostAuthor};
|
||||
use posts::NewPost;
|
||||
use safe_string::SafeString;
|
||||
use tags::Tag;
|
||||
use tests::{db, rockets};
|
||||
use users::tests as userTests;
|
||||
|
||||
#[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,885 +0,0 @@
|
||||
use blogs::Blog;
|
||||
use lists::{self, ListType};
|
||||
use plume_common::activity_pub::inbox::AsActor;
|
||||
use posts::Post;
|
||||
use tags::Tag;
|
||||
use users::User;
|
||||
use whatlang::{self, Lang};
|
||||
|
||||
use {PlumeRocket, Result};
|
||||
|
||||
use super::Timeline;
|
||||
|
||||
#[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
|
||||
)))?,
|
||||
}
|
||||
}
|
||||
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),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+138
-187
@@ -1,13 +1,13 @@
|
||||
use activitypub::{
|
||||
activity::Delete,
|
||||
actor::Person,
|
||||
collection::{OrderedCollection, OrderedCollectionPage},
|
||||
collection::OrderedCollection,
|
||||
object::{Image, Tombstone},
|
||||
Activity, CustomObject, Endpoint,
|
||||
};
|
||||
use bcrypt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
@@ -42,23 +42,15 @@ use db_conn::DbConn;
|
||||
use follows::Follow;
|
||||
use instance::*;
|
||||
use medias::Media;
|
||||
use notifications::Notification;
|
||||
use post_authors::PostAuthor;
|
||||
use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
use schema::users;
|
||||
use search::Searcher;
|
||||
use timeline::Timeline;
|
||||
use {ap_url, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE};
|
||||
use {ap_url, Connection, Error, PlumeRocket, Result};
|
||||
|
||||
pub type CustomPerson = CustomObject<ApSignature, Person>;
|
||||
|
||||
pub enum Role {
|
||||
Admin = 0,
|
||||
Moderator = 1,
|
||||
Normal = 2,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
@@ -66,6 +58,7 @@ pub struct User {
|
||||
pub display_name: String,
|
||||
pub outbox_url: String,
|
||||
pub inbox_url: String,
|
||||
pub is_admin: bool,
|
||||
pub summary: String,
|
||||
pub email: Option<String>,
|
||||
pub hashed_password: Option<String>,
|
||||
@@ -80,12 +73,6 @@ pub struct User {
|
||||
pub last_fetched_date: NaiveDateTime,
|
||||
pub fqn: String,
|
||||
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)]
|
||||
@@ -95,6 +82,7 @@ pub struct NewUser {
|
||||
pub display_name: String,
|
||||
pub outbox_url: String,
|
||||
pub inbox_url: String,
|
||||
pub is_admin: bool,
|
||||
pub summary: String,
|
||||
pub email: Option<String>,
|
||||
pub hashed_password: Option<String>,
|
||||
@@ -106,28 +94,50 @@ pub struct NewUser {
|
||||
pub followers_endpoint: String,
|
||||
pub avatar_id: Option<i32>,
|
||||
pub summary_html: SafeString,
|
||||
pub role: i32,
|
||||
pub fqn: String,
|
||||
}
|
||||
|
||||
pub const AUTH_COOKIE: &str = "user_id";
|
||||
const USER_PREFIX: &str = "@";
|
||||
|
||||
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);
|
||||
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_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>> {
|
||||
users::table
|
||||
.filter(users::instance_id.eq_any(users::table.select(users::instance_id).distinct()))
|
||||
@@ -166,10 +176,6 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
for notif in Notification::find_followed_by(conn, self)? {
|
||||
notif.delete(conn)?
|
||||
}
|
||||
|
||||
diesel::delete(self)
|
||||
.execute(conn)
|
||||
.map(|_| ())
|
||||
@@ -180,14 +186,46 @@ impl User {
|
||||
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)
|
||||
.set(users::role.eq(new_role as i32))
|
||||
.set(users::is_admin.eq(true))
|
||||
.execute(conn)
|
||||
.map(|_| ())
|
||||
.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> {
|
||||
users::table
|
||||
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||
@@ -199,8 +237,10 @@ impl User {
|
||||
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
|
||||
let from_db = users::table
|
||||
.filter(users::fqn.eq(fqn))
|
||||
.first(&*c.conn)
|
||||
.optional()?;
|
||||
.limit(1)
|
||||
.load::<User>(&*c.conn)?
|
||||
.into_iter()
|
||||
.next();
|
||||
if let Some(from_db) = from_db {
|
||||
Ok(from_db)
|
||||
} else {
|
||||
@@ -320,77 +360,16 @@ impl User {
|
||||
.load::<User>(conn)
|
||||
.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") {
|
||||
Some(x) => Some(x.as_str().unwrap().to_owned()),
|
||||
None => None,
|
||||
};
|
||||
Ok((items, next))
|
||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
||||
let acts = self.get_activities(conn)?;
|
||||
let n_acts = acts.len();
|
||||
let mut coll = OrderedCollection::default();
|
||||
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>> {
|
||||
let mut res = ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
@@ -408,32 +387,12 @@ impl User {
|
||||
.send()?;
|
||||
let text = &res.text()?;
|
||||
let json: serde_json::Value = serde_json::from_str(text)?;
|
||||
if let Some(first) = json.get("first") {
|
||||
let mut items: Vec<T> = Vec::new();
|
||||
let mut next = first.as_str().unwrap().to_owned();
|
||||
while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) {
|
||||
if page.is_empty() {
|
||||
break;
|
||||
}
|
||||
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>>())
|
||||
}
|
||||
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>> {
|
||||
@@ -460,31 +419,14 @@ impl User {
|
||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||
.collect::<Vec<String>>())
|
||||
}
|
||||
fn get_activities_count(&self, conn: &Connection) -> i64 {
|
||||
use schema::post_authors;
|
||||
use schema::posts;
|
||||
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
|
||||
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>> {
|
||||
|
||||
fn get_activities(&self, conn: &Connection) -> Result<Vec<serde_json::Value>> {
|
||||
use schema::post_authors;
|
||||
use schema::posts;
|
||||
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
|
||||
let posts = posts::table
|
||||
.filter(posts::published.eq(true))
|
||||
.filter(posts::id.eq_any(posts_by_self))
|
||||
.order(posts::creation_date.desc())
|
||||
.offset(min.into())
|
||||
.limit((max - min).into())
|
||||
.load::<Post>(conn)?;
|
||||
Ok(posts
|
||||
.into_iter()
|
||||
@@ -726,7 +668,7 @@ impl User {
|
||||
pub fn avatar_url(&self, conn: &Connection) -> String {
|
||||
self.avatar_id
|
||||
.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> {
|
||||
@@ -765,7 +707,7 @@ impl User {
|
||||
mime_type: None,
|
||||
href: None,
|
||||
template: Some(format!(
|
||||
"https://{}/remote_interact?{{uri}}",
|
||||
"{}/remote_interact?{{uri}}",
|
||||
self.get_instance(conn)?.public_domain
|
||||
)),
|
||||
},
|
||||
@@ -851,12 +793,6 @@ impl FromId<PlumeRocket> for User {
|
||||
return Err(Error::InvalidValue);
|
||||
}
|
||||
|
||||
let fqn = if instance.local {
|
||||
username.clone()
|
||||
} else {
|
||||
format!("{}@{}", username, instance.public_domain)
|
||||
};
|
||||
|
||||
let user = User::insert(
|
||||
&c.conn,
|
||||
NewUser {
|
||||
@@ -868,7 +804,7 @@ impl FromId<PlumeRocket> for User {
|
||||
username,
|
||||
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||
role: 2,
|
||||
is_admin: false,
|
||||
summary: acct
|
||||
.object
|
||||
.object_props
|
||||
@@ -897,7 +833,6 @@ impl FromId<PlumeRocket> for User {
|
||||
.and_then(|e| e.shared_inbox_string())
|
||||
.ok(),
|
||||
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
|
||||
fqn,
|
||||
avatar_id: None,
|
||||
},
|
||||
)?;
|
||||
@@ -985,41 +920,29 @@ impl NewUser {
|
||||
conn: &Connection,
|
||||
username: String,
|
||||
display_name: String,
|
||||
role: Role,
|
||||
is_admin: bool,
|
||||
summary: &str,
|
||||
email: String,
|
||||
password: String,
|
||||
) -> Result<User> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
let instance = Instance::get_local()?;
|
||||
|
||||
let res = User::insert(
|
||||
User::insert(
|
||||
conn,
|
||||
NewUser {
|
||||
username: username.clone(),
|
||||
username,
|
||||
display_name,
|
||||
role: role as i32,
|
||||
is_admin,
|
||||
summary: summary.to_owned(),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, None, false, None).0),
|
||||
email: Some(email),
|
||||
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))?,
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
outbox_url: instance.compute_box(USER_PREFIX, &username, "outbox"),
|
||||
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,
|
||||
..NewUser::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
// create default timeline
|
||||
Timeline::new_for_user(conn, res.id, "My feed".into(), "followed".into())?;
|
||||
|
||||
Ok(res)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,7 +961,7 @@ pub(crate) mod tests {
|
||||
conn,
|
||||
"admin".to_owned(),
|
||||
"The admin".to_owned(),
|
||||
Role::Admin,
|
||||
true,
|
||||
"Hello there, I'm the admin",
|
||||
"admin@example.com".to_owned(),
|
||||
"invalid_admin_password".to_owned(),
|
||||
@@ -1048,7 +971,7 @@ pub(crate) mod tests {
|
||||
conn,
|
||||
"user".to_owned(),
|
||||
"Some user".to_owned(),
|
||||
Role::Normal,
|
||||
false,
|
||||
"Hello there, I'm no one",
|
||||
"user@example.com".to_owned(),
|
||||
"invalid_user_password".to_owned(),
|
||||
@@ -1058,7 +981,7 @@ pub(crate) mod tests {
|
||||
conn,
|
||||
"other".to_owned(),
|
||||
"Another user".to_owned(),
|
||||
Role::Normal,
|
||||
false,
|
||||
"Hello there, I'm someone else",
|
||||
"other@example.com".to_owned(),
|
||||
"invalid_other_password".to_owned(),
|
||||
@@ -1077,12 +1000,13 @@ pub(crate) mod tests {
|
||||
conn,
|
||||
"test".to_owned(),
|
||||
"test user".to_owned(),
|
||||
Role::Normal,
|
||||
false,
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_name(conn, "test", Instance::get_local().unwrap().id)
|
||||
@@ -1110,6 +1034,7 @@ pub(crate) mod tests {
|
||||
.unwrap()
|
||||
.id
|
||||
);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -1123,6 +1048,7 @@ pub(crate) mod tests {
|
||||
assert!(User::get(conn, inserted[0].id).is_ok());
|
||||
inserted[0].delete(conn, &get_searcher()).unwrap();
|
||||
assert!(User::get(conn, inserted[0].id).is_err());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -1139,12 +1065,34 @@ pub(crate) mod tests {
|
||||
local_inst
|
||||
.main_admin(conn)
|
||||
.unwrap()
|
||||
.set_role(conn, Role::Normal)
|
||||
.revoke_admin_rights(conn)
|
||||
.unwrap();
|
||||
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);
|
||||
|
||||
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(())
|
||||
});
|
||||
}
|
||||
@@ -1158,7 +1106,7 @@ pub(crate) mod tests {
|
||||
conn,
|
||||
"test".to_owned(),
|
||||
"test user".to_owned(),
|
||||
Role::Normal,
|
||||
false,
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
@@ -1167,6 +1115,7 @@ pub(crate) mod tests {
|
||||
|
||||
assert!(test_user.auth("test_password"));
|
||||
assert!(!test_user.auth("other_password"));
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -1196,6 +1145,7 @@ pub(crate) mod tests {
|
||||
.len() as i64,
|
||||
User::count_local(conn).unwrap()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
@@ -1223,6 +1173,7 @@ pub(crate) mod tests {
|
||||
assert_eq!(user.avatar_url(conn), users[0].avatar_url(conn));
|
||||
assert_eq!(user.fqn, users[0].fqn);
|
||||
assert_eq!(user.summary_html, users[0].summary_html);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
nightly-2019-12-17
|
||||
nightly-2019-03-23
|
||||
|
||||
@@ -5,7 +5,6 @@ export ROCKET_SECRET_KEY="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
|
||||
plm migration run
|
||||
plm migration redo
|
||||
plm search init
|
||||
plm instance new -d plume-test.local -n plume-test
|
||||
plm users new -n admin -N 'Admin' -e 'email@exemple.com' -p 'password'
|
||||
|
||||
|
||||
Vendored
-53
@@ -1,53 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
db_type="$(snapctl get db.type)"
|
||||
db_url="$(snapctl get db.url)"
|
||||
if [ "${db_type}" = "sqlite" ]
|
||||
then
|
||||
if [ -n "${db_url}" ]
|
||||
then
|
||||
echo "sqlite backend does not use db.url key"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${db_type}" = "postgres" ]
|
||||
then
|
||||
if [ -z "${db_url}" ]
|
||||
then
|
||||
echo "postgres backend requires db.url to be set"
|
||||
exit 1
|
||||
fi
|
||||
elif [ -n "${db_type}" ]
|
||||
then
|
||||
echo "Invalid db.type: " ${db_type}
|
||||
exit 1
|
||||
fi
|
||||
|
||||
base_url="$(snapctl get base-url)"
|
||||
enabled="$(snapctl get enabled)"
|
||||
|
||||
if [ -n ${enabled} -a \( "${enabled}" != "true" -a "${enabled}" != "false" \) ]
|
||||
then
|
||||
echo "Invalid 'enabled' setting: ${enabled}. Valid values are 'true' or 'false'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n ${base_url} -a \( -z "${enabled}" -o "${enabled}" = "false" \) ]
|
||||
then
|
||||
echo "All required configuration available."
|
||||
echo "Plume can now be enabled by setting 'snap set plume enabled=true' and restarting the service \
|
||||
with 'snap restart plume'"
|
||||
fi
|
||||
|
||||
if [ ! -d ${SNAP_DATA}/media ]
|
||||
then
|
||||
mkdir ${SNAP_DATA}/media
|
||||
fi
|
||||
|
||||
if [ "${enabled}" = "true" -a ! -e ${SNAP_COMMON}/initial-migrations-run ]
|
||||
then
|
||||
cd ${SNAP}
|
||||
exec ./set-environment bin/plm migration run --path ${SNAP_DATA}
|
||||
touch ${SNAP_COMMON}/initial-migrations-run
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
openssl rand -base64 32 > ${SNAP_COMMON}/rocket-secret-key
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd ${SNAP}
|
||||
exec ./set-environment bin/plm migration run --path ${SNAP_DATA}
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
enabled="$(snapctl get enabled)"
|
||||
if [ -z "${enabled}" -o "${enabled}" != "true" ]
|
||||
then
|
||||
echo "Plume not yet enabled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export BASE_URL="$(snapctl get base-url)"
|
||||
database_type="$(snapctl get db.type)"
|
||||
|
||||
if [ z"${database_type}" = z"sqlite" ]
|
||||
then
|
||||
export DATABASE_URL=${SNAP_DATA}/plume.db
|
||||
export MIGRATION_DIR=migrations/sqlite
|
||||
else
|
||||
# Must be postgres, so must have set db.url
|
||||
export DATABASE_URL="$(snapctl get db.url)"
|
||||
export MIGRATION_DIRECTORY=migrations/postgres
|
||||
fi
|
||||
|
||||
ROCKET_ADDRESS="$(snapctl get listen.address)"
|
||||
ROCKET_PORT="$(snapctl get listen.port)"
|
||||
export ROCKET_SECRET_KEY="$(cat ${SNAP_COMMON}/rocket-secret-key)"
|
||||
export SEARCH_INDEX="${SNAP_DATA}/search_index"
|
||||
export MEDIA_UPLOAD_DIRECTORY="${SNAP_DATA}/media"
|
||||
|
||||
cd ${SNAP}
|
||||
exec $@
|
||||
@@ -1,60 +0,0 @@
|
||||
name: plume
|
||||
base: core18
|
||||
adopt-info: plume
|
||||
summary: Multi-user blogging platform, federated over ActivityPub
|
||||
description: |
|
||||
Plume is a federated blogging platform, featuring:
|
||||
* A blog-centric approach: you can create as much blogs as you want with your account, to keep your different publications separated.
|
||||
* Media management: you can upload pictures to illustrate your articles, but also audio files if you host a podcast, and manage them all from Plume.
|
||||
* Federation: Plume is part of a network of interconnected websites called the Fediverse. Each of these websites (often called instances) have their own rules and thematics, but they can all communicate with each other.
|
||||
* Collaborative writing: invite other people to your blogs, and write articles together.
|
||||
grade: devel # must be 'stable' to release into candidate/stable channels
|
||||
confinement: strict
|
||||
|
||||
apps:
|
||||
plume:
|
||||
daemon: simple
|
||||
command: set-environment bin/plume
|
||||
plugs:
|
||||
- network
|
||||
- network-bind
|
||||
plm:
|
||||
command: set-environment bin/plm
|
||||
|
||||
parts:
|
||||
plume:
|
||||
plugin: rust
|
||||
source: .
|
||||
rust-revision: nightly-2019-03-23
|
||||
build-packages:
|
||||
- libssl-dev
|
||||
- pkg-config
|
||||
- libsqlite3-dev
|
||||
- gettext
|
||||
- on arm64,armhf,ppc64el,s390x:
|
||||
- lld-8
|
||||
override-build: |
|
||||
snapcraftctl set-version $(git describe --tags)
|
||||
export PATH=$PATH:$HOME/.cargo/bin
|
||||
rustup install stable
|
||||
cargo +stable install --force cargo-web
|
||||
|
||||
# Only Tier 1 Rust platforms get rust-lld
|
||||
# On the others (arm64, armhf, powerpc64, s390x) fall back to using
|
||||
# the system LLD we've installed earlier.
|
||||
case ${SNAPCRAFT_ARCH_TRIPLE} in \
|
||||
aarch64-linux-gnu|arm-linux-gnueabihf|powerpc64-linux-gnu|s390x-linux-gnu) \
|
||||
RUSTFLAGS="-C linker=lld-8" cargo web deploy -p plume-front --release \
|
||||
;; \
|
||||
*) \
|
||||
cargo web deploy -p plume-front --release \
|
||||
;; \
|
||||
esac
|
||||
|
||||
cargo install --force --no-default-features --features sqlite --path . --root ${SNAPCRAFT_PART_INSTALL}
|
||||
cargo install --force --no-default-features --features sqlite --path plume-cli --root ${SNAPCRAFT_PART_INSTALL}
|
||||
cp -a assets migrations static target translations ${SNAPCRAFT_PART_INSTALL}
|
||||
cp snap/local/set-environment ${SNAPCRAFT_PART_INSTALL}
|
||||
stage-packages:
|
||||
- openssl
|
||||
- libsqlite3-0
|
||||
+1
-3
@@ -7,7 +7,7 @@ use plume_api::posts::*;
|
||||
use plume_common::{activity_pub::broadcast, utils::md_to_html};
|
||||
use plume_models::{
|
||||
blogs::Blog, db_conn::DbConn, instance::Instance, medias::Media, mentions::*, post_authors::*,
|
||||
posts::*, safe_string::SafeString, tags::*, timeline::*, users::User, Error, PlumeRocket,
|
||||
posts::*, safe_string::SafeString, tags::*, users::User, Error, PlumeRocket,
|
||||
};
|
||||
|
||||
#[get("/posts/<id>")]
|
||||
@@ -204,8 +204,6 @@ pub fn create(
|
||||
worker.execute(move || broadcast(&author, act, dest));
|
||||
}
|
||||
|
||||
Timeline::add_to_all_timelines(&rockets, &post, Kind::Original)?;
|
||||
|
||||
Ok(Json(PostData {
|
||||
authors: post.get_authors(conn)?.into_iter().map(|a| a.fqn).collect(),
|
||||
creation_date: post.creation_date.format("%Y-%m-%d").to_string(),
|
||||
|
||||
+8
-35
@@ -5,7 +5,6 @@ extern crate activitypub;
|
||||
extern crate askama_escape;
|
||||
extern crate atom_syndication;
|
||||
extern crate chrono;
|
||||
extern crate clap;
|
||||
extern crate colored;
|
||||
extern crate ctrlc;
|
||||
extern crate diesel;
|
||||
@@ -39,7 +38,6 @@ extern crate validator;
|
||||
extern crate validator_derive;
|
||||
extern crate webfinger;
|
||||
|
||||
use clap::App;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_models::{
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
@@ -75,11 +73,7 @@ compile_i18n!();
|
||||
|
||||
/// Initializes a database pool.
|
||||
fn init_pool() -> Option<DbPool> {
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => println!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
dotenv::dotenv().unwrap();
|
||||
|
||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||
let pool = DbPool::builder()
|
||||
@@ -91,19 +85,6 @@ fn init_pool() -> Option<DbPool> {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new("Plume")
|
||||
.bin_name("plume")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Plume backend server")
|
||||
.after_help(
|
||||
r#"
|
||||
The plume command should be run inside the directory
|
||||
containing the `.env` configuration file and `static` directory.
|
||||
See https://docs.joinplu.me/installation/config
|
||||
and https://docs.joinplu.me/installation/init for more info.
|
||||
"#,
|
||||
)
|
||||
.get_matches();
|
||||
let dbpool = init_pool().expect("main: database pool initialization error");
|
||||
if IMPORTED_MIGRATIONS
|
||||
.is_pending(&dbpool.get().unwrap())
|
||||
@@ -182,7 +163,6 @@ Then try to restart Plume
|
||||
routes::blogs::details,
|
||||
routes::blogs::activity_details,
|
||||
routes::blogs::outbox,
|
||||
routes::blogs::outbox_page,
|
||||
routes::blogs::new,
|
||||
routes::blogs::new_auth,
|
||||
routes::blogs::create,
|
||||
@@ -194,11 +174,13 @@ Then try to restart Plume
|
||||
routes::comments::delete,
|
||||
routes::comments::activity_pub,
|
||||
routes::instance::index,
|
||||
routes::instance::local,
|
||||
routes::instance::feed,
|
||||
routes::instance::federated,
|
||||
routes::instance::admin,
|
||||
routes::instance::admin_mod,
|
||||
routes::instance::admin_instances,
|
||||
routes::instance::admin_users,
|
||||
routes::instance::edit_users,
|
||||
routes::instance::ban,
|
||||
routes::instance::toggle_block,
|
||||
routes::instance::update_settings,
|
||||
routes::instance::shared_inbox,
|
||||
@@ -237,17 +219,9 @@ Then try to restart Plume
|
||||
routes::session::password_reset_request,
|
||||
routes::session::password_reset_form,
|
||||
routes::session::password_reset,
|
||||
routes::theme_files,
|
||||
routes::plume_static_files,
|
||||
routes::static_files,
|
||||
routes::plume_media_files,
|
||||
routes::tags::tag,
|
||||
routes::timelines::details,
|
||||
routes::timelines::new,
|
||||
routes::timelines::create,
|
||||
routes::timelines::edit,
|
||||
routes::timelines::update,
|
||||
routes::timelines::delete,
|
||||
routes::user::me,
|
||||
routes::user::details,
|
||||
routes::user::dashboard,
|
||||
@@ -263,7 +237,6 @@ Then try to restart Plume
|
||||
routes::user::follow_auth,
|
||||
routes::user::activity_details,
|
||||
routes::user::outbox,
|
||||
routes::user::outbox_page,
|
||||
routes::user::inbox,
|
||||
routes::user::ap_followers,
|
||||
routes::user::new,
|
||||
@@ -307,17 +280,17 @@ Then try to restart Plume
|
||||
(
|
||||
"/inbox".to_owned(),
|
||||
"/inbox".to_owned(),
|
||||
Some(rocket::http::Method::Post),
|
||||
rocket::http::Method::Post,
|
||||
),
|
||||
(
|
||||
"/@/<name>/inbox".to_owned(),
|
||||
"/@/<name>/inbox".to_owned(),
|
||||
Some(rocket::http::Method::Post),
|
||||
rocket::http::Method::Post,
|
||||
),
|
||||
(
|
||||
"/api/<path..>".to_owned(),
|
||||
"/api/<path..>".to_owned(),
|
||||
Some(rocket::http::Method::Post),
|
||||
rocket::http::Method::Post,
|
||||
),
|
||||
])
|
||||
.finalize()
|
||||
|
||||
+2
-14
@@ -1,4 +1,4 @@
|
||||
use activitypub::collection::{OrderedCollection, OrderedCollectionPage};
|
||||
use activitypub::collection::OrderedCollection;
|
||||
use atom_syndication::{Entry, FeedBuilder};
|
||||
use diesel::SaveChangesDsl;
|
||||
use rocket::{
|
||||
@@ -181,7 +181,6 @@ pub struct EditForm {
|
||||
pub summary: String,
|
||||
pub icon: Option<i32>,
|
||||
pub banner: Option<i32>,
|
||||
pub theme: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/~/<name>/edit")]
|
||||
@@ -208,7 +207,6 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
summary: blog.summary.clone(),
|
||||
icon: blog.icon_id,
|
||||
banner: blog.banner_id,
|
||||
theme: blog.theme.clone(),
|
||||
},
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
@@ -320,7 +318,6 @@ pub fn update(
|
||||
);
|
||||
blog.icon_id = form.icon;
|
||||
blog.banner_id = form.banner;
|
||||
blog.theme = form.theme.clone();
|
||||
blog.save_changes::<Blog>(&*conn)
|
||||
.expect("Couldn't save blog changes");
|
||||
Ok(Flash::success(
|
||||
@@ -347,16 +344,7 @@ pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<Order
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||
Some(blog.outbox(&*rockets.conn).ok()?)
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
#[get("/~/<name>/outbox?<page>")]
|
||||
pub fn outbox_page(
|
||||
name: String,
|
||||
page: Page,
|
||||
rockets: PlumeRocket,
|
||||
) -> Option<ActivityStream<OrderedCollectionPage>> {
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||
Some(blog.outbox_page(&*rockets.conn, page.limits()).ok()?)
|
||||
}
|
||||
|
||||
#[get("/~/<name>/atom.xml")]
|
||||
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||
|
||||
+71
-170
@@ -1,28 +1,17 @@
|
||||
use rocket::{
|
||||
request::{FormItems, FromForm, LenientForm},
|
||||
request::LenientForm,
|
||||
response::{status, Flash, Redirect},
|
||||
};
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket_i18n::I18n;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use serde_json;
|
||||
use std::str::FromStr;
|
||||
use validator::{Validate, ValidationErrors};
|
||||
|
||||
use inbox;
|
||||
use plume_common::activity_pub::{broadcast, inbox::FromId};
|
||||
use plume_models::{
|
||||
admin::*,
|
||||
comments::Comment,
|
||||
db_conn::DbConn,
|
||||
headers::Headers,
|
||||
instance::*,
|
||||
posts::Post,
|
||||
safe_string::SafeString,
|
||||
search::Searcher,
|
||||
timeline::Timeline,
|
||||
users::{Role, User},
|
||||
Connection, Error, PlumeRocket, CONFIG,
|
||||
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
|
||||
safe_string::SafeString, users::User, Error, PlumeRocket, CONFIG,
|
||||
};
|
||||
use routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
@@ -31,23 +20,64 @@ use template_utils::{IntoContext, Ructe};
|
||||
pub fn index(rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let inst = Instance::get_local()?;
|
||||
let timelines = Timeline::list_all_for_user(&conn, rockets.user.clone().map(|u| u.id))?
|
||||
.into_iter()
|
||||
.filter_map(|t| {
|
||||
if let Ok(latest) = t.get_latest(&conn, 12) {
|
||||
Some((t, latest))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let federated = Post::get_recents_page(conn, Page::default().limits())?;
|
||||
let local = Post::get_instance_page(conn, inst.id, Page::default().limits())?;
|
||||
let user_feed = rockets.user.clone().and_then(|user| {
|
||||
let followed = user.get_followed(conn).ok()?;
|
||||
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
|
||||
in_feed.push(user.id);
|
||||
Post::user_feed_page(conn, in_feed, Page::default().limits()).ok()
|
||||
});
|
||||
|
||||
Ok(render!(instance::index(
|
||||
&rockets.to_context(),
|
||||
inst,
|
||||
User::count_local(conn)?,
|
||||
Post::count_local(conn)?,
|
||||
timelines
|
||||
local,
|
||||
federated,
|
||||
user_feed
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/local?<page>")]
|
||||
pub fn local(page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let instance = Instance::get_local()?;
|
||||
let articles = Post::get_instance_page(&*rockets.conn, instance.id, page.limits())?;
|
||||
Ok(render!(instance::local(
|
||||
&rockets.to_context(),
|
||||
instance,
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/feed?<page>")]
|
||||
pub fn feed(user: User, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let followed = user.get_followed(&*rockets.conn)?;
|
||||
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
|
||||
in_feed.push(user.id);
|
||||
let articles = Post::user_feed_page(&*rockets.conn, in_feed, page.limits())?;
|
||||
Ok(render!(instance::feed(
|
||||
&rockets.to_context(),
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/federated?<page>")]
|
||||
pub fn federated(page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let articles = Post::get_recents_page(&*rockets.conn, page.limits())?;
|
||||
Ok(render!(instance::federated(
|
||||
&rockets.to_context(),
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*rockets.conn)? as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -68,11 +98,6 @@ pub fn admin(_admin: Admin, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/admin", rank = 2)]
|
||||
pub fn admin_mod(_mod: Moderator, rockets: PlumeRocket) -> Ructe {
|
||||
render!(instance::admin_mod(&rockets.to_context()))
|
||||
}
|
||||
|
||||
#[derive(Clone, FromForm, Validate)]
|
||||
pub struct InstanceSettingsForm {
|
||||
#[validate(length(min = "1"))]
|
||||
@@ -111,7 +136,6 @@ pub fn update_settings(
|
||||
form.open_registrations,
|
||||
form.short_description.clone(),
|
||||
form.long_description.clone(),
|
||||
form.default_license.clone(),
|
||||
)
|
||||
.expect("instance::update_settings: save error");
|
||||
Flash::success(
|
||||
@@ -124,7 +148,7 @@ pub fn update_settings(
|
||||
|
||||
#[get("/admin/instances?<page>")]
|
||||
pub fn admin_instances(
|
||||
_mod: Moderator,
|
||||
_admin: Admin,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
@@ -141,7 +165,7 @@ pub fn admin_instances(
|
||||
|
||||
#[post("/admin/instances/<id>/block")]
|
||||
pub fn toggle_block(
|
||||
_mod: Moderator,
|
||||
_admin: Admin,
|
||||
conn: DbConn,
|
||||
id: i32,
|
||||
intl: I18n,
|
||||
@@ -162,7 +186,7 @@ pub fn toggle_block(
|
||||
|
||||
#[get("/admin/users?<page>")]
|
||||
pub fn admin_users(
|
||||
_mod: Moderator,
|
||||
_admin: Admin,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
@@ -175,150 +199,27 @@ pub fn admin_users(
|
||||
)))
|
||||
}
|
||||
|
||||
/// A structure to handle forms that are a list of items on which actions are applied.
|
||||
///
|
||||
/// This is for instance the case of the user list in the administration.
|
||||
pub struct MultiAction<T>
|
||||
where
|
||||
T: FromStr,
|
||||
{
|
||||
ids: Vec<i32>,
|
||||
action: T,
|
||||
}
|
||||
|
||||
impl<'f, T> FromForm<'f> for MultiAction<T>
|
||||
where
|
||||
T: FromStr,
|
||||
{
|
||||
type Error = ();
|
||||
|
||||
fn from_form(items: &mut FormItems, _strict: bool) -> Result<Self, Self::Error> {
|
||||
let (ids, act) = items.fold((vec![], None), |(mut ids, act), item| {
|
||||
let (name, val) = item.key_value_decoded();
|
||||
|
||||
if name == "action" {
|
||||
(ids, T::from_str(&val).ok())
|
||||
} else if let Ok(id) = name.parse::<i32>() {
|
||||
ids.push(id);
|
||||
(ids, act)
|
||||
} else {
|
||||
(ids, act)
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(act) = act {
|
||||
Ok(MultiAction { ids, action: act })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UserActions {
|
||||
Admin,
|
||||
RevokeAdmin,
|
||||
Moderator,
|
||||
RevokeModerator,
|
||||
Ban,
|
||||
}
|
||||
|
||||
impl FromStr for UserActions {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"admin" => Ok(UserActions::Admin),
|
||||
"un-admin" => Ok(UserActions::RevokeAdmin),
|
||||
"moderator" => Ok(UserActions::Moderator),
|
||||
"un-moderator" => Ok(UserActions::RevokeModerator),
|
||||
"ban" => Ok(UserActions::Ban),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/admin/users/edit", data = "<form>")]
|
||||
pub fn edit_users(
|
||||
moderator: Moderator,
|
||||
form: LenientForm<MultiAction<UserActions>>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
// you can't change your own rights
|
||||
if form.ids.contains(&moderator.0.id) {
|
||||
return Ok(Flash::error(
|
||||
Redirect::to(uri!(admin_users: page = _)),
|
||||
i18n!(rockets.intl.catalog, "You can't change your own rights."),
|
||||
));
|
||||
}
|
||||
|
||||
// moderators can't grant or revoke admin rights
|
||||
if !moderator.0.is_admin() {
|
||||
match form.action {
|
||||
UserActions::Admin | UserActions::RevokeAdmin => {
|
||||
return Ok(Flash::error(
|
||||
Redirect::to(uri!(admin_users: page = _)),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You are not allowed to take this action."
|
||||
),
|
||||
))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let conn = &rockets.conn;
|
||||
let searcher = &*rockets.searcher;
|
||||
let worker = &*rockets.worker;
|
||||
match form.action {
|
||||
UserActions::Admin => {
|
||||
for u in form.ids.clone() {
|
||||
User::get(conn, u)?.set_role(conn, Role::Admin)?;
|
||||
}
|
||||
}
|
||||
UserActions::Moderator => {
|
||||
for u in form.ids.clone() {
|
||||
User::get(conn, u)?.set_role(conn, Role::Moderator)?;
|
||||
}
|
||||
}
|
||||
UserActions::RevokeAdmin | UserActions::RevokeModerator => {
|
||||
for u in form.ids.clone() {
|
||||
User::get(conn, u)?.set_role(conn, Role::Normal)?;
|
||||
}
|
||||
}
|
||||
UserActions::Ban => {
|
||||
for u in form.ids.clone() {
|
||||
ban(u, conn, searcher, worker)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(admin_users: page = _)),
|
||||
i18n!(rockets.intl.catalog, "Done."),
|
||||
))
|
||||
}
|
||||
|
||||
fn ban(
|
||||
id: i32,
|
||||
conn: &Connection,
|
||||
searcher: &Searcher,
|
||||
worker: &ScheduledThreadPool,
|
||||
) -> Result<(), ErrorPage> {
|
||||
let u = User::get(&*conn, id)?;
|
||||
u.delete(&*conn, searcher)?;
|
||||
#[post("/admin/users/<id>/ban")]
|
||||
pub fn ban(_admin: Admin, id: i32, rockets: PlumeRocket) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
let u = User::get(&*rockets.conn, id)?;
|
||||
u.delete(&*rockets.conn, &rockets.searcher)?;
|
||||
|
||||
if Instance::get_local()
|
||||
.map(|i| u.instance_id == i.id)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let target = User::one_by_instance(&*conn)?;
|
||||
let delete_act = u.delete_activity(&*conn)?;
|
||||
let target = User::one_by_instance(&*rockets.conn)?;
|
||||
let delete_act = u.delete_activity(&*rockets.conn)?;
|
||||
let u_clone = u.clone();
|
||||
worker.execute(move || broadcast(&u_clone, delete_act, target));
|
||||
rockets
|
||||
.worker
|
||||
.execute(move || broadcast(&u_clone, delete_act, target));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(admin_users: page = _)),
|
||||
i18n!(rockets.intl.catalog, "{} has been banned."; u.name()),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/inbox", data = "<data>")]
|
||||
|
||||
+1
-3
@@ -4,7 +4,7 @@ use rocket_i18n::I18n;
|
||||
use plume_common::activity_pub::broadcast;
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blogs::Blog, inbox::inbox, likes, posts::Post, timeline::*, users::User, Error, PlumeRocket,
|
||||
blogs::Blog, inbox::inbox, likes, posts::Post, users::User, Error, PlumeRocket,
|
||||
};
|
||||
use routes::errors::ErrorPage;
|
||||
|
||||
@@ -23,8 +23,6 @@ pub fn create(
|
||||
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post, &user))?;
|
||||
like.notify(&*conn)?;
|
||||
|
||||
Timeline::add_to_all_timelines(&rockets, &post, Kind::Like(&user))?;
|
||||
|
||||
let dest = User::one_by_instance(&*conn)?;
|
||||
let act = like.to_activity(&*conn)?;
|
||||
rockets.worker.execute(move || broadcast(&user, act, dest));
|
||||
|
||||
@@ -3,7 +3,7 @@ use multipart::server::{
|
||||
save::{SaveResult, SavedData},
|
||||
Multipart,
|
||||
};
|
||||
use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket, CONFIG};
|
||||
use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket};
|
||||
use rocket::{
|
||||
http::ContentType,
|
||||
response::{status, Flash, Redirect},
|
||||
@@ -72,12 +72,7 @@ pub fn upload(
|
||||
.map(|ext| format!(".{}", ext))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let dest = format!(
|
||||
"{}/{}{}",
|
||||
CONFIG.media_directory,
|
||||
GUID::rand().to_string(),
|
||||
ext
|
||||
);
|
||||
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
|
||||
|
||||
match fields["file"][0].data {
|
||||
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
|
||||
|
||||
+8
-53
@@ -1,23 +1,22 @@
|
||||
#![warn(clippy::too_many_arguments)]
|
||||
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
||||
use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE};
|
||||
use rocket::{
|
||||
http::{
|
||||
hyper::header::{CacheControl, CacheDirective, ETag, EntityTag},
|
||||
hyper::header::{CacheControl, CacheDirective},
|
||||
uri::{FromUriParam, Query},
|
||||
RawStr, Status,
|
||||
},
|
||||
request::{self, FromFormValue, FromRequest, Request},
|
||||
response::{self, Flash, NamedFile, Redirect, Responder, Response},
|
||||
response::{Flash, NamedFile, Redirect},
|
||||
Outcome,
|
||||
};
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::Hasher,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
use template_utils::Ructe;
|
||||
|
||||
use plume_models::{posts::Post, Connection};
|
||||
|
||||
const ITEMS_PER_PAGE: i32 = 12;
|
||||
|
||||
/// Special return type used for routes that "cannot fail", and instead
|
||||
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -159,7 +158,6 @@ pub mod reshares;
|
||||
pub mod search;
|
||||
pub mod session;
|
||||
pub mod tags;
|
||||
pub mod timelines;
|
||||
pub mod user;
|
||||
pub mod well_known;
|
||||
|
||||
@@ -170,54 +168,11 @@ pub struct CachedFile {
|
||||
cache_control: CacheControl,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ThemeFile(NamedFile);
|
||||
|
||||
impl<'r> Responder<'r> for ThemeFile {
|
||||
fn respond_to(self, r: &Request) -> response::Result<'r> {
|
||||
let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(&contents);
|
||||
let etag = format!("{:x}", hasher.finish());
|
||||
|
||||
if r.headers()
|
||||
.get("If-None-Match")
|
||||
.any(|s| s[1..s.len() - 1] == etag)
|
||||
{
|
||||
Response::build()
|
||||
.status(Status::NotModified)
|
||||
.header(ETag(EntityTag::strong(etag)))
|
||||
.ok()
|
||||
} else {
|
||||
Response::build()
|
||||
.merge(self.0.respond_to(r)?)
|
||||
.header(ETag(EntityTag::strong(etag)))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/static/cached/<_build_id>/css/<file..>", rank = 1)]
|
||||
pub fn theme_files(file: PathBuf, _build_id: &RawStr) -> Option<ThemeFile> {
|
||||
NamedFile::open(Path::new("static/css/").join(file))
|
||||
.ok()
|
||||
.map(ThemeFile)
|
||||
}
|
||||
|
||||
#[get("/static/cached/<_build_id>/<file..>", rank = 2)]
|
||||
pub fn plume_static_files(file: PathBuf, _build_id: &RawStr) -> Option<CachedFile> {
|
||||
static_files(file)
|
||||
}
|
||||
#[get("/static/media/<file..>")]
|
||||
pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
|
||||
NamedFile::open(Path::new(&CONFIG.media_directory).join(file))
|
||||
.ok()
|
||||
.map(|f| CachedFile {
|
||||
inner: f,
|
||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/static/<file..>", rank = 3)]
|
||||
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
|
||||
NamedFile::open(Path::new("static/").join(file))
|
||||
|
||||
+4
-9
@@ -23,7 +23,6 @@ use plume_models::{
|
||||
posts::*,
|
||||
safe_string::SafeString,
|
||||
tags::*,
|
||||
timeline::*,
|
||||
users::User,
|
||||
Error, PlumeRocket,
|
||||
};
|
||||
@@ -298,7 +297,7 @@ pub fn update(
|
||||
post.license = form.license.clone();
|
||||
post.cover_id = form.cover;
|
||||
post.update(&*conn, &rockets.searcher)
|
||||
.expect("post::update: update error");
|
||||
.expect("post::update: update error");;
|
||||
|
||||
if post.published {
|
||||
post.update_mentions(
|
||||
@@ -308,7 +307,7 @@ pub fn update(
|
||||
.filter_map(|m| Mention::build_activity(&rockets, &m).ok())
|
||||
.collect(),
|
||||
)
|
||||
.expect("post::update: mentions error");
|
||||
.expect("post::update: mentions error");;
|
||||
}
|
||||
|
||||
let tags = form
|
||||
@@ -340,8 +339,6 @@ pub fn update(
|
||||
.expect("post::update: act error");
|
||||
let dest = User::one_by_instance(&*conn).expect("post::update: dest error");
|
||||
rockets.worker.execute(move || broadcast(&user, act, dest));
|
||||
|
||||
Timeline::add_to_all_timelines(&rockets, &post, Kind::Original).ok();
|
||||
} else {
|
||||
let act = post
|
||||
.update_activity(&*conn)
|
||||
@@ -406,7 +403,7 @@ pub fn create(
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");
|
||||
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");;
|
||||
let slug = form.title.to_string().to_kebab_case();
|
||||
let user = rockets.user.clone().unwrap();
|
||||
|
||||
@@ -532,10 +529,8 @@ pub fn create(
|
||||
.create_activity(&*conn)
|
||||
.expect("posts::create: activity error");
|
||||
let dest = User::one_by_instance(&*conn).expect("posts::create: dest error");
|
||||
let worker = &rockets.worker;
|
||||
let worker = rockets.worker;
|
||||
worker.execute(move || broadcast(&user, act, dest));
|
||||
|
||||
Timeline::add_to_all_timelines(&rockets, &post, Kind::Original)?;
|
||||
}
|
||||
|
||||
Ok(Flash::success(
|
||||
|
||||
@@ -4,8 +4,7 @@ use rocket_i18n::I18n;
|
||||
use plume_common::activity_pub::broadcast;
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blogs::Blog, inbox::inbox, posts::Post, reshares::*, timeline::*, users::User, Error,
|
||||
PlumeRocket,
|
||||
blogs::Blog, inbox::inbox, posts::Post, reshares::*, users::User, Error, PlumeRocket,
|
||||
};
|
||||
use routes::errors::ErrorPage;
|
||||
|
||||
@@ -24,8 +23,6 @@ pub fn create(
|
||||
let reshare = Reshare::insert(&*conn, NewReshare::new(&post, &user))?;
|
||||
reshare.notify(&*conn)?;
|
||||
|
||||
Timeline::add_to_all_timelines(&rockets, &post, Kind::Reshare(&user))?;
|
||||
|
||||
let dest = User::one_by_instance(&*conn)?;
|
||||
let act = reshare.to_activity(&*conn)?;
|
||||
rockets.worker.execute(move || broadcast(&user, act, dest));
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::{routes::errors::ErrorPage, template_utils::Ructe};
|
||||
use plume_models::{timeline::*, PlumeRocket};
|
||||
use rocket::response::Redirect;
|
||||
use routes::Page;
|
||||
use template_utils::IntoContext;
|
||||
|
||||
#[get("/timeline/<id>?<page>")]
|
||||
pub fn details(id: i32, rockets: PlumeRocket, page: Option<Page>) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let all_tl = Timeline::list_all_for_user(&rockets.conn, rockets.user.clone().map(|u| u.id))?;
|
||||
let tl = Timeline::get(&rockets.conn, id)?;
|
||||
let posts = tl.get_page(&rockets.conn, page.limits())?;
|
||||
let total_posts = tl.count_posts(&rockets.conn)?;
|
||||
Ok(render!(timelines::details(
|
||||
&rockets.to_context(),
|
||||
tl,
|
||||
posts,
|
||||
all_tl,
|
||||
page.0,
|
||||
Page::total(total_posts as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
#[get("/timeline/new")]
|
||||
pub fn new() -> Result<Ructe, ErrorPage> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[post("/timeline/new")]
|
||||
pub fn create() -> Result<Redirect, Ructe> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[get("/timeline/<_id>/edit")]
|
||||
pub fn edit(_id: i32) -> Result<Ructe, ErrorPage> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[post("/timeline/<_id>/edit")]
|
||||
pub fn update(_id: i32) -> Result<Redirect, Ructe> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[post("/timeline/<_id>/delete")]
|
||||
pub fn delete(_id: i32) -> Result<Redirect, ErrorPage> {
|
||||
unimplemented!()
|
||||
}
|
||||
+23
-39
@@ -1,9 +1,5 @@
|
||||
use activitypub::{
|
||||
activity::Create,
|
||||
collection::{OrderedCollection, OrderedCollectionPage},
|
||||
};
|
||||
use activitypub::{activity::Create, collection::OrderedCollection};
|
||||
use atom_syndication::{Entry, FeedBuilder};
|
||||
use diesel::SaveChangesDsl;
|
||||
use rocket::{
|
||||
http::{ContentType, Cookies},
|
||||
request::LenientForm,
|
||||
@@ -24,10 +20,8 @@ use plume_models::{
|
||||
headers::Headers,
|
||||
inbox::inbox as local_inbox,
|
||||
instance::Instance,
|
||||
medias::Media,
|
||||
posts::{LicensedArticle, Post},
|
||||
reshares::Reshare,
|
||||
safe_string::SafeString,
|
||||
users::*,
|
||||
Error, PlumeRocket,
|
||||
};
|
||||
@@ -348,9 +342,7 @@ pub fn edit(name: String, user: User, rockets: PlumeRocket) -> Result<Ructe, Err
|
||||
UpdateUserForm {
|
||||
display_name: user.display_name.clone(),
|
||||
email: user.email.clone().unwrap_or_default(),
|
||||
summary: user.summary.clone(),
|
||||
theme: user.preferred_theme,
|
||||
hide_custom_css: user.hide_custom_css,
|
||||
summary: user.summary,
|
||||
},
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
@@ -375,34 +367,34 @@ pub struct UpdateUserForm {
|
||||
pub display_name: String,
|
||||
pub email: String,
|
||||
pub summary: String,
|
||||
pub theme: Option<String>,
|
||||
pub hide_custom_css: bool,
|
||||
}
|
||||
|
||||
#[put("/@/<_name>/edit", data = "<form>")]
|
||||
pub fn update(
|
||||
_name: String,
|
||||
conn: DbConn,
|
||||
mut user: User,
|
||||
user: User,
|
||||
form: LenientForm<UpdateUserForm>,
|
||||
intl: I18n,
|
||||
) -> Result<Flash<Redirect>, ErrorPage> {
|
||||
user.display_name = form.display_name.clone();
|
||||
user.email = Some(form.email.clone());
|
||||
user.summary = form.summary.clone();
|
||||
user.summary_html = SafeString::new(
|
||||
&utils::md_to_html(
|
||||
&form.summary,
|
||||
None,
|
||||
false,
|
||||
Some(Media::get_media_processor(&conn, vec![&user])),
|
||||
)
|
||||
.0,
|
||||
);
|
||||
user.preferred_theme = form.theme.clone();
|
||||
user.hide_custom_css = form.hide_custom_css;
|
||||
let _: User = user.save_changes(&*conn).map_err(Error::from)?;
|
||||
|
||||
user.update(
|
||||
&*conn,
|
||||
if !form.display_name.is_empty() {
|
||||
form.display_name.clone()
|
||||
} else {
|
||||
user.display_name.clone()
|
||||
},
|
||||
if !form.email.is_empty() {
|
||||
form.email.clone()
|
||||
} else {
|
||||
user.email.clone().unwrap_or_default()
|
||||
},
|
||||
if !form.summary.is_empty() {
|
||||
form.summary.clone()
|
||||
} else {
|
||||
user.summary.to_string()
|
||||
},
|
||||
)?;
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(me)),
|
||||
i18n!(intl.catalog, "Your profile has been updated."),
|
||||
@@ -525,7 +517,7 @@ pub fn create(
|
||||
conn,
|
||||
form.username.to_string(),
|
||||
form.username.to_string(),
|
||||
Role::Normal,
|
||||
false,
|
||||
"",
|
||||
form.email.to_string(),
|
||||
User::hash_pass(&form.password).map_err(to_validation)?,
|
||||
@@ -556,15 +548,7 @@ pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<Order
|
||||
let user = User::find_by_fqn(&rockets, &name).ok()?;
|
||||
user.outbox(&*rockets.conn).ok()
|
||||
}
|
||||
#[get("/@/<name>/outbox?<page>")]
|
||||
pub fn outbox_page(
|
||||
name: String,
|
||||
page: Page,
|
||||
rockets: PlumeRocket,
|
||||
) -> Option<ActivityStream<OrderedCollectionPage>> {
|
||||
let user = User::find_by_fqn(&rockets, &name).ok()?;
|
||||
user.outbox_page(&*rockets.conn, page.limits()).ok()
|
||||
}
|
||||
|
||||
#[post("/@/<name>/inbox", data = "<data>")]
|
||||
pub fn inbox(
|
||||
name: String,
|
||||
|
||||
+11
-11
@@ -48,16 +48,14 @@ impl Resolver<PlumeRocket> for WebfingerResolver {
|
||||
CONFIG.base_url.as_str()
|
||||
}
|
||||
|
||||
fn find(prefix: Prefix, acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
|
||||
match prefix {
|
||||
Prefix::Acct => User::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|usr| usr.webfinger(&*ctx.conn))
|
||||
.or(Err(ResolverError::NotFound)),
|
||||
Prefix::Group => Blog::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|blog| blog.webfinger(&*ctx.conn))
|
||||
.or(Err(ResolverError::NotFound)),
|
||||
Prefix::Custom(_) => Err(ResolverError::NotFound),
|
||||
}
|
||||
fn find(acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
|
||||
User::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|usr| usr.webfinger(&*ctx.conn))
|
||||
.or_else(|_| {
|
||||
Blog::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|blog| blog.webfinger(&*ctx.conn))
|
||||
.or(Err(ResolverError::NotFound))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +72,9 @@ pub fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> {
|
||||
"Invalid resource. Make sure to request an acct: URI"
|
||||
}
|
||||
ResolverError::NotFound => "Requested resource was not found",
|
||||
ResolverError::WrongDomain => "This is not the instance of the requested resource",
|
||||
ResolverError::WrongInstance => {
|
||||
"This is not the instance of the requested resource"
|
||||
}
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
+103
-141
@@ -5,7 +5,7 @@ use rocket::http::{Method, Status};
|
||||
use rocket::request::Request;
|
||||
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
|
||||
use rocket_i18n::Catalog;
|
||||
use std::collections::{btree_map::BTreeMap, hash_map::DefaultHasher};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
use templates::Html;
|
||||
|
||||
@@ -108,15 +108,6 @@ pub fn translate_notification(ctx: BaseContext, notif: Notification) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn i18n_timeline_name(cat: &Catalog, tl: &str) -> String {
|
||||
match tl {
|
||||
"Your feed" => i18n!(cat, "Your feed"),
|
||||
"Local feed" => i18n!(cat, "Local feed"),
|
||||
"Federated feed" => i18n!(cat, "Federated feed"),
|
||||
n => n.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Size {
|
||||
Small,
|
||||
Medium,
|
||||
@@ -152,11 +143,11 @@ pub fn avatar(
|
||||
))
|
||||
}
|
||||
|
||||
pub fn tabs(links: &[(impl AsRef<str>, String, bool)]) -> Html<String> {
|
||||
pub fn tabs(links: &[(&str, String, bool)]) -> Html<String> {
|
||||
let mut res = String::from(r#"<div class="tabs">"#);
|
||||
for (url, title, selected) in links {
|
||||
res.push_str(r#"<a dir="auto" href=""#);
|
||||
res.push_str(url.as_ref());
|
||||
res.push_str(r#"<a href=""#);
|
||||
res.push_str(url);
|
||||
if *selected {
|
||||
res.push_str(r#"" class="selected">"#);
|
||||
} else {
|
||||
@@ -185,14 +176,14 @@ pub fn paginate_param(
|
||||
p
|
||||
})
|
||||
.unwrap_or_default();
|
||||
res.push_str(r#"<div class="pagination" dir="auto">"#);
|
||||
res.push_str(r#"<div class="pagination">"#);
|
||||
if page != 1 {
|
||||
res.push_str(
|
||||
format!(
|
||||
r#"<a href="?{}page={}">{}</a>"#,
|
||||
param,
|
||||
page - 1,
|
||||
i18n!(catalog, "Previous page")
|
||||
catalog.gettext("Previous page")
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
@@ -203,7 +194,7 @@ pub fn paginate_param(
|
||||
r#"<a href="?{}page={}">{}</a>"#,
|
||||
param,
|
||||
page + 1,
|
||||
i18n!(catalog, "Next page")
|
||||
catalog.gettext("Next page")
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
@@ -240,143 +231,114 @@ macro_rules! icon {
|
||||
};
|
||||
}
|
||||
|
||||
/// A builder type to generate `<input>` tags in a type-safe way.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// This example uses all options, but you don't have to specify everything.
|
||||
///
|
||||
/// ```rust
|
||||
/// # let current_email = "foo@bar.baz";
|
||||
/// # let catalog = gettext::Catalog::parse("").unwrap();
|
||||
/// Input::new("mail", "Your email address")
|
||||
/// .input_type("email")
|
||||
/// .default(current_email)
|
||||
/// .optional()
|
||||
/// .details("We won't use it for advertising.")
|
||||
/// .set_prop("class", "email-input")
|
||||
/// .to_html(catalog);
|
||||
/// ```
|
||||
pub struct Input {
|
||||
/// The name of the input (`name` and `id` in HTML).
|
||||
name: String,
|
||||
/// The description of this field.
|
||||
label: String,
|
||||
/// The `type` of the input (`text`, `email`, `password`, etc).
|
||||
input_type: String,
|
||||
/// The default value for this input field.
|
||||
default: Option<String>,
|
||||
/// `true` if this field is not required (will add a little badge next to the label).
|
||||
optional: bool,
|
||||
/// A small message to display next to the label.
|
||||
details: Option<String>,
|
||||
/// Additional HTML properties.
|
||||
props: BTreeMap<String, String>,
|
||||
/// The error message to show next to this field.
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// Creates a new input with a given name.
|
||||
pub fn new(name: impl ToString, label: impl ToString) -> Input {
|
||||
Input {
|
||||
name: name.to_string(),
|
||||
label: label.to_string(),
|
||||
input_type: "text".into(),
|
||||
default: None,
|
||||
optional: false,
|
||||
details: None,
|
||||
props: BTreeMap::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `type` of this input.
|
||||
pub fn input_type(mut self, t: impl ToString) -> Input {
|
||||
self.input_type = t.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Marks this field as optional.
|
||||
pub fn optional(mut self) -> Input {
|
||||
self.optional = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Fills the input with a default value (useful for edition form, to show the current values).
|
||||
pub fn default(mut self, val: impl ToString) -> Input {
|
||||
self.default = Some(val.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds additional information next to the label.
|
||||
pub fn details(mut self, text: impl ToString) -> Input {
|
||||
self.details = Some(text.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines an additional HTML property.
|
||||
///
|
||||
/// This method can be called multiple times for the same input.
|
||||
pub fn set_prop(mut self, key: impl ToString, val: impl ToString) -> Input {
|
||||
self.props.insert(key.to_string(), val.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows an error message
|
||||
pub fn error(mut self, errs: &validator::ValidationErrors) -> Input {
|
||||
if let Some(field_errs) = errs.clone().field_errors().get(self.name.as_str()) {
|
||||
self.error = Some(
|
||||
field_errs[0]
|
||||
.message
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the HTML markup for this field.
|
||||
pub fn html(mut self, cat: &Catalog) -> Html<String> {
|
||||
if !self.optional {
|
||||
self = self.set_prop("required", true);
|
||||
}
|
||||
macro_rules! input {
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {{
|
||||
use std::borrow::Cow;
|
||||
use validator::ValidationErrorsKind;
|
||||
let cat = $catalog;
|
||||
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}" dir="auto">
|
||||
<label for="{name}">
|
||||
{label}
|
||||
{optional}
|
||||
{details}
|
||||
</label>
|
||||
{error}
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props} dir="auto"/>
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
||||
"#,
|
||||
name = self.name,
|
||||
label = self.label,
|
||||
kind = self.input_type,
|
||||
optional = if self.optional {
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
kind = stringify!($kind),
|
||||
optional = if $optional {
|
||||
format!("<small>{}</small>", i18n!(cat, "Optional"))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
details = self
|
||||
.details
|
||||
.map(|d| format!("<small>{}</small>", d))
|
||||
.unwrap_or_default(),
|
||||
error = self
|
||||
.error
|
||||
.map(|e| format!(r#"<p class="error" dir="auto">{}</p>"#, e))
|
||||
.unwrap_or_default(),
|
||||
val = escape(&self.default.unwrap_or_default()),
|
||||
props = self
|
||||
.props
|
||||
.into_iter()
|
||||
.fold(String::new(), |mut res, (key, val)| {
|
||||
res.push_str(&format!("{}=\"{}\" ", key, val));
|
||||
res
|
||||
})
|
||||
details = if $details.len() > 0 {
|
||||
format!("<small>{}</small>", i18n!(cat, $details))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
error = if let Some(ValidationErrorsKind::Field(errs)) =
|
||||
$err.errors().get(stringify!($name))
|
||||
{
|
||||
format!(
|
||||
r#"<p class="error">{}</p>"#,
|
||||
errs[0]
|
||||
.message
|
||||
.clone()
|
||||
.unwrap_or(Cow::from("Unknown error"))
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
val = escape(&$form.$name),
|
||||
props = $props
|
||||
))
|
||||
}
|
||||
}};
|
||||
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!(
|
||||
$catalog,
|
||||
$name($kind),
|
||||
$label,
|
||||
true,
|
||||
$details,
|
||||
$form,
|
||||
$err,
|
||||
$props
|
||||
)
|
||||
};
|
||||
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!(
|
||||
$catalog,
|
||||
$name($kind),
|
||||
$label,
|
||||
true,
|
||||
"",
|
||||
$form,
|
||||
$err,
|
||||
$props
|
||||
)
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!(
|
||||
$catalog,
|
||||
$name($kind),
|
||||
$label,
|
||||
false,
|
||||
$details,
|
||||
$form,
|
||||
$err,
|
||||
$props
|
||||
)
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
input!(
|
||||
$catalog,
|
||||
$name($kind),
|
||||
$label,
|
||||
false,
|
||||
"",
|
||||
$form,
|
||||
$err,
|
||||
$props
|
||||
)
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
|
||||
input!($catalog, $name($kind), $label, false, "", $form, $err, "")
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {{
|
||||
let cat = $catalog;
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
kind = stringify!($kind),
|
||||
props = $props
|
||||
))
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* Heading */
|
||||
// Heading
|
||||
main header.article {
|
||||
overflow: hidden;
|
||||
background: $background;
|
||||
color: $text-color;
|
||||
background: $white;
|
||||
color: $black;
|
||||
display: grid;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
@@ -52,14 +52,14 @@ main header.article {
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, transparent 20vh, $black 80vh);
|
||||
background: linear-gradient(180deg, transparent 20vh, black 80vh);
|
||||
}
|
||||
|
||||
& > img {
|
||||
z-index: 1;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
background: $primary;
|
||||
background: $purple;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ main .article-info {
|
||||
}
|
||||
}
|
||||
|
||||
/* The article itself */
|
||||
// The article itself
|
||||
main article {
|
||||
max-width: $article-width;
|
||||
margin: 2.5em auto;
|
||||
@@ -104,7 +104,7 @@ main article {
|
||||
}
|
||||
}
|
||||
|
||||
/* Metadata under the article */
|
||||
// Metadata under the article
|
||||
main .article-meta, main .article-meta button {
|
||||
padding: 0;
|
||||
font-size: 1.1em;
|
||||
@@ -129,7 +129,7 @@ main .article-meta {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Article Tags */
|
||||
// Article Tags
|
||||
.tags {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
@@ -141,7 +141,7 @@ main .article-meta {
|
||||
padding: 0px;
|
||||
margin: 0px 10px 10px 0px;
|
||||
transition: all 0.2s ease-in;
|
||||
border: 1px solid $primary;
|
||||
border: 1px solid $purple;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
@@ -149,12 +149,12 @@ main .article-meta {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparentize($primary, 0.9);
|
||||
background: transparentize($purple, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Likes & Boosts */
|
||||
// Likes & Boosts
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -181,7 +181,7 @@ main .article-meta {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $text-color;
|
||||
color: $black;
|
||||
border: none;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
@@ -201,7 +201,7 @@ main .article-meta {
|
||||
|
||||
&.reshared, &.liked {
|
||||
svg.feather {
|
||||
color: $background;
|
||||
color: $white;
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
@@ -234,35 +234,35 @@ main .article-meta {
|
||||
}
|
||||
|
||||
.reshares {
|
||||
p, .action:hover { color: $primary; }
|
||||
p, .action:hover { color: $purple; }
|
||||
|
||||
.action svg.feather {
|
||||
padding: 0.7em;
|
||||
box-sizing: border-box;
|
||||
color: $primary;
|
||||
border: solid $primary thin;
|
||||
color: $purple;
|
||||
border: solid $purple thin;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action:hover svg.feather {
|
||||
background: transparentize($primary, 0.85);
|
||||
background: transparentize($purple, 0.85);
|
||||
}
|
||||
|
||||
.action.reshared svg.feather {
|
||||
background: $primary;
|
||||
background: $purple;
|
||||
}
|
||||
.action.reshared:hover svg.feather {
|
||||
background: transparentize($primary, 0.75)
|
||||
color: $primary;
|
||||
background: transparentize($purple, 0.75)
|
||||
color: $purple;
|
||||
}
|
||||
}
|
||||
|
||||
/* Comments */
|
||||
// Comments
|
||||
.comments {
|
||||
margin: 0 $horizontal-margin;
|
||||
|
||||
h2 {
|
||||
color: $primary;
|
||||
color: $purple;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -271,7 +271,7 @@ main .article-meta {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* New comment form */
|
||||
// New comment form
|
||||
> form input[type="submit"] {
|
||||
font-size: 1em;
|
||||
-webkit-appearance: none;
|
||||
@@ -281,17 +281,17 @@ main .article-meta {
|
||||
a.button, form.inline, form.inline input {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $text-color;
|
||||
color: $black;
|
||||
margin-right: 2em;
|
||||
font-family: $route159;
|
||||
font-weight: normal;
|
||||
|
||||
&::before {
|
||||
color: $primary;
|
||||
color: $purple;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
&:hover { color: $primary; }
|
||||
&:hover { color: $purple; }
|
||||
}
|
||||
|
||||
.comment {
|
||||
@@ -325,7 +325,7 @@ main .article-meta {
|
||||
}
|
||||
|
||||
.dt-published a {
|
||||
color: transparentize($text-color, 0.6);
|
||||
color: transparentize($black, 0.6);
|
||||
}
|
||||
|
||||
.author {
|
||||
@@ -339,11 +339,11 @@ main .article-meta {
|
||||
}
|
||||
|
||||
.display-name {
|
||||
color: $text-color;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.display-name { color: $primary; }
|
||||
.display-name { color: $purple; }
|
||||
small { opacity: 1; }
|
||||
}
|
||||
}
|
||||
@@ -384,7 +384,7 @@ main .article-meta {
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: transparentize($text-color, 0.6);
|
||||
color: transparentize($black, 0.6);
|
||||
}
|
||||
|
||||
article {
|
||||
@@ -400,7 +400,7 @@ main .article-meta {
|
||||
left: 20vw;
|
||||
right: 20vw;
|
||||
background: $gray;
|
||||
border: 1px solid $primary;
|
||||
border: 1px solid $purple;
|
||||
z-index: 2;
|
||||
padding: 2em;
|
||||
overflow-y: auto;
|
||||
@@ -421,7 +421,7 @@ main .article-meta {
|
||||
}
|
||||
|
||||
|
||||
/* Content warning */
|
||||
// content warning
|
||||
.cw-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -459,7 +459,7 @@ input:checked ~ .cw-container > .cw-text {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* Bottom action bar */
|
||||
// Bottom action bar
|
||||
|
||||
.bottom-bar {
|
||||
z-index: 10;
|
||||
@@ -481,12 +481,3 @@ input:checked ~ .cw-container > .cw-text {
|
||||
margin: auto $horizontal-margin;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footnote related styles */
|
||||
.footnote-definition {
|
||||
p {
|
||||
font-size: smaller;
|
||||
/* Make sure the definition is inline with the label-definition */
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,14 @@ input, textarea, select {
|
||||
-webkit-appearance: textarea;
|
||||
|
||||
background: $form-input-background;
|
||||
color: $text-color;
|
||||
border: solid $form-input-border thin;
|
||||
color: $black;
|
||||
border: solid $black thin;
|
||||
|
||||
font-size: 1.2em;
|
||||
font-weight: 400;
|
||||
|
||||
&:focus {
|
||||
border-color: $primary;
|
||||
border-color: $purple;
|
||||
}
|
||||
}
|
||||
form input[type="submit"] {
|
||||
@@ -44,7 +44,7 @@ input[type="checkbox"] {
|
||||
-webkit-appearance: checkbox;
|
||||
}
|
||||
|
||||
/* Inline forms (containing only CSRF token and a <submit>, for protected links) */
|
||||
/** Inline forms (containing only CSRF token and a <submit>, for protected links) **/
|
||||
|
||||
form.inline {
|
||||
display: inline;
|
||||
@@ -64,7 +64,7 @@ form.inline {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: $primary;
|
||||
color: $purple;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
@@ -78,15 +78,15 @@ form.inline {
|
||||
margin: 0.5em auto;
|
||||
padding: 0.75em 1em;
|
||||
|
||||
background: $primary;
|
||||
color: $primary-text-color;
|
||||
background: $purple;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: transparentize($primary, 0.1);
|
||||
background: transparentize($purple, 0.1);
|
||||
}
|
||||
|
||||
&.destructive {
|
||||
@@ -99,10 +99,10 @@ form.inline {
|
||||
|
||||
&.secondary {
|
||||
background: $gray;
|
||||
color: $text-color;
|
||||
color: $black;
|
||||
|
||||
&:hover {
|
||||
background: transparentize($text-color, 0.9);
|
||||
background: transparentize($black, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ input[type="submit"] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* The writing page */
|
||||
// Writing page
|
||||
form.new-post {
|
||||
max-width: 60em;
|
||||
.title {
|
||||
@@ -162,11 +162,3 @@ header.center {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
form > header {
|
||||
display: flex;
|
||||
|
||||
input[type="submit"] {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,31 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: $background;
|
||||
color: $text-color;
|
||||
color: $black;
|
||||
font-family: $route159;
|
||||
|
||||
::selection {
|
||||
background: transparentize($primary, 0.7);
|
||||
background: transparentize($purple, 0.7);
|
||||
}
|
||||
::-moz-selection {
|
||||
background: transparentize($primary, 0.7);
|
||||
background: transparentize($purple, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: $primary;
|
||||
color: $purple;
|
||||
text-decoration: none;
|
||||
}
|
||||
a::selection {
|
||||
color: $background;
|
||||
color: $white;
|
||||
}
|
||||
a::-moz-selection {
|
||||
color: $background;
|
||||
color: $white;
|
||||
}
|
||||
small {
|
||||
margin-left: 1em;
|
||||
color: transparentize($text-color, 0.6);
|
||||
color: transparentize($black, 0.6);
|
||||
font-size: 0.75em;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
@@ -68,7 +61,7 @@ small {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* Main */
|
||||
/// Main
|
||||
body > main > *, .h-feed > * {
|
||||
margin: 1em $horizontal-margin;
|
||||
}
|
||||
@@ -137,13 +130,13 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
/* Errors */
|
||||
/// Errors
|
||||
p.error {
|
||||
color: $red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* User page */
|
||||
/// User page
|
||||
.user h1 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -159,9 +152,9 @@ p.error {
|
||||
margin-right: 1em;
|
||||
padding: 0.35em 1em;
|
||||
|
||||
background: $background;
|
||||
color: $primary;
|
||||
border: 1px solid $primary;
|
||||
background: $white;
|
||||
color: $purple;
|
||||
border: 1px solid $purple;
|
||||
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -170,7 +163,7 @@ p.error {
|
||||
margin: 2em 0px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
/// Cards
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -210,9 +203,9 @@ p.error {
|
||||
font-weight: normal;
|
||||
a {
|
||||
transition: color 0.1s ease-in;
|
||||
color: $text-color;
|
||||
color: $black;
|
||||
|
||||
&:hover { color: $primary; }
|
||||
&:hover { color: $purple; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +242,7 @@ p.error {
|
||||
}
|
||||
}
|
||||
|
||||
/* Instance presentation */
|
||||
/// Instance presentation
|
||||
.presentation {
|
||||
max-width: none;
|
||||
|
||||
@@ -263,7 +256,7 @@ p.error {
|
||||
}
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
// Stats
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
@@ -287,7 +280,7 @@ p.error {
|
||||
}
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
/// Pagination
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
@@ -297,7 +290,7 @@ p.error {
|
||||
}
|
||||
}
|
||||
|
||||
/* Flex boxes */
|
||||
/// Flex boxes
|
||||
.flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -317,10 +310,6 @@ p.error {
|
||||
flex: 1;
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
.grow:first-child {
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left-icon {
|
||||
@@ -332,13 +321,13 @@ p.error {
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
/// Footer
|
||||
body > footer {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: space-around;
|
||||
background: $primary;
|
||||
color: $primary-text-color;
|
||||
background: $purple;
|
||||
color: $white;
|
||||
margin-top: 5em;
|
||||
|
||||
* {
|
||||
@@ -347,13 +336,13 @@ body > footer {
|
||||
|
||||
hr {
|
||||
transform: skew(-15deg);
|
||||
background: $primary-text-color;
|
||||
background: $white;
|
||||
border: none;
|
||||
width: .2em;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: $primary-text-color;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
div {
|
||||
@@ -456,170 +445,12 @@ figure {
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: $text-color;
|
||||
color: $black;
|
||||
padding: 1em;
|
||||
|
||||
&.selected {
|
||||
color: $primary;
|
||||
border-bottom: 1px solid $primary;
|
||||
color: $purple;
|
||||
border-bottom: 1px solid $purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
@keyframes menuOpening {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
body > header {
|
||||
flex-direction: column;
|
||||
|
||||
nav#menu {
|
||||
display: inline-flex;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: none;
|
||||
appearance: none;
|
||||
text-align: center;
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
||||
body > header:focus-within #content, #content.show {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
animation: 0.2s menuOpening;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
transform: skewX(-10deg);
|
||||
top: 0;
|
||||
left: -20%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
z-index: -10;
|
||||
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
> nav {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
color: $background;
|
||||
font-size: 1.4em;
|
||||
font-weight: 300;
|
||||
|
||||
&.title { font-size: 1.8em; }
|
||||
|
||||
> *:first-child { width: 3rem; }
|
||||
> img:first-child { height: 3rem; }
|
||||
> *:last-child { margin-left: 1rem; }
|
||||
> nav hr {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: solid $background 0.1rem;
|
||||
}
|
||||
.mobile-label { display: initial; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main .article-meta {
|
||||
> *, .comments {
|
||||
margin: 0 5%;
|
||||
}
|
||||
> p {
|
||||
margin: 2em 5%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.comments > * { margin: auto 5%; }
|
||||
.comments .comment { padding: 2em 0px; }
|
||||
}
|
||||
main .article-info, main article, main h1.article, main h2.article {
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-width: 80%;
|
||||
min-height: 80%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin: auto 0px 2em;
|
||||
}
|
||||
|
||||
.stats { flex-direction: column; }
|
||||
body > footer {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
body > footer * {
|
||||
margin: 1em auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flex.wrap { flex-direction: column; }
|
||||
|
||||
.cards, .list {
|
||||
margin: 1rem 0 5rem;
|
||||
}
|
||||
|
||||
.split {
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
|
||||
& > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
& > div {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
main .article-meta .comments .comment {
|
||||
header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ body > header {
|
||||
transform: skewX(-15deg);
|
||||
left: -1em;
|
||||
padding: 1em 1em 1em 2em;
|
||||
background: $primary;
|
||||
background: $purple;
|
||||
align-self: flex-start;
|
||||
|
||||
a {
|
||||
@@ -40,7 +40,7 @@ body > header {
|
||||
hr {
|
||||
height: 100%;
|
||||
width: 0.2em;
|
||||
background: $primary;
|
||||
background: $purple;
|
||||
border: none;
|
||||
transform: skewX(-15deg);
|
||||
}
|
||||
@@ -96,12 +96,12 @@ body > header {
|
||||
}
|
||||
|
||||
p.success {
|
||||
color: darken($success-color, 20%);
|
||||
background: lighten($success-color, 40%);
|
||||
color: darken($green, 20%);
|
||||
background: lighten($green, 40%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Only enable label animations on large screens */
|
||||
// Only enable label animations on normal screens
|
||||
@media screen and (min-width: 600px) {
|
||||
header nav a {
|
||||
i {
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Color Scheme */
|
||||
// Color Scheme
|
||||
$gray: #F3F3F3;
|
||||
$black: #242424;
|
||||
$white: #F8F8F8;
|
||||
@@ -10,19 +10,14 @@ $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 */
|
||||
// Dimensions
|
||||
|
||||
$article-width: 70ch;
|
||||
$horizontal-margin: 20%;
|
||||
$margin: 0 $horizontal-margin;
|
||||
|
||||
/* Fonts */
|
||||
// Fonts
|
||||
|
||||
$route159: "Route159", serif;
|
||||
$playfair: "Playfair Display", serif;
|
||||
@@ -1,10 +1,10 @@
|
||||
@font-face {
|
||||
font-family: "Feather";
|
||||
src: url('./fonts/Feather/Feather.eot'); /* IE9 */
|
||||
src: url('./fonts/Feather/Feather.eot') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('./fonts/Feather/Feather.woff') format('woff'), /* Chrome, Firefox */
|
||||
url('./fonts/Feather/Feather.ttf') format('truetype'), /* Chrome, Firefox, Opera, Safari, Android, iOS 4.2+ */
|
||||
url('./fonts/Feather/Feather.svg') format('svg'); /* iOS 4.1- */
|
||||
src: url('../fonts/Feather/Feather.eot'); /* IE9 */
|
||||
src: url('../fonts/Feather/Feather.eot') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/Feather/Feather.woff') format('woff'), /* Chrome, Firefox */
|
||||
url('../fonts/Feather/Feather.ttf') format('truetype'), /* Chrome, Firefox, Opera, Safari, Android, iOS 4.2+ */
|
||||
url('../fonts/Feather/Feather.svg') format('svg'); /* iOS 4.1- */
|
||||
}
|
||||
|
||||
.feather {
|
||||
@@ -0,0 +1,175 @@
|
||||
/* color palette: https://coolors.co/23f0c7-ef767a-7765e3-6457a6-ffe347 */
|
||||
|
||||
@import url('../fonts/Route159/Route159.css');
|
||||
@import url('../fonts/Lora/Lora.css');
|
||||
@import url('../fonts/Playfair_Display/PlayfairDisplay.css');
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
@import 'variables';
|
||||
@import 'global';
|
||||
@import 'header';
|
||||
@import 'article';
|
||||
@import 'forms';
|
||||
|
||||
/// Small screens
|
||||
@media screen and (max-width: 600px) {
|
||||
@keyframes menuOpening {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
body > header {
|
||||
flex-direction: column;
|
||||
|
||||
nav#menu {
|
||||
display: inline-flex;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: none;
|
||||
appearance: none;
|
||||
text-align: center;
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
||||
body > header:focus-within #content, #content.show {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
animation: 0.2s menuOpening;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
transform: skewX(-10deg);
|
||||
top: 0;
|
||||
left: -20%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
z-index: -10;
|
||||
|
||||
background: $purple;
|
||||
}
|
||||
|
||||
> nav {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
color: $white;
|
||||
font-size: 1.4em;
|
||||
font-weight: 300;
|
||||
|
||||
&.title { font-size: 1.8em; }
|
||||
|
||||
> *:first-child { width: 3rem; }
|
||||
> img:first-child { height: 3rem; }
|
||||
> *:last-child { margin-left: 1rem; }
|
||||
> nav hr {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: solid $white 0.1rem;
|
||||
}
|
||||
.mobile-label { display: initial; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main .article-meta {
|
||||
> *, .comments {
|
||||
margin: 0 5%;
|
||||
}
|
||||
> p {
|
||||
margin: 2em 5%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.comments > * { margin: auto 5%; }
|
||||
.comments .comment { padding: 2em 0px; }
|
||||
}
|
||||
main .article-info, main article, main h1.article, main h2.article {
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-width: 80%;
|
||||
min-height: 80%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin: auto 0px 2em;
|
||||
}
|
||||
|
||||
.stats { flex-direction: column; }
|
||||
body > footer {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
body > footer * {
|
||||
margin: 1em auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flex.wrap { flex-direction: column; }
|
||||
|
||||
.cards, .list {
|
||||
margin: 1rem 0 5rem;
|
||||
}
|
||||
|
||||
.split {
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
|
||||
& > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
& > div {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
main .article-meta .comments .comment {
|
||||
header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 209 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user