Compare commits

..

48 Commits

Author SHA1 Message Date
Igor Galić 3c4abcff81 Allow updating of custom_domain 2019-08-21 11:09:42 +02:00
Igor Galić d242e6df11 restore df7adaf0e0 ← after fixing mount-point; fix query() parsing 2019-08-20 15:21:01 +02:00
Igor Galić f395c1046c fix mount point for blog post routing function 2019-08-20 15:06:57 +02:00
Igor Galić df7adaf0e0 when building links, skip both, /custom_domain/ and <custom_domain> 2019-08-20 15:04:29 +02:00
Igor Galić 768f126f1d use Status::* as @fdb-hiroshima suggested 2019-08-20 14:51:14 +02:00
Igor Galić 169469816d use Custom status code
thanks, @fdb-hiroshima

Co-Authored-By: fdb-hiroshima <35889323+fdb-hiroshima@users.noreply.github.com>
2019-08-20 14:43:20 +02:00
Igor Galić 064dd79eef Allow editing blogs with custom_domain
n.b.: no validation yet
2019-08-20 12:24:36 +02:00
Igor Galić df47cddb87 also mount domain_validation into /custom_domains/ 2019-08-20 11:47:24 +02:00
Igor Galić f67ce93d94 fix getter router for domain_validation! 2019-08-20 11:33:00 +02:00
Igor Galić 38ece9b5a6 do not panic if reqwest does not work 2019-08-20 11:18:23 +02:00
Igor Galić 5e46922ed0 validate custom domain! 2019-08-19 22:42:34 +02:00
Igor Galić 6072351840 one step closer to validating custom domains! 2019-08-19 22:04:22 +02:00
Igor Galić 444a4673f4 correctly mutex-lock and modify valid_domains
thanks to @fdb-hiroshima for the review
2019-08-19 21:19:52 +02:00
Igor Galić ed30284386 implement domain validation using rocket::State
this doesn't work (yet?), because i don't know how to store something
mutable in State<>
2019-08-15 17:47:19 +02:00
Igor Galić bf1673dda1 start exploring some ideas for domain validation 2019-08-14 23:51:28 +02:00
Igor Galić ff0c82efc1 review by @fdb-hiroshima
Co-Authored-By: fdb-hiroshima <35889323+fdb-hiroshima@users.noreply.github.com>
2019-08-14 23:51:27 +02:00
Ana Gelez da6757c55c Basic domain validation
The actual DNS check still needs to be written
2019-08-14 23:51:27 +02:00
Igor Galić 60270121dc fix clippy warning
Co-Authored-By: fdb-hiroshima <35889323+fdb-hiroshima@users.noreply.github.com>
2019-08-14 23:51:26 +02:00
Ana Gelez fdc7da0edf Strings have to be cloned 2019-08-14 23:51:25 +02:00
Igor Galić 2dedcdbc53 start modifying /blogs/new to add custom_domain to the form 2019-08-14 23:51:24 +02:00
Trinity Pointard b172a80e35 finally fix url!
also please a bit clippy
2019-08-14 23:51:23 +02:00
Trinity Pointard c5f6b88b1d make url! work better
there is still some issue with `None`, and an error making no sense at
some place
2019-08-14 23:51:23 +02:00
Igor Galić 6cd8bd89b2 fix syntax: we're now down to type errors 2019-08-14 23:51:22 +02:00
Trinity Pointard cdc919e308 allow more syntaxes for url
allow for params that are used only by one of normal/custom routes
2019-08-14 23:51:21 +02:00
Igor Galić f635dcf6c3 move custom_ route functions into a custom namespace
this way, we can actually use the url! macro
2019-08-14 23:51:20 +02:00
Trinity Pointard 7139119b8f add url! macro for custom domain path uri creation
see doc-comment for limitations
2019-08-14 23:51:20 +02:00
Igor Galić 203da23cf2 follow up on @fdb-hiroshima & @BaptisteGelez review 2019-08-14 23:51:19 +02:00
Igor Galić f73fba583a custom_domainify posts::details 2019-08-14 23:51:18 +02:00
Igor Galić fe110b5d8a fix issues pointed out by @BaptisteGelez in review 2019-08-14 23:51:17 +02:00
Igor Galić cc0df4ecb2 redirect blog urls to custom_domain if it exists 2019-08-14 23:51:16 +02:00
Igor Galić 9cee38ae6a add url to Blog, this seems useful. 2019-08-14 23:51:16 +02:00
Igor Galić 8e7f789969 Allow searching from custom_domain 2019-08-14 23:51:15 +02:00
Igor Galić 92fbd174eb extend 404 handler to handle all the requests our custom_ routes dont 2019-08-14 23:51:14 +02:00
Igor Galić 6253adf768 simplify / unify error handling
We want to return a Result<Blog>, instead of a QueryResult<Blog>.
We can use map_err() to map it to the desired error type.

thanks to review from @BaptisteGelez & @fdb-hiroshima.
2019-08-14 23:51:13 +02:00
Igor Galić a0aef50674 extract common routing code into private "_guts()" functions 2019-08-14 23:51:13 +02:00
Igor Galić 8e6b1ab86e simplify retrieval in find_by_host()
we can use "first()" instead of limit(1).load().etc…
since on a UNIQUE field, we only expect 1 result.

first() returns QueryResult, which is Result<T, diesel::Error>, so we
need to implement a converter for that error type.

This commit addresses @fdb-hiroshima's review.
2019-08-14 23:51:12 +02:00
Igor Galić 1c34ac38f7 replace if / else with ok_or() 2019-08-14 23:51:11 +02:00
Igor Galić 468e663344 Add custom_details and custom_activity_details as first routes
how this works: we use find_by_host() to find the Host in question, the
defer to the existing function!

Caveat: Currently, we, in that function, we do another lookup DB lookup
for the Blog, even thou we already know it.
It might be wise, to have both of those another wrapper here?!
2019-08-14 23:51:10 +02:00
Igor Galić 0645f7e253 CustomDomainFairing must not match /static/ and /api/
thanks, again, @fdb-hiroshima for helping with this code, when i got
stuck in lifetime-hecc.
2019-08-14 23:51:10 +02:00
Igor Galić f94b0c79c5 move impl closer to mother struct 2019-08-14 23:51:09 +02:00
Igor Galić b09b51c74b normalize URLs before setting them 2019-08-14 23:51:08 +02:00
Igor Galić e6747de998 cache custom_domains list
follow #572 and cache the list of custom domains.
2019-08-14 23:51:07 +02:00
Igor Galić 2746e088ae appease clippy 2019-08-14 23:51:06 +02:00
Igor Galić 3a4c2f2cf9 create & attach an AdHoc Fairing for dealing with Custom Domains
this rewrites the URL to /custom_domain/<url>
(we have no route handlers for this path yet)

Lots of help from @fdb-hiroshima & @BaptisteGelez in dealing with the
borrow checker — thank you 💜
2019-08-14 23:51:06 +02:00
Igor Galić 351c01f71c list custom_domains
thanks a lot to @fdb-hiroshima and @BaptisteGelez for helping with the
code.
2019-08-14 23:51:05 +02:00
Igor Galić 65ae51b7e5 implement Host more completely by doing less
we now use DieselNewType and Shrinkwrap to automatically derive all the
things we need.
2019-08-14 23:51:04 +02:00
Igor Galić 92bbeeb45e add Host(String) wrapper type
we can use this to handle rocket Requests (from_request)
but we still have to figure out how to automatically assign the type to
custom_domain: Optional<Host>, and public_domain: Host.
2019-08-14 23:51:03 +02:00
Igor Galić 6bcc4f0ab0 add custom_domain (default to NULL) to schema 2019-08-14 23:51:03 +02:00
316 changed files with 13280 additions and 17083 deletions
-47
View File
@@ -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
View File
@@ -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
+1048 -1060
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -16,14 +16,15 @@ 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"
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
num_cpus = "1.10"
reqwest = "0.9"
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"
runtime-fmt = "0.3.0"
scheduled-thread-pool = "0.2.2"
serde = "1.0"
serde_json = "1.0"
@@ -65,7 +66,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 -1
View File
@@ -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">
-3
View File
@@ -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;
-12
View File
@@ -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';
-12
View File
@@ -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';
+20 -100
View File
@@ -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(())
--#!}
@@ -0,0 +1,2 @@
-- undo the adding of custom_domain column to blogs table.
ALTER TABLE blogs DROP COLUMN custom_domain;
@@ -0,0 +1,2 @@
--- Adding custom domain to Blog as an optional field
ALTER TABLE blogs ADD COLUMN custom_domain VARCHAR DEFAULT NULL UNIQUE;
@@ -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(())
--#!}
@@ -0,0 +1,56 @@
-- undo the adding of "custom_domain" to blogs
CREATE TABLE IF NOT EXISTS "blogs_drop_custom_domain" (
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_drop_custom_domain (
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
) 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_drop_custom_domain" RENAME to blogs;
@@ -0,0 +1,57 @@
-- add custom_domain to blogs
CREATE TABLE IF NOT EXISTS "blogs_add_custom_domain" (
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,
custom_domain text default NULL UNIQUE,
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
);
INSERT INTO blogs_add_custom_domain (
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
) 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_add_custom_domain" RENAME to blogs;
@@ -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 [%]';
+5 -11
View File
@@ -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
View File
@@ -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"),
+1 -1
View File
@@ -1,4 +1,4 @@
#![feature(associated_type_defaults)]
#![feature(custom_attribute, associated_type_defaults)]
extern crate activitypub;
#[macro_use]
+1 -1
View File
@@ -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>,
-2
View File
@@ -10,5 +10,3 @@ gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699f
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
View File
@@ -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
View File
@@ -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"));
+2 -4
View File
@@ -23,10 +23,8 @@ serde_derive = "1.0"
serde_json = "1.0"
tantivy = "0.10.1"
url = "2.1"
walkdir = "2.2"
webfinger = "0.4.1"
whatlang = "0.7.1"
shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2"
[dependencies.chrono]
@@ -50,5 +48,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"]
+1 -17
View File
@@ -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, ()))
}
}
}
+142 -59
View File
@@ -1,17 +1,17 @@
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},
rsa::Rsa,
sign::{Signer, Verifier},
};
use rocket::{
http::RawStr,
outcome::IntoOutcome,
request::{self, FromFormValue, FromRequest, Request},
};
use serde_json;
use url::Url;
use webfinger::*;
@@ -26,11 +26,70 @@ use posts::Post;
use safe_string::SafeString;
use schema::blogs;
use search::Searcher;
use std::default::Default;
use std::fmt::{self, Display};
use std::ops::Deref;
use std::sync::RwLock;
use users::User;
use {ap_url, Connection, Error, PlumeRocket, Result, ITEMS_PER_PAGE};
use {Connection, Error, PlumeRocket, Result};
pub type CustomGroup = CustomObject<ApSignature, Group>;
#[derive(Clone, Debug, PartialEq, DieselNewType)]
pub struct Host(String);
impl Host {
pub fn new(host: impl ToString) -> Host {
Host(host.to_string())
}
}
impl Deref for Host {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl Display for Host {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Host {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<'a, 'r> FromRequest<'a, 'r> for Host {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Host, ()> {
request
.headers()
.get_one("Host")
.and_then(|x| {
if Blog::list_custom_domains().contains(&x.to_string()) {
Some(Host::new(x))
} else {
None
}
})
.or_forward(())
}
}
impl<'v> FromFormValue<'v> for Host {
type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> std::result::Result<Host, &'v RawStr> {
let val = String::from_form_value(form_value)?;
Ok(Host::new(&val))
}
}
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
#[changeset_options(treat_none_as_null = "true")]
pub struct Blog {
@@ -49,7 +108,7 @@ pub struct Blog {
pub summary_html: SafeString,
pub icon_id: Option<i32>,
pub banner_id: Option<i32>,
pub theme: Option<String>,
pub custom_domain: Option<Host>,
}
#[derive(Default, Insertable)]
@@ -67,11 +126,15 @@ pub struct NewBlog {
pub summary_html: SafeString,
pub icon_id: Option<i32>,
pub banner_id: Option<i32>,
pub theme: Option<String>,
pub custom_domain: Option<Host>,
}
const BLOG_PREFIX: &str = "~";
lazy_static! {
static ref CUSTOM_DOMAINS: RwLock<Vec<String>> = RwLock::new(vec![]);
}
impl Blog {
insert!(blogs, NewBlog, |inserted, conn| {
let instance = inserted.get_instance(conn)?;
@@ -140,8 +203,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 {
@@ -149,6 +214,13 @@ impl Blog {
}
}
pub fn find_by_host(c: &PlumeRocket, host: Host) -> Result<Blog> {
blogs::table
.filter(blogs::custom_domain.eq(host))
.first::<Blog>(&*c.conn)
.map_err(|_| Error::NotFound)
}
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
.links
@@ -225,49 +297,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(
@@ -311,10 +346,23 @@ impl Blog {
})
}
pub fn url(&self) -> String {
format!(
"https://{}",
self.custom_domain
.clone()
.unwrap_or_else(|| Host::new(format!(
"{}/~/{}",
Instance::get_local().unwrap().public_domain,
self.fqn,
)))
)
}
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> {
@@ -332,6 +380,23 @@ impl Blog {
.map(|_| ())
.map_err(Error::from)
}
pub fn list_custom_domains() -> Vec<String> {
CUSTOM_DOMAINS.read().unwrap().clone()
}
pub fn cache_custom_domains(conn: &Connection) {
*CUSTOM_DOMAINS.write().unwrap() = Blog::list_custom_domains_uncached(conn).unwrap();
}
pub fn list_custom_domains_uncached(conn: &Connection) -> Result<Vec<String>> {
blogs::table
.filter(blogs::custom_domain.is_not_null())
.select(blogs::custom_domain)
.load::<Option<String>>(conn)
.map_err(Error::from)
.map(|res| res.into_iter().map(Option::unwrap).collect::<Vec<_>>())
}
}
impl IntoId for Blog {
@@ -434,7 +499,7 @@ impl FromId<PlumeRocket> for Blog {
.summary_string()
.unwrap_or_default(),
),
theme: None,
custom_domain: None,
},
)
}
@@ -484,6 +549,7 @@ impl NewBlog {
title: String,
summary: String,
instance_id: i32,
custom_domain: Option<Host>,
) -> Result<NewBlog> {
let (pub_key, priv_key) = sign::gen_keypair();
Ok(NewBlog {
@@ -493,6 +559,7 @@ impl NewBlog {
instance_id,
public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
custom_domain,
..NewBlog::default()
})
}
@@ -520,6 +587,7 @@ pub(crate) mod tests {
"Blog name".to_owned(),
"This is a small blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -531,6 +599,7 @@ pub(crate) mod tests {
"My blog".to_owned(),
"Welcome to my blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("blog.myname.me")),
)
.unwrap(),
)
@@ -542,6 +611,7 @@ pub(crate) mod tests {
"Why I like Plume".to_owned(),
"In this blog I will explay you why I like Plume so much".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -602,6 +672,7 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("some.blog.com")),
)
.unwrap(),
)
@@ -612,8 +683,9 @@ pub(crate) mod tests {
Instance::get_local().unwrap().id
);
// TODO add tests for remote instance
Ok(())
})
});
}
#[test]
@@ -629,6 +701,7 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -640,6 +713,7 @@ pub(crate) mod tests {
"Blog".to_owned(),
"I've named my blog Blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("named.example.blog")),
)
.unwrap(),
)
@@ -713,8 +787,9 @@ pub(crate) mod tests {
.unwrap()
.iter()
.any(|b| b.id == blog[1].id));
Ok(())
})
});
}
#[test]
@@ -731,14 +806,16 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
.unwrap();
assert_eq!(Blog::find_by_fqn(&r, "SomeName").unwrap().id, blog.id);
Ok(())
})
});
}
#[test]
@@ -754,14 +831,16 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("some.blog.com")),
)
.unwrap(),
)
.unwrap();
assert_eq!(blog.fqn, "SomeName");
Ok(())
})
});
}
#[test]
@@ -772,8 +851,9 @@ pub(crate) mod tests {
blogs[0].delete(conn, &get_searcher()).unwrap();
assert!(Blog::get(conn, blogs[0].id).is_err());
Ok(())
})
});
}
#[test]
@@ -790,6 +870,7 @@ pub(crate) mod tests {
"Some name".to_owned(),
"This is some blog".to_owned(),
Instance::get_local().unwrap().id,
None,
)
.unwrap(),
)
@@ -801,6 +882,7 @@ pub(crate) mod tests {
"Blog".to_owned(),
"I've named my blog Blog".to_owned(),
Instance::get_local().unwrap().id,
Some(Host::new("my.blog.com")),
)
.unwrap(),
)
@@ -842,8 +924,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 +987,6 @@ pub(crate) mod tests {
assert_eq!(blog.banner_url(conn), blogs[0].banner_url(conn));
Ok(())
})
});
}
}
+2 -1
View File
@@ -436,7 +436,8 @@ mod tests {
}
_ => panic!("Unexpected result"),
};
Ok(())
})
});
}
}
-5
View File
@@ -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()),
};
}
-15
View File
@@ -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())
}
}
}
+1 -2
View File
@@ -235,8 +235,7 @@ mod tests {
)
.expect("Couldn't insert new follow");
assert_eq!(follow.ap_url, String::from("https://some.url/"));
Ok(())
})
});
}
}
+12 -1
View File
@@ -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(())
});
}
+15 -64
View File
@@ -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(())
});
}
+22 -23
View File
@@ -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;
@@ -11,6 +10,8 @@ extern crate bcrypt;
extern crate chrono;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_derive_newtype;
extern crate guid_create;
extern crate heck;
extern crate itertools;
@@ -34,7 +35,6 @@ extern crate serde_json;
#[macro_use]
extern crate tantivy;
extern crate url;
extern crate walkdir;
extern crate webfinger;
extern crate whatlang;
@@ -64,7 +64,6 @@ pub enum Error {
SerDe,
Search(search::SearcherError),
Signature,
TimelineQuery(timeline::query::QueryError),
Unauthorized,
Url,
Webfinger,
@@ -76,7 +75,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 +139,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 +175,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 +225,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 +253,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 +279,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 +303,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 +332,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 +366,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 +376,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;
-3
View File
@@ -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)
}
}
-554
View File
@@ -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
View File
@@ -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(())
});
}
+1 -1
View File
@@ -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 {
+2 -12
View File
@@ -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))
+69 -6
View File
@@ -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(())
});
}
-3
View File
@@ -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)
}
}
-3
View File
@@ -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()),
+78 -128
View File
@@ -1,17 +1,17 @@
table! {
api_tokens (id) {
id -> Int4,
id -> Integer,
creation_date -> Timestamp,
value -> Text,
scopes -> Text,
app_id -> Int4,
user_id -> Int4,
app_id -> Integer,
user_id -> Integer,
}
}
table! {
apps (id) {
id -> Int4,
id -> Integer,
name -> Text,
client_id -> Text,
client_secret -> Text,
@@ -23,71 +23,71 @@ table! {
table! {
blog_authors (id) {
id -> Int4,
blog_id -> Int4,
author_id -> Int4,
id -> Integer,
blog_id -> Integer,
author_id -> Integer,
is_owner -> Bool,
}
}
table! {
blogs (id) {
id -> Int4,
actor_id -> Varchar,
title -> Varchar,
id -> Integer,
actor_id -> Text,
title -> Text,
summary -> Text,
outbox_url -> Varchar,
inbox_url -> Varchar,
instance_id -> Int4,
outbox_url -> Text,
inbox_url -> Text,
instance_id -> Integer,
creation_date -> Timestamp,
ap_url -> Text,
private_key -> Nullable<Text>,
public_key -> Text,
fqn -> Text,
summary_html -> Text,
icon_id -> Nullable<Int4>,
banner_id -> Nullable<Int4>,
theme -> Nullable<Varchar>,
icon_id -> Nullable<Integer>,
banner_id -> Nullable<Integer>,
custom_domain -> Nullable<Text>,
}
}
table! {
comment_seers (id) {
id -> Integer,
comment_id -> Integer,
user_id -> Integer,
}
}
table! {
comments (id) {
id -> Int4,
id -> Integer,
content -> Text,
in_response_to_id -> Nullable<Int4>,
post_id -> Int4,
author_id -> Int4,
in_response_to_id -> Nullable<Integer>,
post_id -> Integer,
author_id -> Integer,
creation_date -> Timestamp,
ap_url -> Nullable<Varchar>,
ap_url -> Nullable<Text>,
sensitive -> Bool,
spoiler_text -> Text,
public_visibility -> Bool,
}
}
table! {
comment_seers (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
}
}
table! {
follows (id) {
id -> Int4,
follower_id -> Int4,
following_id -> Int4,
id -> Integer,
follower_id -> Integer,
following_id -> Integer,
ap_url -> Text,
}
}
table! {
instances (id) {
id -> Int4,
public_domain -> Varchar,
name -> Varchar,
id -> Integer,
public_domain -> Text,
name -> Text,
local -> Bool,
blocked -> Bool,
creation_date -> Timestamp,
@@ -95,70 +95,50 @@ table! {
short_description -> Text,
long_description -> Text,
default_license -> Text,
long_description_html -> Varchar,
short_description_html -> Varchar,
long_description_html -> Text,
short_description_html -> Text,
}
}
table! {
likes (id) {
id -> Int4,
user_id -> Int4,
post_id -> Int4,
id -> Integer,
user_id -> Integer,
post_id -> Integer,
creation_date -> Timestamp,
ap_url -> Varchar,
}
}
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,
ap_url -> Text,
}
}
table! {
medias (id) {
id -> Int4,
id -> Integer,
file_path -> Text,
alt_text -> Text,
is_remote -> Bool,
remote_url -> Nullable<Text>,
sensitive -> Bool,
content_warning -> Nullable<Text>,
owner_id -> Int4,
owner_id -> Integer,
}
}
table! {
mentions (id) {
id -> Int4,
mentioned_id -> Int4,
post_id -> Nullable<Int4>,
comment_id -> Nullable<Int4>,
id -> Integer,
mentioned_id -> Integer,
post_id -> Nullable<Integer>,
comment_id -> Nullable<Integer>,
}
}
table! {
notifications (id) {
id -> Int4,
user_id -> Int4,
id -> Integer,
user_id -> Integer,
creation_date -> Timestamp,
kind -> Varchar,
object_id -> Int4,
kind -> Text,
object_id -> Integer,
}
}
@@ -173,89 +153,70 @@ table! {
table! {
post_authors (id) {
id -> Int4,
post_id -> Int4,
author_id -> Int4,
id -> Integer,
post_id -> Integer,
author_id -> Integer,
}
}
table! {
posts (id) {
id -> Int4,
blog_id -> Int4,
slug -> Varchar,
title -> Varchar,
id -> Integer,
blog_id -> Integer,
slug -> Text,
title -> Text,
content -> Text,
published -> Bool,
license -> Varchar,
license -> Text,
creation_date -> Timestamp,
ap_url -> Varchar,
ap_url -> Text,
subtitle -> Text,
source -> Text,
cover_id -> Nullable<Int4>,
cover_id -> Nullable<Integer>,
}
}
table! {
reshares (id) {
id -> Int4,
user_id -> Int4,
post_id -> Int4,
ap_url -> Varchar,
id -> Integer,
user_id -> Integer,
post_id -> Integer,
ap_url -> Text,
creation_date -> Timestamp,
}
}
table! {
tags (id) {
id -> Int4,
id -> Integer,
tag -> Text,
is_hashtag -> Bool,
post_id -> Int4,
}
}
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,
post_id -> Integer,
}
}
table! {
users (id) {
id -> Int4,
username -> Varchar,
display_name -> Varchar,
outbox_url -> Varchar,
inbox_url -> Varchar,
id -> Integer,
username -> Text,
display_name -> Text,
outbox_url -> Text,
inbox_url -> Text,
is_admin -> Bool,
summary -> Text,
email -> Nullable<Text>,
hashed_password -> Nullable<Text>,
instance_id -> Int4,
instance_id -> Integer,
creation_date -> Timestamp,
ap_url -> Text,
private_key -> Nullable<Text>,
public_key -> Text,
shared_inbox_url -> Nullable<Varchar>,
followers_endpoint -> Varchar,
avatar_id -> Nullable<Int4>,
shared_inbox_url -> Nullable<Text>,
followers_endpoint -> Text,
avatar_id -> Nullable<Integer>,
last_fetched_date -> Timestamp,
fqn -> Text,
summary_html -> Text,
role -> Int4,
preferred_theme -> Nullable<Varchar>,
hide_custom_css -> Bool,
}
}
@@ -270,10 +231,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 +242,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!(
@@ -295,13 +249,11 @@ allow_tables_to_appear_in_same_query!(
apps,
blog_authors,
blogs,
comments,
comment_seers,
comments,
follows,
instances,
likes,
list_elems,
lists,
medias,
mentions,
notifications,
@@ -310,7 +262,5 @@ allow_tables_to_appear_in_same_query!(
posts,
reshares,
tags,
timeline,
timeline_definition,
users,
);
+6 -4
View File
@@ -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(())
});
}
+2 -2
View File
@@ -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();
+2 -6
View File
@@ -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();
-844
View File
@@ -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(())
});
}
}
-885
View File
@@ -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),
]
);
}
}
+137 -186
View File
@@ -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> {
@@ -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(())
});
}
+468 -498
View File
File diff suppressed because it is too large Load Diff
+434 -461
View File
File diff suppressed because it is too large Load Diff
+446 -476
View File
File diff suppressed because it is too large Load Diff
+468 -498
View File
File diff suppressed because it is too large Load Diff
+472 -502
View File
File diff suppressed because it is too large Load Diff
+411 -438
View File
File diff suppressed because it is too large Load Diff
+416 -443
View File
File diff suppressed because it is too large Load Diff
+466 -496
View File
File diff suppressed because it is too large Load Diff
+486 -516
View File
File diff suppressed because it is too large Load Diff
+471 -501
View File
File diff suppressed because it is too large Load Diff
+447 -474
View File
File diff suppressed because it is too large Load Diff
+436 -463
View File
File diff suppressed because it is too large Load Diff
+465 -495
View File
File diff suppressed because it is too large Load Diff
+475 -505
View File
File diff suppressed because it is too large Load Diff
+488 -518
View File
File diff suppressed because it is too large Load Diff
+467 -497
View File
File diff suppressed because it is too large Load Diff
+406 -426
View File
File diff suppressed because it is too large Load Diff
+462 -492
View File
File diff suppressed because it is too large Load Diff
+436 -463
View File
File diff suppressed because it is too large Load Diff
+430 -460
View File
File diff suppressed because it is too large Load Diff
+469 -499
View File
File diff suppressed because it is too large Load Diff
+414 -441
View File
File diff suppressed because it is too large Load Diff
+411 -438
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
nightly-2019-12-17
nightly-2019-03-23
-1
View File
@@ -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'
-53
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
openssl rand -base64 32 > ${SNAP_COMMON}/rocket-secret-key
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
cd ${SNAP}
exec ./set-environment bin/plm migration run --path ${SNAP_DATA}
-30
View File
@@ -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 $@
-60
View File
@@ -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

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