Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c4abcff81 | |||
| d242e6df11 | |||
| f395c1046c | |||
| df7adaf0e0 | |||
| 768f126f1d | |||
| 169469816d | |||
| 064dd79eef | |||
| df47cddb87 | |||
| f67ce93d94 | |||
| 38ece9b5a6 | |||
| 5e46922ed0 | |||
| 6072351840 | |||
| 444a4673f4 | |||
| ed30284386 | |||
| bf1673dda1 | |||
| ff0c82efc1 | |||
| da6757c55c | |||
| 60270121dc | |||
| fdc7da0edf | |||
| 2dedcdbc53 | |||
| b172a80e35 | |||
| c5f6b88b1d | |||
| 6cd8bd89b2 | |||
| cdc919e308 | |||
| f635dcf6c3 | |||
| 7139119b8f | |||
| 203da23cf2 | |||
| f73fba583a | |||
| fe110b5d8a | |||
| cc0df4ecb2 | |||
| 9cee38ae6a | |||
| 8e7f789969 | |||
| 92fbd174eb | |||
| 6253adf768 | |||
| a0aef50674 | |||
| 8e6b1ab86e | |||
| 1c34ac38f7 | |||
| 468e663344 | |||
| 0645f7e253 | |||
| f94b0c79c5 | |||
| b09b51c74b | |||
| e6747de998 | |||
| 2746e088ae | |||
| 3a4c2f2cf9 | |||
| 351c01f71c | |||
| 65ae51b7e5 | |||
| 92bbeeb45e | |||
| 6bcc4f0ab0 | |||
| fb60236a54 | |||
| 4f7c20fc26 | |||
| 54c6d21fc5 | |||
| c031804464 | |||
| 5289fe872a | |||
| 7ea4acc289 | |||
| 6dbc043a7e | |||
| fb1668ede7 | |||
| 3d27e283ad | |||
| 4b205fa995 | |||
| e7126ae335 | |||
| b2312d7f1b |
+2
-46
@@ -1,46 +1,2 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, sexual identity and orientation, etc.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at baptiste@gelez.xyz. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
You can read our Code of Conduct [here](https://docs.joinplu.me/organization/code-of-conduct).
|
||||
By contributing to this repository, you agree to be bound by this Code of Conduct.
|
||||
|
||||
Generated
+457
-189
File diff suppressed because it is too large
Load Diff
+7
-5
@@ -8,6 +8,7 @@ repository = "https://github.com/Plume-org/Plume"
|
||||
activitypub = "0.1.3"
|
||||
askama_escape = "0.1"
|
||||
atom_syndication = "0.6"
|
||||
clap = "2.33"
|
||||
colored = "1.8"
|
||||
dotenv = "0.14"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
@@ -17,20 +18,21 @@ guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
lettre = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||
lettre_email = { git = "https://github.com/lettre/lettre", rev = "c988b1760ad8179d9e7f3fb8594d2b86cf2a0a49" }
|
||||
num_cpus = "1.0"
|
||||
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 = "3.0"
|
||||
rpassword = "4.0"
|
||||
runtime-fmt = "0.3.0"
|
||||
scheduled-thread-pool = "0.2.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_qs = "0.4"
|
||||
serde_qs = "0.5"
|
||||
shrinkwraprs = "0.2.1"
|
||||
validator = "0.8"
|
||||
validator_derive = "0.8"
|
||||
webfinger = "0.3.1"
|
||||
webfinger = "0.4.1"
|
||||
|
||||
[[bin]]
|
||||
name = "plume"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<!--
|
||||
If your issue is not purely technical, it should be debated on Loomio first
|
||||
|
||||
https://framavox.org/g/WK40YHMA/plume
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Plume version (and/or commit):
|
||||
@@ -3,27 +3,53 @@
|
||||
Plume
|
||||
</h1>
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/Plume-org/Plume"><img src="https://travis-ci.org/Plume-org/Plume.svg?branch=master" alt="Build status"></a>
|
||||
<a href="hhttps://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
|
||||
<a href="https://github.com/Plume-org/Plume/"><img alt="CircleCI" src="https://img.shields.io/circleci/build/gh/Plume-org/Plume.svg"></a>
|
||||
<a href="https://codecov.io/gh/Plume-org/Plume"><img src="https://codecov.io/gh/Plume-org/Plume/branch/master/graph/badge.svg" alt="Code coverage"></a>
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/plume"><img src="https://d322cqt584bo4o.cloudfront.net/plume/localized.svg"></a>
|
||||
<a href="https://hub.docker.com/r/plumeorg/plume"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/plumeorg/plume.svg"></a>
|
||||
<a href="https://liberapay.com/Plume"><img alt="Liberapay patrons" src="https://img.shields.io/liberapay/patrons/Plume.svg"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://joinplu.me/">Website</a>
|
||||
—
|
||||
<a href="https://docs.joinplu.me/">Documentation</a>
|
||||
—
|
||||
<a href="https://contribute.joinplu.me/">Contribute</a>
|
||||
<a href="https://docs.joinplu.me/contribute">Contribute</a>
|
||||
—
|
||||
<a href="https://joinplu.me/#instances">Instances list</a>
|
||||
</p>
|
||||
|
||||
Plume is a federated blogging engine, based on ActivityPub. It uses the Rocket framework, and Diesel to interact with the database.
|
||||
Plume is a **federated blogging engine**, based on *ActivityPub*. It is written in *Rust*, with the *Rocket* framework, and *Diesel* to interact with the database.
|
||||
The front-end uses *Ructe* templates, *WASM* and *SCSS*.
|
||||
|
||||
It is not yet ready for production use, but we have all the basic features (account management, blogs, articles, comments, etc) and a basic federation.
|
||||
## Features
|
||||
|
||||
Feel free to join our Matrix room: `#plume:disroot.org` to discuss about the project!
|
||||
A lot of features are still missing, but what is already here should be quite stable. Current and planned features include:
|
||||
|
||||
We also have a [Loomio group](https://framavox.org/g/WK40YHMA/plume) to debate and vote and features. Feel free to join it to take part in Plume's decision process.
|
||||
- **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.
|
||||
|
||||
For more informations on the different ways to contribute, check out our [contribution page](https://contribute.joinplu.me). We are welcoming any kind of contribution, and you can probably find a way to help.
|
||||
## Get involved
|
||||
|
||||
As we want the various spaces related to the project (GitHub, Matrix, Loomio, etc) to be as safe as possible for everyone, we adopted [a code of conduct](https://github.com/Plume-org/Plume/blob/master/CODE_OF_CONDUCT.md). Please read it and make sure you accept it before contributing.
|
||||
If you want to have regular news about the project, the best place is probably [our blog](https://fediverse.blog/~/PlumeDev), or our Matrix room: [`#plume:disroot.org`](https://riot.im/app/#/room/#plume:disroot.org).
|
||||
|
||||
If you want to contribute more, a good first step is to read [our contribution guides](https://docs.joinplu.me/contribute). We accept all kind of contribution:
|
||||
|
||||
- [Back-end or front-end development](https://docs.joinplu.me/contribute/development/)
|
||||
- [Translations](https://docs.joinplu.me/contribute/translations/)
|
||||
- [Documentation](https://docs.joinplu.me/contribute/documentation/)
|
||||
- UI and/or UX design (we don't have a dedicated guide yet, but [we can talk](https://docs.joinplu.me/contribute/discussion/) to see how we can work together!)
|
||||
- [Taking part in discussions](https://docs.joinplu.me/contribute/discussion/)
|
||||
- [Financial support](https://docs.joinplu.me/contribute/donations/)
|
||||
|
||||
But this list is not exhaustive and if you want to contribute differently you are welcome too!
|
||||
|
||||
As we want the various spaces related to the project (GitHub, Matrix, Loomio, etc) to be as safe as possible for everyone, we adopted [a code of conduct](https://docs.joinplu.me/organization/code-of-conduct). Please read it and make sure you accept it before contributing.
|
||||
|
||||
## Starting your own instance
|
||||
|
||||
We provide various way to install Plume: from source, with pre-built binaries, with Docker or with YunoHost.
|
||||
For detailed explanations, please refer to [the documentation](https://docs.joinplu.me/installation/).
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE password_reset_requests;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE password_reset_requests (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR NOT NULL,
|
||||
token VARCHAR NOT NULL,
|
||||
expiration_date TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX password_reset_requests_token ON password_reset_requests (token);
|
||||
CREATE UNIQUE INDEX password_reset_requests_email ON password_reset_requests (email);
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE password_reset_requests;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE password_reset_requests (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
email VARCHAR NOT NULL,
|
||||
token VARCHAR NOT NULL,
|
||||
expiration_date DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX password_reset_requests_token ON password_reset_requests (token);
|
||||
CREATE UNIQUE INDEX password_reset_requests_email ON password_reset_requests (email);
|
||||
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
dotenv = "0.14"
|
||||
rpassword = "3.0"
|
||||
rpassword = "4.0"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["r2d2", "chrono"]
|
||||
|
||||
@@ -25,7 +25,11 @@ fn main() {
|
||||
.subcommand(users::command());
|
||||
let matches = app.clone().get_matches();
|
||||
|
||||
dotenv::dotenv().ok();
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => println!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str());
|
||||
let _ = conn.as_ref().map(|conn| Instance::cache_local(conn));
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
activitypub = "0.1.1"
|
||||
activitystreams-derive = "0.1.0"
|
||||
activitystreams-derive = "0.1.1"
|
||||
activitystreams-traits = "0.1.0"
|
||||
array_tool = "1.0"
|
||||
base64 = "0.10"
|
||||
heck = "0.3.0"
|
||||
hex = "0.3"
|
||||
hyper = "0.12.28"
|
||||
hyper = "0.12.33"
|
||||
openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
reqwest = "0.9"
|
||||
@@ -19,6 +19,7 @@ serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shrinkwraprs = "0.2.1"
|
||||
tokio = "0.1.22"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use activitypub::{Activity, Link, Object};
|
||||
use array_tool::vec::Uniq;
|
||||
use reqwest::ClientBuilder;
|
||||
use reqwest::r#async::ClientBuilder;
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::{FromRequest, Request},
|
||||
@@ -8,6 +8,7 @@ use rocket::{
|
||||
Outcome,
|
||||
};
|
||||
use serde_json;
|
||||
use tokio::prelude::*;
|
||||
|
||||
use self::sign::Signable;
|
||||
|
||||
@@ -129,15 +130,17 @@ where
|
||||
.sign(sender)
|
||||
.expect("activity_pub::broadcast: signature error");
|
||||
|
||||
let mut rt = tokio::runtime::current_thread::Runtime::new()
|
||||
.expect("Error while initializing tokio runtime for federation");
|
||||
let client = ClientBuilder::new()
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("Can't build client");
|
||||
for inbox in boxes {
|
||||
// TODO: run it in Sidekiq or something like that
|
||||
let body = signed.to_string();
|
||||
let mut headers = request::headers();
|
||||
headers.insert("Digest", request::Digest::digest(&body));
|
||||
let res = ClientBuilder::new()
|
||||
.connect_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.build()
|
||||
.and_then(|client| {
|
||||
rt.spawn(
|
||||
client
|
||||
.post(&inbox)
|
||||
.headers(headers.clone())
|
||||
@@ -148,19 +151,15 @@ where
|
||||
)
|
||||
.body(body)
|
||||
.send()
|
||||
});
|
||||
match res {
|
||||
Ok(mut r) => {
|
||||
.and_then(|r| r.into_body().concat2())
|
||||
.map(move |response| {
|
||||
println!("Successfully sent activity to inbox ({})", inbox);
|
||||
if let Ok(response) = r.text() {
|
||||
println!("Response: \"{:?}\"\n\n", response)
|
||||
} else {
|
||||
println!("Error while reading response")
|
||||
}
|
||||
}
|
||||
Err(e) => println!("Error while sending to inbox ({:?})", e),
|
||||
}
|
||||
println!("Response: \"{:?}\"\n", response)
|
||||
})
|
||||
.map_err(|e| println!("Error while sending to inbox ({:?})", e)),
|
||||
);
|
||||
}
|
||||
rt.run().unwrap();
|
||||
}
|
||||
|
||||
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -20,6 +20,7 @@ extern crate shrinkwraprs;
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate tokio;
|
||||
|
||||
pub mod activity_pub;
|
||||
pub mod utils;
|
||||
|
||||
@@ -4,8 +4,8 @@ version = "0.3.0"
|
||||
authors = ["Plume contributors"]
|
||||
|
||||
[dependencies]
|
||||
stdweb = "=0.4.14"
|
||||
stdweb-internal-runtime = "=0.1.3"
|
||||
stdweb = "=0.4.18"
|
||||
stdweb-internal-runtime = "=0.1.4"
|
||||
gettext = { git = "https://github.com/Plume-org/gettext/", rev = "294c54d74c699fbc66502b480a37cc66c1daa7f3" }
|
||||
gettext-macros = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
gettext-utils = { git = "https://github.com/Plume-org/gettext-macros/", rev = "a7c605f7edd6bfbfbfe7778026bfefd88d82db10" }
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = ["Plume contributors"]
|
||||
activitypub = "0.1.1"
|
||||
ammonia = "2.1.1"
|
||||
askama_escape = "0.1"
|
||||
bcrypt = "0.4"
|
||||
bcrypt = "0.5"
|
||||
guid-create = "0.1"
|
||||
heck = "0.3.0"
|
||||
itertools = "0.8.0"
|
||||
@@ -17,15 +17,14 @@ openssl = "0.10.22"
|
||||
rocket = "0.4.0"
|
||||
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
|
||||
reqwest = "0.9"
|
||||
scheduled-thread-pool = "0.2.0"
|
||||
scheduled-thread-pool = "0.2.2"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
tantivy = "0.9.1"
|
||||
url = "1.7"
|
||||
webfinger = "0.3.1"
|
||||
tantivy = "0.10.1"
|
||||
url = "2.1"
|
||||
webfinger = "0.4.1"
|
||||
whatlang = "0.7.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
diesel-derive-newtype = "0.1.2"
|
||||
|
||||
[dependencies.chrono]
|
||||
|
||||
+121
-1
@@ -7,6 +7,11 @@ use openssl::{
|
||||
rsa::Rsa,
|
||||
sign::{Signer, Verifier},
|
||||
};
|
||||
use rocket::{
|
||||
http::RawStr,
|
||||
outcome::IntoOutcome,
|
||||
request::{self, FromFormValue, FromRequest, Request},
|
||||
};
|
||||
use serde_json;
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
@@ -21,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 {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 {
|
||||
@@ -44,6 +108,7 @@ pub struct Blog {
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
pub custom_domain: Option<Host>,
|
||||
}
|
||||
|
||||
#[derive(Default, Insertable)]
|
||||
@@ -61,10 +126,15 @@ pub struct NewBlog {
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
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)?;
|
||||
@@ -144,8 +214,15 @@ 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(acct.to_owned(), true)?
|
||||
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)?
|
||||
.links
|
||||
.into_iter()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
@@ -269,6 +346,19 @@ 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())
|
||||
@@ -290,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 {
|
||||
@@ -392,6 +499,7 @@ impl FromId<PlumeRocket> for Blog {
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
custom_domain: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -441,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 {
|
||||
@@ -450,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()
|
||||
})
|
||||
}
|
||||
@@ -477,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(),
|
||||
)
|
||||
@@ -488,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(),
|
||||
)
|
||||
@@ -499,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(),
|
||||
)
|
||||
@@ -559,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(),
|
||||
)
|
||||
@@ -587,6 +701,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -598,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(),
|
||||
)
|
||||
@@ -690,6 +806,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -714,6 +831,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(),
|
||||
)
|
||||
@@ -752,6 +870,7 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local().unwrap().id,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -763,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(),
|
||||
)
|
||||
|
||||
@@ -10,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;
|
||||
@@ -65,6 +67,7 @@ pub enum Error {
|
||||
Unauthorized,
|
||||
Url,
|
||||
Webfinger,
|
||||
Expired,
|
||||
}
|
||||
|
||||
impl From<bcrypt::BcryptError> for Error {
|
||||
@@ -367,6 +370,7 @@ pub mod medias;
|
||||
pub mod mentions;
|
||||
pub mod migrations;
|
||||
pub mod notifications;
|
||||
pub mod password_reset_requests;
|
||||
pub mod plume_rocket;
|
||||
pub mod post_authors;
|
||||
pub mod posts;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
use chrono::{offset::Utc, Duration, NaiveDateTime};
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use schema::password_reset_requests;
|
||||
use {Connection, Error, Result};
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
pub struct PasswordResetRequest {
|
||||
pub id: i32,
|
||||
pub email: String,
|
||||
pub token: String,
|
||||
pub expiration_date: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "password_reset_requests"]
|
||||
pub struct NewPasswordResetRequest {
|
||||
pub email: String,
|
||||
pub token: String,
|
||||
pub expiration_date: NaiveDateTime,
|
||||
}
|
||||
|
||||
const TOKEN_VALIDITY_HOURS: i64 = 2;
|
||||
|
||||
impl PasswordResetRequest {
|
||||
pub fn insert(conn: &Connection, email: &str) -> Result<String> {
|
||||
// first, delete other password reset tokens associated with this email:
|
||||
let existing_requests =
|
||||
password_reset_requests::table.filter(password_reset_requests::email.eq(email));
|
||||
diesel::delete(existing_requests).execute(conn)?;
|
||||
|
||||
// now, generate a random token, set the expiry date,
|
||||
// and insert it into the DB:
|
||||
let token = plume_common::utils::random_hex();
|
||||
let expiration_date = Utc::now()
|
||||
.naive_utc()
|
||||
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
|
||||
.expect("could not calculate expiration date");
|
||||
let new_request = NewPasswordResetRequest {
|
||||
email: email.to_owned(),
|
||||
token: token.clone(),
|
||||
expiration_date,
|
||||
};
|
||||
diesel::insert_into(password_reset_requests::table)
|
||||
.values(new_request)
|
||||
.execute(conn)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn find_by_token(conn: &Connection, token: &str) -> Result<Self> {
|
||||
let token = password_reset_requests::table
|
||||
.filter(password_reset_requests::token.eq(token))
|
||||
.first::<Self>(conn)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
if token.expiration_date < Utc::now().naive_utc() {
|
||||
return Err(Error::Expired);
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
|
||||
let request = Self::find_by_token(&conn, &token)?;
|
||||
|
||||
let filter =
|
||||
password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
|
||||
diesel::delete(filter).execute(conn)?;
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use diesel::Connection;
|
||||
use tests::db;
|
||||
use users::tests as user_tests;
|
||||
|
||||
#[test]
|
||||
fn test_insert_and_find_password_reset_request() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
user_tests::fill_database(&conn);
|
||||
let admin_email = "admin@example.com";
|
||||
|
||||
let token = PasswordResetRequest::insert(&conn, admin_email)
|
||||
.expect("couldn't insert new request");
|
||||
let request = PasswordResetRequest::find_by_token(&conn, &token)
|
||||
.expect("couldn't retrieve request");
|
||||
|
||||
assert!(&token.len() > &32);
|
||||
assert_eq!(&request.email, &admin_email);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_delete_previous_password_reset_request() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
user_tests::fill_database(&conn);
|
||||
let admin_email = "admin@example.com";
|
||||
|
||||
PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
|
||||
PasswordResetRequest::insert(&conn, &admin_email)
|
||||
.expect("couldn't insert second request");
|
||||
|
||||
let count = password_reset_requests::table.count().get_result(&*conn);
|
||||
assert_eq!(Ok(1), count);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_password_reset_request_by_token_time() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
user_tests::fill_database(&conn);
|
||||
let admin_email = "admin@example.com";
|
||||
let token = "abcdef";
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
diesel::insert_into(password_reset_requests::table)
|
||||
.values((
|
||||
password_reset_requests::email.eq(&admin_email),
|
||||
password_reset_requests::token.eq(&token),
|
||||
password_reset_requests::expiration_date.eq(now),
|
||||
))
|
||||
.execute(&*conn)
|
||||
.expect("could not insert request");
|
||||
|
||||
match PasswordResetRequest::find_by_token(&conn, &token) {
|
||||
Err(Error::Expired) => (),
|
||||
_ => panic!("Received unexpected result finding expired token"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_and_delete_password_reset_request() {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
user_tests::fill_database(&conn);
|
||||
let admin_email = "admin@example.com";
|
||||
|
||||
let token = PasswordResetRequest::insert(&conn, &admin_email)
|
||||
.expect("couldn't insert new request");
|
||||
PasswordResetRequest::find_and_delete_by_token(&conn, &token)
|
||||
.expect("couldn't find and delete request");
|
||||
|
||||
let count = password_reset_requests::table.count().get_result(&*conn);
|
||||
assert_eq!(Ok(0), count);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
+87
-76
@@ -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,70 +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>,
|
||||
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,
|
||||
@@ -94,116 +95,125 @@ 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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
password_reset_requests (id) {
|
||||
id -> Int4,
|
||||
email -> Varchar,
|
||||
token -> Varchar,
|
||||
expiration_date -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
@@ -239,14 +249,15 @@ allow_tables_to_appear_in_same_query!(
|
||||
apps,
|
||||
blog_authors,
|
||||
blogs,
|
||||
comments,
|
||||
comment_seers,
|
||||
comments,
|
||||
follows,
|
||||
instances,
|
||||
likes,
|
||||
medias,
|
||||
mentions,
|
||||
notifications,
|
||||
password_reset_requests,
|
||||
post_authors,
|
||||
posts,
|
||||
reshares,
|
||||
|
||||
@@ -707,7 +707,7 @@ impl User {
|
||||
mime_type: None,
|
||||
href: None,
|
||||
template: Some(format!(
|
||||
"{}/remote_interact?{{uri}}",
|
||||
"https://{}/remote_interact?{{uri}}",
|
||||
self.get_instance(conn)?.public_domain
|
||||
)),
|
||||
},
|
||||
|
||||
+468
-498
File diff suppressed because it is too large
Load Diff
+434
-461
File diff suppressed because it is too large
Load Diff
+446
-476
File diff suppressed because it is too large
Load Diff
+468
-498
File diff suppressed because it is too large
Load Diff
+472
-502
File diff suppressed because it is too large
Load Diff
+411
-438
File diff suppressed because it is too large
Load Diff
+416
-443
File diff suppressed because it is too large
Load Diff
+466
-496
File diff suppressed because it is too large
Load Diff
+486
-516
File diff suppressed because it is too large
Load Diff
+471
-501
File diff suppressed because it is too large
Load Diff
+447
-474
File diff suppressed because it is too large
Load Diff
+436
-463
File diff suppressed because it is too large
Load Diff
+465
-495
File diff suppressed because it is too large
Load Diff
+475
-505
File diff suppressed because it is too large
Load Diff
+488
-518
File diff suppressed because it is too large
Load Diff
+467
-497
File diff suppressed because it is too large
Load Diff
+406
-426
File diff suppressed because it is too large
Load Diff
+462
-492
File diff suppressed because it is too large
Load Diff
+436
-463
File diff suppressed because it is too large
Load Diff
+430
-460
File diff suppressed because it is too large
Load Diff
+469
-499
File diff suppressed because it is too large
Load Diff
+414
-441
File diff suppressed because it is too large
Load Diff
+411
-438
File diff suppressed because it is too large
Load Diff
+2
-6
@@ -24,13 +24,9 @@ if [ $ARCH == "aarch64" -o $ARCH == "armv71" ] ; then
|
||||
apt-get install -y --no-install-recommends build-essential subversion ninja-build cmake
|
||||
mkdir -p /scratch/src
|
||||
cd /scratch/src
|
||||
# Pin LLVM to post 7.0.1 tag and pin to a known-good revision for Plume builds
|
||||
svn co -r350977 http://llvm.org/svn/llvm-project/llvm/trunk/ llvm
|
||||
svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_800/final/ llvm
|
||||
cd /scratch/src/llvm/tools
|
||||
# Pin lld to post 7.0.1 tag and pin to a known-good revision for Plume builds
|
||||
svn co -r350975 http://llvm.org/svn/llvm-project/lld/trunk lld
|
||||
#svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
|
||||
#svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra
|
||||
svn co http://llvm.org/svn/llvm-project/lld/tags/RELEASE_800/final/ lld
|
||||
mkdir -p /scratch/build/arm
|
||||
cd /scratch/build/arm
|
||||
if [ "$ARCH" == "aarch64" ] ; then
|
||||
|
||||
+65
-3
@@ -5,6 +5,7 @@ extern crate activitypub;
|
||||
extern crate askama_escape;
|
||||
extern crate atom_syndication;
|
||||
extern crate chrono;
|
||||
extern crate clap;
|
||||
extern crate colored;
|
||||
extern crate ctrlc;
|
||||
extern crate diesel;
|
||||
@@ -21,6 +22,7 @@ extern crate num_cpus;
|
||||
extern crate plume_api;
|
||||
extern crate plume_common;
|
||||
extern crate plume_models;
|
||||
extern crate reqwest;
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
@@ -38,19 +40,24 @@ extern crate validator;
|
||||
extern crate validator_derive;
|
||||
extern crate webfinger;
|
||||
|
||||
use clap::App;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
blogs::Host,
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
instance::Instance,
|
||||
migrations::IMPORTED_MIGRATIONS,
|
||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||
Connection, Error, CONFIG,
|
||||
};
|
||||
use rocket::{fairing::AdHoc, http::ext::IntoOwned, http::uri::Origin};
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use std::collections::HashMap;
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
init_i18n!(
|
||||
"plume", ar, bg, ca, cs, de, en, eo, es, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr, sk, sv
|
||||
@@ -73,7 +80,11 @@ compile_i18n!();
|
||||
|
||||
/// Initializes a database pool.
|
||||
fn init_pool() -> Option<DbPool> {
|
||||
dotenv::dotenv().ok();
|
||||
match dotenv::dotenv() {
|
||||
Ok(path) => println!("Configuration read from {}", path.display()),
|
||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||
e => e.map(|_| ()).unwrap(),
|
||||
}
|
||||
|
||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||
let pool = DbPool::builder()
|
||||
@@ -81,10 +92,24 @@ fn init_pool() -> Option<DbPool> {
|
||||
.build(manager)
|
||||
.ok()?;
|
||||
Instance::cache_local(&pool.get().unwrap());
|
||||
Blog::cache_custom_domains(&pool.get().unwrap());
|
||||
Some(pool)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new("Plume")
|
||||
.bin_name("plume")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Plume backend server")
|
||||
.after_help(
|
||||
r#"
|
||||
The plume command should be run inside the directory
|
||||
containing the `.env` configuration file and `static` directory.
|
||||
See https://docs.joinplu.me/installation/config
|
||||
and https://docs.joinplu.me/installation/init for more info.
|
||||
"#,
|
||||
)
|
||||
.get_matches();
|
||||
let dbpool = init_pool().expect("main: database pool initialization error");
|
||||
if IMPORTED_MIGRATIONS
|
||||
.is_pending(&dbpool.get().unwrap())
|
||||
@@ -156,7 +181,42 @@ Then try to restart Plume
|
||||
println!("Please refer to the documentation to see how to configure it.");
|
||||
}
|
||||
|
||||
let custom_domain_fairing = AdHoc::on_request("Custom Blog Domains", |req, _data| {
|
||||
let host = req.guard::<Host>();
|
||||
if host.is_success()
|
||||
&& req
|
||||
.uri()
|
||||
.segments()
|
||||
.next()
|
||||
.map(|path| path != "static" && path != "api")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let rewrite_uri = format!("/custom_domains/{}/{}", host.unwrap(), req.uri());
|
||||
let uri = Origin::parse_owned(rewrite_uri).unwrap();
|
||||
let uri = uri.to_normalized().into_owned();
|
||||
req.set_uri(uri);
|
||||
}
|
||||
});
|
||||
|
||||
let valid_domains: HashMap<String, Instant> = HashMap::new();
|
||||
let rocket = rocket::custom(CONFIG.rocket.clone().unwrap())
|
||||
.mount(
|
||||
"/custom_domains/domain_validation/",
|
||||
routes![routes::blogs::custom::domain_validation,],
|
||||
)
|
||||
.mount(
|
||||
"/domain_validation/",
|
||||
routes![routes::blogs::domain_validation,],
|
||||
)
|
||||
.mount(
|
||||
"/custom_domains/",
|
||||
routes![
|
||||
routes::blogs::custom::details,
|
||||
routes::posts::custom::details,
|
||||
routes::blogs::custom::activity_details,
|
||||
routes::search::custom::search,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
@@ -269,6 +329,7 @@ Then try to restart Plume
|
||||
.manage(dbpool)
|
||||
.manage(Arc::new(workpool))
|
||||
.manage(searcher)
|
||||
.manage(Mutex::new(valid_domains))
|
||||
.manage(include_i18n!())
|
||||
.attach(
|
||||
CsrfFairingBuilder::new()
|
||||
@@ -295,7 +356,8 @@ Then try to restart Plume
|
||||
])
|
||||
.finalize()
|
||||
.expect("main: csrf fairing creation error"),
|
||||
);
|
||||
)
|
||||
.attach(custom_domain_fairing);
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||
|
||||
+194
-29
@@ -2,11 +2,14 @@ use activitypub::collection::OrderedCollection;
|
||||
use atom_syndication::{Entry, FeedBuilder};
|
||||
use diesel::SaveChangesDsl;
|
||||
use rocket::{
|
||||
http::ContentType,
|
||||
http::{ContentType, Status},
|
||||
request::LenientForm,
|
||||
response::{content::Content, Flash, Redirect},
|
||||
State,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
@@ -16,14 +19,17 @@ use plume_models::{
|
||||
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
|
||||
users::User, Connection, PlumeRocket,
|
||||
};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use reqwest::Client;
|
||||
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
fn detail_guts(
|
||||
blog: Blog,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||
let posts = Post::blog_page(conn, &blog, page.limits())?;
|
||||
let articles_count = Post::count_for_blog(conn, &blog)?;
|
||||
let authors = &blog.list_authors(conn)?;
|
||||
@@ -35,7 +41,43 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
|
||||
page.0,
|
||||
Page::total(articles_count as i32),
|
||||
posts
|
||||
)))
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
pub fn details(
|
||||
name: String,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||
|
||||
// check this first, and return early
|
||||
// doing this prevents partially moving `blog` into the `match (tuple)`,
|
||||
// which makes it impossible to reuse then.
|
||||
if blog.custom_domain == None {
|
||||
return detail_guts(blog, page, rockets);
|
||||
}
|
||||
|
||||
match (blog.custom_domain, page) {
|
||||
(Some(ref custom_domain), Some(ref page)) => {
|
||||
Ok(Redirect::to(format!("https://{}/?page={}", custom_domain, page)).into())
|
||||
}
|
||||
(Some(ref custom_domain), _) => {
|
||||
Ok(Redirect::to(format!("https://{}/", custom_domain)).into())
|
||||
}
|
||||
// we need this match arm, or the match won't compile
|
||||
(None, _) => unreachable!("This code path should have already been handled!"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activity_detail_guts(
|
||||
blog: Blog,
|
||||
rockets: PlumeRocket,
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomGroup>> {
|
||||
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
|
||||
}
|
||||
|
||||
#[get("/~/<name>", rank = 1)]
|
||||
@@ -45,7 +87,7 @@ pub fn activity_details(
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomGroup>> {
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
|
||||
activity_detail_guts(blog, rockets, _ap)
|
||||
}
|
||||
|
||||
#[get("/blogs/new")]
|
||||
@@ -57,6 +99,76 @@ pub fn new(rockets: PlumeRocket, _user: User) -> Ructe {
|
||||
))
|
||||
}
|
||||
|
||||
// mounted as /domain_validation/
|
||||
#[get("/<validation_id>")]
|
||||
pub fn domain_validation(
|
||||
validation_id: String,
|
||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
||||
) -> Status {
|
||||
let mutex = valid_domains.inner().lock();
|
||||
let mut validation_map = mutex.unwrap();
|
||||
let validation_getter = validation_map.clone();
|
||||
|
||||
let value = validation_getter.get(&validation_id);
|
||||
if value.is_none() {
|
||||
// validation id not found
|
||||
return Status::NotFound;
|
||||
}
|
||||
|
||||
// we have valid id, now check the time
|
||||
let valid_until = value.unwrap();
|
||||
let now = Instant::now();
|
||||
|
||||
// nope, expired (410: gone)
|
||||
if now.duration_since(*valid_until).as_secs() > 0 {
|
||||
validation_map.remove(&validation_id);
|
||||
// validation expired
|
||||
return Status::Gone;
|
||||
}
|
||||
|
||||
validation_map.remove(&validation_id);
|
||||
Status::Ok
|
||||
}
|
||||
|
||||
pub mod custom {
|
||||
use plume_common::activity_pub::{ActivityStream, ApRequest};
|
||||
use plume_models::{blogs::Blog, blogs::CustomGroup, blogs::Host, PlumeRocket};
|
||||
use rocket::{http::Status, State};
|
||||
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
#[get("/<custom_domain>?<page>", rank = 2)]
|
||||
pub fn details(
|
||||
custom_domain: String,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain))?;
|
||||
super::detail_guts(blog, page, rockets)
|
||||
}
|
||||
|
||||
#[get("/<custom_domain>", rank = 1)]
|
||||
pub fn activity_details(
|
||||
custom_domain: String,
|
||||
rockets: PlumeRocket,
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomGroup>> {
|
||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain)).ok()?;
|
||||
super::activity_detail_guts(blog, rockets, _ap)
|
||||
}
|
||||
|
||||
// mounted as /custom_domains/domain_validation/
|
||||
#[get("/<validation_id>")]
|
||||
pub fn domain_validation(
|
||||
validation_id: String,
|
||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
||||
) -> Status {
|
||||
super::domain_validation(validation_id, valid_domains)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/blogs/new", rank = 2)]
|
||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
@@ -72,6 +184,7 @@ pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
pub struct NewBlogForm {
|
||||
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
|
||||
pub title: String,
|
||||
pub custom_domain: String,
|
||||
}
|
||||
|
||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
@@ -83,16 +196,43 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_domain(domain: &str, valid_domains: State<Mutex<HashMap<String, Instant>>>) -> bool {
|
||||
let mutex = valid_domains.inner().lock();
|
||||
let mut validation_map = mutex.unwrap();
|
||||
|
||||
let random_id = utils::random_hex();
|
||||
validation_map.insert(
|
||||
random_id.clone(),
|
||||
Instant::now().checked_add(Duration::new(60, 0)).unwrap(),
|
||||
);
|
||||
|
||||
let client = Client::new();
|
||||
let validation_uri = format!("https://{}/domain_validation/{}", domain, random_id);
|
||||
|
||||
match client.get(&validation_uri).send() {
|
||||
Ok(resp) => resp.status().is_success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/blogs/new", data = "<form>")]
|
||||
pub fn create(
|
||||
form: LenientForm<NewBlogForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
||||
) -> RespondOrRedirect {
|
||||
let slug = utils::make_actor_id(&form.title);
|
||||
let conn = &*rockets.conn;
|
||||
let intl = &rockets.intl.catalog;
|
||||
let user = rockets.user.clone().unwrap();
|
||||
|
||||
let (custom_domain, dns_ok) = if form.custom_domain.is_empty() {
|
||||
(None, true)
|
||||
} else {
|
||||
let dns_check = valid_domain(&form.custom_domain.clone(), valid_domains);
|
||||
(Some(Host::new(form.custom_domain.clone())), dns_check)
|
||||
};
|
||||
|
||||
let mut errors = match form.validate() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e,
|
||||
@@ -111,7 +251,10 @@ pub fn create(
|
||||
);
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
if !errors.is_empty() {
|
||||
return render!(blogs::new(&rockets.to_context(), &*form, errors)).into();
|
||||
}
|
||||
|
||||
let blog = Blog::insert(
|
||||
&*conn,
|
||||
NewBlog::new_local(
|
||||
@@ -121,6 +264,7 @@ pub fn create(
|
||||
Instance::get_local()
|
||||
.expect("blog::create: instance error")
|
||||
.id,
|
||||
custom_domain,
|
||||
)
|
||||
.expect("blog::create: new local error"),
|
||||
)
|
||||
@@ -136,17 +280,23 @@ pub fn create(
|
||||
)
|
||||
.expect("blog::create: author error");
|
||||
|
||||
Ok(Flash::success(
|
||||
if dns_ok {
|
||||
Flash::success(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created!"),
|
||||
))
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
Err(render!(blogs::new(&rockets.to_context(), &*form, errors)))
|
||||
Flash::warning(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created, but the custom domain seems invalid. Please check it is correct from your blog's settings."),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/~/<name>/delete")]
|
||||
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Flash<Redirect>, Ructe> {
|
||||
pub fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect {
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).expect("blog::delete: blog not found");
|
||||
|
||||
@@ -158,19 +308,21 @@ pub fn delete(name: String, rockets: PlumeRocket) -> Result<Flash<Redirect>, Ruc
|
||||
{
|
||||
blog.delete(&conn, &rockets.searcher)
|
||||
.expect("blog::expect: deletion error");
|
||||
Ok(Flash::success(
|
||||
Flash::success(
|
||||
Redirect::to(uri!(super::instance::index)),
|
||||
i18n!(rockets.intl.catalog, "Your blog was deleted."),
|
||||
))
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
// TODO actually return 403 error code
|
||||
Err(render!(errors::not_authorized(
|
||||
render!(errors::not_authorized(
|
||||
&rockets.to_context(),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You are not allowed to delete this blog."
|
||||
)
|
||||
)))
|
||||
))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +333,7 @@ pub struct EditForm {
|
||||
pub summary: String,
|
||||
pub icon: Option<i32>,
|
||||
pub banner: Option<i32>,
|
||||
pub custom_domain: String,
|
||||
}
|
||||
|
||||
#[get("/~/<name>/edit")]
|
||||
@@ -198,6 +351,10 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
.clone()
|
||||
.expect("blogs::edit: User was None while it shouldn't");
|
||||
let medias = Media::for_user(conn, user.id).expect("Couldn't list media");
|
||||
let custom_domain = match blog.custom_domain {
|
||||
Some(ref c) => c.to_string(),
|
||||
_ => String::from(""),
|
||||
};
|
||||
Ok(render!(blogs::edit(
|
||||
&rockets.to_context(),
|
||||
&blog,
|
||||
@@ -207,6 +364,7 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
summary: blog.summary.clone(),
|
||||
icon: blog.icon_id,
|
||||
banner: blog.banner_id,
|
||||
custom_domain: custom_domain,
|
||||
},
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
@@ -236,16 +394,27 @@ pub fn update(
|
||||
name: String,
|
||||
form: LenientForm<EditForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
) -> RespondOrRedirect {
|
||||
let conn = &*rockets.conn;
|
||||
let intl = &rockets.intl.catalog;
|
||||
let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found");
|
||||
if rockets
|
||||
if !rockets
|
||||
.user
|
||||
.clone()
|
||||
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// TODO actually return 403 error code
|
||||
return render!(errors::not_authorized(
|
||||
&rockets.to_context(),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You are not allowed to edit this blog."
|
||||
)
|
||||
))
|
||||
.into();
|
||||
}
|
||||
|
||||
let user = rockets
|
||||
.user
|
||||
.clone()
|
||||
@@ -307,6 +476,10 @@ pub fn update(
|
||||
);
|
||||
blog.icon_id = form.icon;
|
||||
blog.banner_id = form.banner;
|
||||
if !form.custom_domain.is_empty() {
|
||||
blog.custom_domain = Some(Host::new(form.custom_domain.clone()))
|
||||
}
|
||||
|
||||
blog.save_changes::<Blog>(&*conn)
|
||||
.expect("Couldn't save blog changes");
|
||||
Ok(Flash::success(
|
||||
@@ -324,16 +497,8 @@ pub fn update(
|
||||
err
|
||||
))
|
||||
})
|
||||
} else {
|
||||
// TODO actually return 403 error code
|
||||
Err(render!(errors::not_authorized(
|
||||
&rockets.to_context(),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"You are not allowed to edit this blog."
|
||||
)
|
||||
)))
|
||||
}
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
#[get("/~/<name>/outbox")]
|
||||
|
||||
+21
-4
@@ -1,6 +1,6 @@
|
||||
use plume_models::{Error, PlumeRocket};
|
||||
use plume_models::{instance::Instance, Error, PlumeRocket};
|
||||
use rocket::{
|
||||
response::{self, Responder},
|
||||
response::{self, Redirect, Responder},
|
||||
Request,
|
||||
};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
@@ -29,9 +29,26 @@ impl<'r> Responder<'r> for ErrorPage {
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub fn not_found(req: &Request) -> Ructe {
|
||||
pub fn not_found(req: &Request) -> Result<Ructe, Redirect> {
|
||||
let rockets = req.guard::<PlumeRocket>().unwrap();
|
||||
render!(errors::not_found(&rockets.to_context()))
|
||||
if req
|
||||
.uri()
|
||||
.segments()
|
||||
.next()
|
||||
.map(|path| path == "custom_domains")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let path = req
|
||||
.uri()
|
||||
.segments()
|
||||
.skip(2)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/");
|
||||
let public_domain = Instance::get_local().unwrap().public_domain;
|
||||
Err(Redirect::to(format!("https://{}/{}", public_domain, path)))
|
||||
} else {
|
||||
Ok(render!(errors::not_found(&rockets.to_context())))
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(422)]
|
||||
|
||||
+17
-17
@@ -13,7 +13,7 @@ use plume_models::{
|
||||
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
|
||||
safe_string::SafeString, users::User, Error, PlumeRocket, CONFIG,
|
||||
};
|
||||
use routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page};
|
||||
use routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/")]
|
||||
@@ -114,10 +114,19 @@ pub fn update_settings(
|
||||
_admin: Admin,
|
||||
form: LenientForm<InstanceSettingsForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
) -> RespondOrRedirect {
|
||||
let conn = &*rockets.conn;
|
||||
form.validate()
|
||||
.and_then(|_| {
|
||||
if let Err(e) = form.validate() {
|
||||
let local_inst =
|
||||
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||
render!(instance::admin(
|
||||
&rockets.to_context(),
|
||||
local_inst,
|
||||
form.clone(),
|
||||
e
|
||||
))
|
||||
.into()
|
||||
} else {
|
||||
let instance =
|
||||
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||
instance
|
||||
@@ -129,21 +138,12 @@ pub fn update_settings(
|
||||
form.long_description.clone(),
|
||||
)
|
||||
.expect("instance::update_settings: save error");
|
||||
Ok(Flash::success(
|
||||
Flash::success(
|
||||
Redirect::to(uri!(admin)),
|
||||
i18n!(rockets.intl.catalog, "Instance settings have been saved."),
|
||||
))
|
||||
})
|
||||
.or_else(|e| {
|
||||
let local_inst =
|
||||
Instance::get_local().expect("instance::update_settings: local instance error");
|
||||
Err(render!(instance::admin(
|
||||
&rockets.to_context(),
|
||||
local_inst,
|
||||
form.clone(),
|
||||
e
|
||||
)))
|
||||
})
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/admin/instances?<page>")]
|
||||
|
||||
@@ -38,14 +38,16 @@ pub fn upload(
|
||||
ct: &ContentType,
|
||||
conn: DbConn,
|
||||
) -> Result<Redirect, status::BadRequest<&'static str>> {
|
||||
if ct.is_form_data() {
|
||||
if !ct.is_form_data() {
|
||||
return Ok(Redirect::to(uri!(new)));
|
||||
}
|
||||
|
||||
let (_, boundary) = ct
|
||||
.params()
|
||||
.find(|&(k, _)| k == "boundary")
|
||||
.ok_or_else(|| status::BadRequest(Some("No boundary")))?;
|
||||
|
||||
match Multipart::with_body(data.open(), boundary).save().temp() {
|
||||
SaveResult::Full(entries) => {
|
||||
if let SaveResult::Full(entries) = Multipart::with_body(data.open(), boundary).save().temp() {
|
||||
let fields = entries.fields;
|
||||
|
||||
let filename = fields
|
||||
@@ -105,9 +107,6 @@ pub fn upload(
|
||||
)
|
||||
.map_err(|_| status::BadRequest(Some("Error while saving media")))?;
|
||||
Ok(Redirect::to(uri!(details: id = media.id)))
|
||||
}
|
||||
SaveResult::Partial(_, _) | SaveResult::Error(_) => Ok(Redirect::to(uri!(new))),
|
||||
}
|
||||
} else {
|
||||
Ok(Redirect::to(uri!(new)))
|
||||
}
|
||||
|
||||
+45
-2
@@ -7,18 +7,61 @@ use rocket::{
|
||||
RawStr, Status,
|
||||
},
|
||||
request::{self, FromFormValue, FromRequest, Request},
|
||||
response::NamedFile,
|
||||
response::{Flash, NamedFile, Redirect},
|
||||
Outcome,
|
||||
};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use template_utils::Ructe;
|
||||
|
||||
use plume_models::{posts::Post, Connection};
|
||||
|
||||
const ITEMS_PER_PAGE: i32 = 12;
|
||||
|
||||
#[derive(Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
||||
/// Special return type used for routes that "cannot fail", and instead
|
||||
/// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Responder)]
|
||||
pub enum RespondOrRedirect {
|
||||
Response(Ructe),
|
||||
FlashResponse(Flash<Ructe>),
|
||||
Redirect(Redirect),
|
||||
FlashRedirect(Flash<Redirect>),
|
||||
}
|
||||
|
||||
impl From<Ructe> for RespondOrRedirect {
|
||||
fn from(response: Ructe) -> Self {
|
||||
RespondOrRedirect::Response(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Flash<Ructe>> for RespondOrRedirect {
|
||||
fn from(response: Flash<Ructe>) -> Self {
|
||||
RespondOrRedirect::FlashResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Redirect> for RespondOrRedirect {
|
||||
fn from(redirect: Redirect) -> Self {
|
||||
RespondOrRedirect::Redirect(redirect)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Flash<Redirect>> for RespondOrRedirect {
|
||||
fn from(redirect: Flash<Redirect>) -> Self {
|
||||
RespondOrRedirect::FlashRedirect(redirect)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Shrinkwrap, Copy, Clone, UriDisplayQuery)]
|
||||
pub struct Page(i32);
|
||||
|
||||
impl fmt::Display for Page {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'v> FromFormValue<'v> for Page {
|
||||
type Error = &'v RawStr;
|
||||
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
|
||||
|
||||
+109
-36
@@ -26,26 +26,20 @@ use plume_models::{
|
||||
users::User,
|
||||
Error, PlumeRocket,
|
||||
};
|
||||
use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm};
|
||||
use routes::{
|
||||
comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm, RespondOrRedirect,
|
||||
};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||
pub fn details(
|
||||
blog: String,
|
||||
slug: String,
|
||||
fn detail_guts(
|
||||
blog: &Blog,
|
||||
post: &Post,
|
||||
responding_to: Option<i32>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
rockets: &PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let user = rockets.user.clone();
|
||||
let blog = Blog::find_by_fqn(&rockets, &blog)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
||||
if post.published
|
||||
|| post
|
||||
.get_authors(&*conn)?
|
||||
.into_iter()
|
||||
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0))
|
||||
{
|
||||
|
||||
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
||||
|
||||
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
||||
@@ -53,7 +47,7 @@ pub fn details(
|
||||
Ok(render!(posts::details(
|
||||
&rockets.to_context(),
|
||||
post.clone(),
|
||||
blog,
|
||||
blog.clone(),
|
||||
&NewCommentForm {
|
||||
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
|
||||
content: previous.clone().and_then(|p| Some(format!(
|
||||
@@ -86,12 +80,84 @@ pub fn details(
|
||||
user.clone().and_then(|u| u.has_reshared(&*conn, &post).ok()).unwrap_or(false),
|
||||
user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false),
|
||||
post.get_authors(&*conn)?[0].clone()
|
||||
)))
|
||||
} else {
|
||||
Ok(render!(errors::not_authorized(
|
||||
)).into())
|
||||
}
|
||||
|
||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||
pub fn details(
|
||||
blog: String,
|
||||
slug: String,
|
||||
responding_to: Option<i32>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let user = rockets.user.clone();
|
||||
let blog = Blog::find_by_fqn(&rockets, &blog)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
||||
|
||||
if !(post.published
|
||||
|| post
|
||||
.get_authors(&*conn)?
|
||||
.into_iter()
|
||||
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
|
||||
{
|
||||
return Ok(render!(errors::not_authorized(
|
||||
&rockets.to_context(),
|
||||
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
||||
)))
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
// check this first, and return early
|
||||
// doing this prevents partially moving `blog` into the `match (tuple)`,
|
||||
// which makes it impossible to reuse then.
|
||||
if blog.custom_domain == None {
|
||||
return detail_guts(&blog, &post, responding_to, &rockets);
|
||||
}
|
||||
|
||||
match (blog.custom_domain, responding_to) {
|
||||
(Some(ref custom_domain), Some(ref responding_to)) => Ok(Redirect::to(format!(
|
||||
"https://{}/{}?responding_to={}",
|
||||
custom_domain, slug, responding_to
|
||||
))
|
||||
.into()),
|
||||
(Some(ref custom_domain), _) => {
|
||||
Ok(Redirect::to(format!("https://{}/{}", custom_domain, slug)).into())
|
||||
}
|
||||
(None, _) => unreachable!("This code path should have already been handled!"),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod custom {
|
||||
use plume_models::{blogs::Blog, blogs::Host, posts::Post, PlumeRocket};
|
||||
use routes::{errors::ErrorPage, RespondOrRedirect};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/<custom_domain>/<slug>?<responding_to>", rank = 4)]
|
||||
pub fn details(
|
||||
custom_domain: String,
|
||||
slug: String,
|
||||
responding_to: Option<i32>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let user = rockets.user.clone();
|
||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain))?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
||||
if !(post.published
|
||||
|| post
|
||||
.get_authors(&*conn)?
|
||||
.into_iter()
|
||||
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)))
|
||||
{
|
||||
return Ok(render!(errors::not_authorized(
|
||||
&rockets.to_context(),
|
||||
i18n!(rockets.intl.catalog, "This post isn't published yet.")
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
super::detail_guts(&blog, &post, responding_to, &rockets)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +285,7 @@ pub fn update(
|
||||
cl: ContentLen,
|
||||
form: LenientForm<NewPostForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
) -> RespondOrRedirect {
|
||||
let conn = &*rockets.conn;
|
||||
let b = Blog::find_by_fqn(&rockets, &blog).expect("post::update: blog error");
|
||||
let mut post =
|
||||
@@ -255,10 +321,11 @@ pub fn update(
|
||||
.expect("posts::update: is author in error")
|
||||
{
|
||||
// actually it's not "Ok"…
|
||||
Ok(Flash::error(
|
||||
Flash::error(
|
||||
Redirect::to(uri!(super::blogs::details: name = blog, page = _)),
|
||||
i18n!(&intl, "You are not allowed to publish on this blog."),
|
||||
))
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
let (content, mentions, hashtags) = utils::md_to_html(
|
||||
form.content.to_string().as_ref(),
|
||||
@@ -345,14 +412,15 @@ pub fn update(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Flash::success(
|
||||
Flash::success(
|
||||
Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _)),
|
||||
i18n!(intl, "Your article has been updated."),
|
||||
))
|
||||
)
|
||||
.into()
|
||||
}
|
||||
} else {
|
||||
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
|
||||
Err(render!(posts::new(
|
||||
render!(posts::new(
|
||||
&rockets.to_context(),
|
||||
i18n!(intl, "Edit {0}"; &form.title),
|
||||
b,
|
||||
@@ -363,7 +431,8 @@ pub fn update(
|
||||
errors.clone(),
|
||||
medias.clone(),
|
||||
cl.0
|
||||
)))
|
||||
))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +465,7 @@ pub fn create(
|
||||
form: LenientForm<NewPostForm>,
|
||||
cl: ContentLen,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Result<Ructe, ErrorPage>> {
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");;
|
||||
let slug = form.title.to_string().to_kebab_case();
|
||||
@@ -429,7 +498,8 @@ pub fn create(
|
||||
&rockets.intl.catalog,
|
||||
"You are not allowed to publish on this blog."
|
||||
),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let (content, mentions, hashtags) = utils::md_to_html(
|
||||
@@ -530,10 +600,11 @@ pub fn create(
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _)),
|
||||
i18n!(&rockets.intl.catalog, "Your article has been saved."),
|
||||
))
|
||||
)
|
||||
.into())
|
||||
} else {
|
||||
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
|
||||
Err(Ok(render!(posts::new(
|
||||
Ok(render!(posts::new(
|
||||
&rockets.to_context(),
|
||||
i18n!(rockets.intl.catalog, "New article"),
|
||||
blog,
|
||||
@@ -544,7 +615,8 @@ pub fn create(
|
||||
errors.clone(),
|
||||
medias,
|
||||
cl.0
|
||||
))))
|
||||
))
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,14 +699,14 @@ pub fn remote_interact_post(
|
||||
blog_name: String,
|
||||
slug: String,
|
||||
remote: LenientForm<RemoteForm>,
|
||||
) -> Result<Result<Ructe, Redirect>, ErrorPage> {
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let target = Blog::find_by_fqn(&rockets, &blog_name)
|
||||
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
|
||||
if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote)
|
||||
.ok()
|
||||
.and_then(|uri| rt_format!(uri, uri = target.ap_url).ok())
|
||||
{
|
||||
Ok(Err(Redirect::to(uri)))
|
||||
Ok(Redirect::to(uri).into())
|
||||
} else {
|
||||
let mut errs = ValidationErrors::new();
|
||||
errs.add("remote", ValidationError {
|
||||
@@ -643,13 +715,14 @@ pub fn remote_interact_post(
|
||||
params: HashMap::new(),
|
||||
});
|
||||
//could not get your remote url?
|
||||
Ok(Ok(render!(posts::remote_interact(
|
||||
Ok(render!(posts::remote_interact(
|
||||
&rockets.to_context(),
|
||||
target,
|
||||
super::session::LoginForm::default(),
|
||||
ValidationErrors::default(),
|
||||
remote.clone(),
|
||||
errs
|
||||
))))
|
||||
))
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
+21
-2
@@ -49,8 +49,7 @@ macro_rules! param_to_query {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/search?<query..>")]
|
||||
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||
fn search_guts(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||
let conn = &*rockets.conn;
|
||||
let query = query.map(Form::into_inner).unwrap_or_default();
|
||||
let page = query.page.unwrap_or_default();
|
||||
@@ -83,3 +82,23 @@ pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/search?<query..>")]
|
||||
pub fn search(query: Option<Form<SearchQuery>>, rockets: PlumeRocket) -> Ructe {
|
||||
search_guts(query, rockets)
|
||||
}
|
||||
|
||||
pub mod custom {
|
||||
use plume_models::PlumeRocket;
|
||||
use rocket::request::Form;
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[get("/<_custom_domain>/search?<query..>")]
|
||||
pub fn search(
|
||||
_custom_domain: String,
|
||||
query: Option<Form<super::SearchQuery>>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Ructe {
|
||||
super::search_guts(query, rockets)
|
||||
}
|
||||
}
|
||||
|
||||
+38
-84
@@ -7,6 +7,7 @@ use rocket::{
|
||||
State,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use routes::RespondOrRedirect;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
sync::{Arc, Mutex},
|
||||
@@ -16,10 +17,10 @@ use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
use mail::{build_mail, Mailer};
|
||||
use plume_models::{
|
||||
password_reset_requests::*,
|
||||
users::{User, AUTH_COOKIE},
|
||||
Error, PlumeRocket, CONFIG,
|
||||
};
|
||||
use routes::errors::ErrorPage;
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/login?<m>")]
|
||||
@@ -45,7 +46,7 @@ pub fn create(
|
||||
form: LenientForm<LoginForm>,
|
||||
mut cookies: Cookies,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
) -> RespondOrRedirect {
|
||||
let conn = &*rockets.conn;
|
||||
let user = User::find_by_email(&*conn, &form.email_or_name)
|
||||
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
|
||||
@@ -76,7 +77,10 @@ pub fn create(
|
||||
String::new()
|
||||
};
|
||||
|
||||
if errors.is_empty() {
|
||||
if !errors.is_empty() {
|
||||
return render!(session::login(&rockets.to_context(), None, &*form, errors)).into();
|
||||
}
|
||||
|
||||
cookies.add_private(
|
||||
Cookie::build(AUTH_COOKIE, user_id)
|
||||
.same_site(SameSite::Lax)
|
||||
@@ -96,28 +100,20 @@ pub fn create(
|
||||
)
|
||||
.unwrap_or_else(|| "/".to_owned());
|
||||
|
||||
let uri = Uri::parse(&destination)
|
||||
.map(IntoOwned::into_owned)
|
||||
.map_err(|_| {
|
||||
if let Ok(uri) = Uri::parse(&destination).map(IntoOwned::into_owned) {
|
||||
Flash::success(
|
||||
Redirect::to(uri),
|
||||
i18n!(&rockets.intl.catalog, "You are now connected."),
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
render!(session::login(
|
||||
&(conn, &rockets.intl.catalog, None, None),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri),
|
||||
i18n!(&rockets.intl.catalog, "You are now connected."),
|
||||
))
|
||||
} else {
|
||||
Err(render!(session::login(
|
||||
&rockets.to_context(),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
)))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,29 +160,17 @@ pub struct ResetForm {
|
||||
pub fn password_reset_request(
|
||||
mail: State<Arc<Mutex<Mailer>>>,
|
||||
form: Form<ResetForm>,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Ructe {
|
||||
let mut requests = requests.lock().unwrap();
|
||||
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
|
||||
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
|
||||
if User::find_by_email(&*rockets.conn, &form.email).is_ok() {
|
||||
let token = PasswordResetRequest::insert(&*rockets.conn, &form.email)
|
||||
.expect("password_reset_request::insert: error");
|
||||
|
||||
if User::find_by_email(&*rockets.conn, &form.email).is_ok()
|
||||
&& !requests.iter().any(|x| x.mail == form.email.clone())
|
||||
{
|
||||
let id = plume_common::utils::random_hex();
|
||||
|
||||
requests.push(ResetRequest {
|
||||
mail: form.email.clone(),
|
||||
id: id.clone(),
|
||||
creation_date: Instant::now(),
|
||||
});
|
||||
|
||||
let link = format!("https://{}/password-reset/{}", CONFIG.base_url, id);
|
||||
let url = format!("https://{}/password-reset/{}", CONFIG.base_url, token);
|
||||
if let Some(message) = build_mail(
|
||||
form.email.clone(),
|
||||
i18n!(rockets.intl.catalog, "Password reset"),
|
||||
i18n!(rockets.intl.catalog, "Here is the link to reset your password: {0}"; link),
|
||||
i18n!(rockets.intl.catalog, "Here is the link to reset your password: {0}"; url),
|
||||
) {
|
||||
if let Some(ref mut mail) = *mail.lock().unwrap() {
|
||||
mail.send(message.into())
|
||||
@@ -199,17 +183,10 @@ pub fn password_reset_request(
|
||||
}
|
||||
|
||||
#[get("/password-reset/<token>")]
|
||||
pub fn password_reset_form(
|
||||
token: String,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|x| x.id == token.clone())
|
||||
.ok_or(Error::NotFound)?;
|
||||
pub fn password_reset_form(token: String, rockets: PlumeRocket) -> Result<Ructe, Ructe> {
|
||||
PasswordResetRequest::find_by_token(&*rockets.conn, &token)
|
||||
.map_err(|err| password_reset_error_response(err, &rockets))?;
|
||||
|
||||
Ok(render!(session::password_reset(
|
||||
&rockets.to_context(),
|
||||
&NewPasswordForm::default(),
|
||||
@@ -239,23 +216,17 @@ fn passwords_match(form: &NewPasswordForm) -> Result<(), ValidationError> {
|
||||
#[post("/password-reset/<token>", data = "<form>")]
|
||||
pub fn password_reset(
|
||||
token: String,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
form: Form<NewPasswordForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Flash<Redirect>, Ructe> {
|
||||
form.validate()
|
||||
.and_then(|_| {
|
||||
let mut requests = requests.lock().unwrap();
|
||||
let req = requests
|
||||
.iter()
|
||||
.find(|x| x.id == token.clone())
|
||||
.ok_or_else(|| to_validation(0))?
|
||||
.clone();
|
||||
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
|
||||
// Reset link is only valid for 2 hours
|
||||
requests.retain(|r| *r != req);
|
||||
let user = User::find_by_email(&*rockets.conn, &req.mail).map_err(to_validation)?;
|
||||
user.reset_password(&*rockets.conn, &form.password).ok();
|
||||
.map_err(|err| render!(session::password_reset(&rockets.to_context(), &form, err)))?;
|
||||
|
||||
PasswordResetRequest::find_and_delete_by_token(&*rockets.conn, &token)
|
||||
.and_then(|request| User::find_by_email(&*rockets.conn, &request.email))
|
||||
.and_then(|user| user.reset_password(&*rockets.conn, &form.password))
|
||||
.map_err(|err| password_reset_error_response(err, &rockets))?;
|
||||
|
||||
Ok(Flash::success(
|
||||
Redirect::to(uri!(
|
||||
new: m = _
|
||||
@@ -265,30 +236,13 @@ pub fn password_reset(
|
||||
"Your password was successfully reset."
|
||||
),
|
||||
))
|
||||
} else {
|
||||
Ok(Flash::error(
|
||||
Redirect::to(uri!(
|
||||
new: m = _
|
||||
)),
|
||||
i18n!(
|
||||
rockets.intl.catalog,
|
||||
"Sorry, but the link expired. Try again"
|
||||
),
|
||||
))
|
||||
}
|
||||
})
|
||||
.map_err(|err| render!(session::password_reset(&rockets.to_context(), &form, err)))
|
||||
}
|
||||
|
||||
fn to_validation<T>(_: T) -> ValidationErrors {
|
||||
let mut errors = ValidationErrors::new();
|
||||
errors.add(
|
||||
"",
|
||||
ValidationError {
|
||||
code: Cow::from("server_error"),
|
||||
message: Some(Cow::from("An unknown error occured")),
|
||||
params: std::collections::HashMap::new(),
|
||||
},
|
||||
);
|
||||
errors
|
||||
fn password_reset_error_response(err: Error, rockets: &PlumeRocket) -> Ructe {
|
||||
match err {
|
||||
Error::Expired => render!(session::password_reset_request_expired(
|
||||
&rockets.to_context()
|
||||
)),
|
||||
_ => render!(errors::not_found(&rockets.to_context())),
|
||||
}
|
||||
}
|
||||
|
||||
+12
-10
@@ -25,14 +25,14 @@ use plume_models::{
|
||||
users::*,
|
||||
Error, PlumeRocket,
|
||||
};
|
||||
use routes::{errors::ErrorPage, Page, RemoteForm};
|
||||
use routes::{errors::ErrorPage, Page, RemoteForm, RespondOrRedirect};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/me")]
|
||||
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
pub fn me(user: Option<User>) -> RespondOrRedirect {
|
||||
match user {
|
||||
Some(user) => Ok(Redirect::to(uri!(details: name = user.username))),
|
||||
None => Err(utils::requires_login("", uri!(me))),
|
||||
Some(user) => Redirect::to(uri!(details: name = user.username)).into(),
|
||||
None => utils::requires_login("", uri!(me)).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ pub fn follow_not_connected(
|
||||
name: String,
|
||||
remote_form: Option<LenientForm<RemoteForm>>,
|
||||
i18n: I18n,
|
||||
) -> Result<Result<Flash<Ructe>, Redirect>, ErrorPage> {
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let target = User::find_by_fqn(&rockets, &name)?;
|
||||
if let Some(remote_form) = remote_form {
|
||||
if let Some(uri) = User::fetch_remote_interact_uri(&remote_form)
|
||||
@@ -207,7 +207,7 @@ pub fn follow_not_connected(
|
||||
.ok()
|
||||
})
|
||||
{
|
||||
Ok(Err(Redirect::to(uri)))
|
||||
Ok(Redirect::to(uri).into())
|
||||
} else {
|
||||
let mut err = ValidationErrors::default();
|
||||
err.add("remote",
|
||||
@@ -217,7 +217,7 @@ pub fn follow_not_connected(
|
||||
params: HashMap::new(),
|
||||
},
|
||||
);
|
||||
Ok(Ok(Flash::new(
|
||||
Ok(Flash::new(
|
||||
render!(users::follow_remote(
|
||||
&rockets.to_context(),
|
||||
target,
|
||||
@@ -228,10 +228,11 @@ pub fn follow_not_connected(
|
||||
)),
|
||||
"callback",
|
||||
uri!(follow: name = name).to_string(),
|
||||
)))
|
||||
)
|
||||
.into())
|
||||
}
|
||||
} else {
|
||||
Ok(Ok(Flash::new(
|
||||
Ok(Flash::new(
|
||||
render!(users::follow_remote(
|
||||
&rockets.to_context(),
|
||||
target,
|
||||
@@ -243,7 +244,8 @@ pub fn follow_not_connected(
|
||||
)),
|
||||
"callback",
|
||||
uri!(follow: name = name).to_string(),
|
||||
)))
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,14 +48,16 @@ impl Resolver<PlumeRocket> for WebfingerResolver {
|
||||
CONFIG.base_url.as_str()
|
||||
}
|
||||
|
||||
fn find(acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
|
||||
User::find_by_fqn(&ctx, &acct)
|
||||
fn find(prefix: Prefix, acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
|
||||
match prefix {
|
||||
Prefix::Acct => User::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|usr| usr.webfinger(&*ctx.conn))
|
||||
.or_else(|_| {
|
||||
Blog::find_by_fqn(&ctx, &acct)
|
||||
.or(Err(ResolverError::NotFound)),
|
||||
Prefix::Group => Blog::find_by_fqn(&ctx, &acct)
|
||||
.and_then(|blog| blog.webfinger(&*ctx.conn))
|
||||
.or(Err(ResolverError::NotFound))
|
||||
})
|
||||
.or(Err(ResolverError::NotFound)),
|
||||
Prefix::Custom(_) => Err(ResolverError::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +74,7 @@ pub fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> {
|
||||
"Invalid resource. Make sure to request an acct: URI"
|
||||
}
|
||||
ResolverError::NotFound => "Requested resource was not found",
|
||||
ResolverError::WrongInstance => {
|
||||
"This is not the instance of the requested resource"
|
||||
}
|
||||
ResolverError::WrongDomain => "This is not the instance of the requested resource",
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
+108
-8
@@ -1,7 +1,10 @@
|
||||
use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
|
||||
|
||||
use rocket::http::hyper::header::{ETag, EntityTag};
|
||||
use rocket::http::{Method, Status};
|
||||
use rocket::http::{
|
||||
uri::{FromUriParam, Query},
|
||||
Method, Status,
|
||||
};
|
||||
use rocket::request::Request;
|
||||
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
|
||||
use rocket_i18n::Catalog;
|
||||
@@ -13,6 +16,16 @@ pub use askama_escape::escape;
|
||||
|
||||
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
||||
|
||||
pub struct NoValue; // workarround for missing FromUriParam implementation for Option
|
||||
|
||||
impl FromUriParam<Query, NoValue> for Option<i32> {
|
||||
type Target = Option<i32>;
|
||||
|
||||
fn from_uri_param(_: NoValue) -> Self::Target {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub type BaseContext<'a> = &'a (
|
||||
&'a Connection,
|
||||
&'a Catalog,
|
||||
@@ -146,7 +159,7 @@ pub fn avatar(
|
||||
pub fn tabs(links: &[(&str, String, bool)]) -> Html<String> {
|
||||
let mut res = String::from(r#"<div class="tabs">"#);
|
||||
for (url, title, selected) in links {
|
||||
res.push_str(r#"<a href=""#);
|
||||
res.push_str(r#"<a dir="auto" href=""#);
|
||||
res.push_str(url);
|
||||
if *selected {
|
||||
res.push_str(r#"" class="selected">"#);
|
||||
@@ -176,7 +189,7 @@ pub fn paginate_param(
|
||||
p
|
||||
})
|
||||
.unwrap_or_default();
|
||||
res.push_str(r#"<div class="pagination">"#);
|
||||
res.push_str(r#"<div class="pagination" dir="auto">"#);
|
||||
if page != 1 {
|
||||
res.push_str(
|
||||
format!(
|
||||
@@ -239,13 +252,13 @@ macro_rules! input {
|
||||
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}">
|
||||
<label for="{name}" dir="auto">
|
||||
{label}
|
||||
{optional}
|
||||
{details}
|
||||
</label>
|
||||
{error}
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props} dir="auto"/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
@@ -264,7 +277,7 @@ macro_rules! input {
|
||||
$err.errors().get(stringify!($name))
|
||||
{
|
||||
format!(
|
||||
r#"<p class="error">{}</p>"#,
|
||||
r#"<p class="error" dir="auto">{}</p>"#,
|
||||
errs[0]
|
||||
.message
|
||||
.clone()
|
||||
@@ -332,8 +345,8 @@ macro_rules! input {
|
||||
let cat = $catalog;
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
||||
<label for="{name}" dir="auto">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props} dir="auto"/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
@@ -342,3 +355,90 @@ macro_rules! input {
|
||||
))
|
||||
}};
|
||||
}
|
||||
|
||||
/// This macro imitate rocket's uri!, but with support for custom domains
|
||||
///
|
||||
/// It takes one more argument, domain, which must appear first, and must be an Option<&str>
|
||||
/// sample call :
|
||||
/// assuming both take the same parameters
|
||||
/// url!(custom_domain=Some("something.tld"), posts::details: slug = "title", responding_to = _, blog = "blogname"));
|
||||
///
|
||||
/// assuming posts::details take one more parameter than posts::custom::details
|
||||
/// url!(custom_domain=Some("something.tld"), posts::details:
|
||||
/// common=[slug = "title", responding_to = _],
|
||||
/// normal=[blog = "blogname"]));
|
||||
///
|
||||
/// you can also provide custom=[] for custom-domain specific arguments
|
||||
/// custom_domain can be changed to anything, indicating custom domain varname in the custom-domain
|
||||
/// function (most likely custom_domain or _custom_domain)
|
||||
macro_rules! url {
|
||||
($custom_domain:ident=$domain:expr, $module:ident::$route:ident:
|
||||
common=[$($common_args:tt = $common_val:expr),*],
|
||||
normal=[$($normal_args:tt = $normal_val:expr),*],
|
||||
custom=[$($custom_args:tt = $custom_val:expr),*]) => {{
|
||||
let domain: &Option<plume_models::blogs::Host> = &$domain; //for type inference with None
|
||||
$(
|
||||
let $common_args = $common_val;
|
||||
)*
|
||||
if let Some(domain) = domain {
|
||||
$(
|
||||
let $custom_args = $custom_val;
|
||||
)*
|
||||
let origin = uri!(crate::routes::$module::custom::$route:
|
||||
$custom_domain = domain.to_string(),
|
||||
$($common_args = $common_args,)*
|
||||
$($custom_args = $custom_args,)*
|
||||
);
|
||||
let path = origin
|
||||
.segments()
|
||||
.skip(1)// skip is <custom_domain> part
|
||||
.map(|seg| format!("/{}", seg)).collect::<String>();
|
||||
let query = origin.query()
|
||||
.filter(|q| !q.is_empty())
|
||||
.map(|q| format!("?{}", q))
|
||||
.unwrap_or_default();
|
||||
format!("https://{}{}{}", &domain, path, query)
|
||||
} else {
|
||||
$(
|
||||
let $normal_args = $normal_val;
|
||||
)*
|
||||
url!($module::$route:
|
||||
$($common_args = $common_args,)*
|
||||
$($normal_args = $normal_args,)*)
|
||||
.to_string()
|
||||
}
|
||||
}};
|
||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
||||
common=[$($tt:tt)*]) => {
|
||||
url!($cd=$d, $m::$r: common=[$($tt)*], normal=[], custom=[])
|
||||
};
|
||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
||||
normal=[$($tt:tt)*]) => {
|
||||
url!($cd=$d, $m::$r: common=[], normal=[$($tt)*], custom=[])
|
||||
};
|
||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
||||
custom=[$($tt:tt)*]) => {
|
||||
url!($cd=$d, $m::$r: common=[], normal=[], custom=[$($tt)*])
|
||||
};
|
||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
||||
common=[$($co:tt)*],
|
||||
normal=[$($no:tt)*]) => {
|
||||
url!($cd=$d, $m::$r: common=[$($co)*], normal=[$($no)*], custom=[])
|
||||
};
|
||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
||||
common=[$($co:tt)*],
|
||||
custom=[$($cu:tt)*]) => {
|
||||
url!($cd=$d, $m::$r: common=[$($co)*], normal=[], custom=[$($cu)*])
|
||||
};
|
||||
($cd:ident=$d:expr, $m:ident::$r:ident:
|
||||
normal=[$($no:tt)*],
|
||||
custom=[$($cu:tt)*]) => {
|
||||
url!($cd=$d, $m::$r: common=[], normal=[$($no)*], custom=[$($cu)*])
|
||||
};
|
||||
($custom_domain:ident=$domain:expr, $module:ident::$route:ident: $($common_args:tt)*) => {
|
||||
url!($custom_domain=$domain, $module::$route: common=[$($common_args)*])
|
||||
};
|
||||
($module:ident::$route:ident: $($tt:tt)*) => {
|
||||
uri!(crate::routes::$module::$route: $($tt)*)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ form input[type="submit"] {
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
|
||||
overflow-y: scroll;
|
||||
font-family: $lora;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5;
|
||||
@@ -127,7 +127,7 @@ form.new-post {
|
||||
}
|
||||
textarea {
|
||||
min-height: 20em;
|
||||
overflow-y: hidden;
|
||||
overflow-y: scroll;
|
||||
resize: none;
|
||||
-webkit-appearance: textarea;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<meta content="120" property="og:image:width" />
|
||||
<meta content="120" property="og:image:height" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
<meta content="'@Instance::get_local().unwrap().name" property="og:site_name" />
|
||||
<meta content="@blog.ap_url" property="og:url" />
|
||||
<meta content="@Instance::get_local().unwrap().name" property="og:site_name" />
|
||||
<meta content="@blog.url()" property="og:url" />
|
||||
<meta content="@blog.fqn" property="profile:username" />
|
||||
<meta content="@blog.title" property="og:title" />
|
||||
<meta content="@blog.summary_html" name="description">
|
||||
@@ -24,7 +24,7 @@
|
||||
<link href='@Instance::get_local().unwrap().compute_box("~", &blog.fqn, "atom.xml")' rel='alternate' type='application/atom+xml'>
|
||||
<link href='@blog.ap_url' rel='alternate' type='application/activity+json'>
|
||||
}, {
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
||||
<a href="@url!(custom_domain = blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])" dir="auto">@blog.title</a>
|
||||
}, {
|
||||
<div class="hidden">
|
||||
@for author in authors {
|
||||
@@ -41,38 +41,38 @@
|
||||
}
|
||||
<div class="h-card">
|
||||
<div class="user">
|
||||
<div class="flex wrap">
|
||||
<div class="flex wrap" dir="auto">
|
||||
<div class="avatar medium" style="background-image: url('@blog.icon_url(ctx.0)');" aria-label="@i18n!(ctx.1, "{}'s icon"; &blog.title)"></div>
|
||||
<img class="hidden u-photo" src="@blog.icon_url(ctx.0)"/>
|
||||
|
||||
<h1 class="grow flex vertical">
|
||||
<span class="p-name">@blog.title</span>
|
||||
<small>~@blog.fqn</small>
|
||||
<small dir="auto">~@blog.fqn</small>
|
||||
</h1>
|
||||
|
||||
@if ctx.2.clone().and_then(|u| u.is_author_in(ctx.0, &blog).ok()).unwrap_or(false) {
|
||||
<a href="@uri!(posts::new: blog = &blog.fqn)" class="button">@i18n!(ctx.1, "New article")</a>
|
||||
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button">@i18n!(ctx.1, "Edit")</a>
|
||||
<a href="@uri!(posts::new: blog = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "New article")</a>
|
||||
<a href="@uri!(blogs::edit: name = &blog.fqn)" class="button" dir="auto">@i18n!(ctx.1, "Edit")</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<main class="user-summary">
|
||||
<main class="user-summary" dir="auto">
|
||||
<p>
|
||||
@i18n!(ctx.1, "There's one author on this blog: ", "There are {0} authors on this blog: "; authors.len())
|
||||
@for (i, author) in authors.iter().enumerate() {@if i >= 1 {, }
|
||||
<a class="author p-author" href="@uri!(user::details: name = &author.fqn)">@author.name()</a>}
|
||||
<a class="author p-author" href="@uri!(user::details: name = &author.fqn)" dir="auto">@author.name()</a>}
|
||||
</p>
|
||||
@Html(blog.summary_html.clone())
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<h2 dir="auto">
|
||||
@i18n!(ctx.1, "Latest articles")
|
||||
<small><a href="@uri!(blogs::atom_feed: name = &blog.fqn)" title="Atom feed">@icon!("rss")</a></small>
|
||||
</h2>
|
||||
@if posts.is_empty() {
|
||||
<p>@i18n!(ctx.1, "No posts to see here yet.")</p>
|
||||
<p dir="auto">@i18n!(ctx.1, "No posts to see here yet.")</p>
|
||||
}
|
||||
<div class="cards">
|
||||
@for article in posts {
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
<label for="summary">@i18n!(ctx.1, "Description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
||||
<textarea id="summary" name="summary" rows="20">@form.summary</textarea>
|
||||
|
||||
@input!(ctx.1, custom_domain (optional text), "Custom Domain", form, errors, "")
|
||||
|
||||
<p>
|
||||
@i18n!(ctx.1, "You can upload images to your gallery, to use them as blog icons, or banners.")
|
||||
<a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload images")</a>
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
@(ctx: BaseContext, form: &NewBlogForm, errors: ValidationErrors)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "Create a blog")</h1>
|
||||
<h1 dir="auto">@i18n!(ctx.1, "Create a blog")</h1>
|
||||
<form method="post" action="@uri!(blogs::create)">
|
||||
@input!(ctx.1, title (text), "Title", form, errors, "required minlength=\"1\"")
|
||||
<input type="submit" value="@i18n!(ctx.1, "Create blog")"/>
|
||||
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required minlength=\"1\"")
|
||||
<input type="submit" value="@i18n!(ctx.1, "Create blog")" dir="auto"/>
|
||||
|
||||
@input!(ctx.1, custom_domain (optional text), "Custom Domain", form, errors, "")
|
||||
<input type="submit" value="@i18n!(ctx.1, "Make your blog available under a custom domain")" dir="auto"/>
|
||||
</form>
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="comment u-comment h-cite" id="comment-@comm.id">
|
||||
<main class="content">
|
||||
<header>
|
||||
<a class="author u-author h-card" href="@uri!(user::details: name = &author.fqn)">
|
||||
<a class="author u-author h-card" href="@uri!(user::details: name = &author.fqn)" dir="auto">
|
||||
@avatar(ctx.0, &author, Size::Small, true, ctx.1)
|
||||
<span class="display-name p-name">@author.name()</span>
|
||||
<small>@author.fqn</small>
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="text p-content">
|
||||
@if comm.sensitive {
|
||||
<details>
|
||||
<summary>@comm.spoiler_text</summary>
|
||||
<summary dir="auto">@comm.spoiler_text</summary>
|
||||
}
|
||||
@Html(&comm.content)
|
||||
@if comm.sensitive {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
@if !articles.is_empty() {
|
||||
<div class="h-feed">
|
||||
<h2><span class="p-name">@title</span> — <a href="@link">@i18n!(ctx.1, "View all")</a></h2>
|
||||
<h2 dir="auto"><span class="p-name">@title</span> — <a href="@link">@i18n!(ctx.1, "View all")</a></h2>
|
||||
<div class="cards">
|
||||
@for article in articles {
|
||||
@:post_card(ctx, article)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
@(ctx: BaseContext, id: &str, title: String, optional: bool, medias: Vec<Media>, selected: Option<i32>)
|
||||
|
||||
<label for="@id">
|
||||
<label for="@id" dir="auto">
|
||||
@title
|
||||
@if optional {
|
||||
<small>@i18n!(ctx.1, "Optional")</small>
|
||||
@@ -13,7 +13,7 @@
|
||||
<option value="none" @if selected.is_none() { selected }>@i18n!(ctx.1, "None")</option>
|
||||
@for media in medias {
|
||||
@if media.category() == MediaCategory::Image {
|
||||
<option value="@media.id" @if selected.map(|c| c == media.id).unwrap_or(false) { selected }>
|
||||
<option value="@media.id" @if selected.map(|c| c == media.id).unwrap_or(false) { selected } dir="auto">
|
||||
@if !media.alt_text.is_empty() {
|
||||
@media.alt_text
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
@(ctx: BaseContext, instance: Instance, n_users: i64, n_articles: i64)
|
||||
|
||||
<section class="split">
|
||||
<section class="split" dir="auto">
|
||||
<div class="presentation card">
|
||||
<h2>@i18n!(ctx.1, "What is Plume?")</h2>
|
||||
<main>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
@if article.cover_id.is_some() {
|
||||
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
|
||||
}
|
||||
<h3 class="p-name">
|
||||
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().fqn, slug = &article.slug, responding_to = _)">
|
||||
<h3 class="p-name" dir="auto">
|
||||
<a class="u-url" href="@url!(custom_domain = article.get_blog(ctx.0).unwrap().custom_domain, posts::details: common=[ slug = &article.slug, responding_to = NoValue], normal=[blog = article.get_blog(ctx.0).unwrap().fqn])">
|
||||
@article.title
|
||||
</a>
|
||||
</h3>
|
||||
<main>
|
||||
<p class="p-summary">@article.subtitle</p>
|
||||
<p class="p-summary" dir="auto">@article.subtitle</p>
|
||||
</main>
|
||||
<footer class="authors">
|
||||
@Html(i18n!(ctx.1, "By {0}"; format!(
|
||||
@@ -25,10 +25,9 @@
|
||||
@if article.published {
|
||||
⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
|
||||
}
|
||||
⋅ <a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a>
|
||||
⋅ <a href="@url!(custom_domain = article.get_blog(ctx.0).unwrap().custom_domain, blogs::details: common=[page = None], normal=[name = &article.get_blog(ctx.0).unwrap().fqn])">@article.get_blog(ctx.0).unwrap().title</a>
|
||||
@if !article.published {
|
||||
⋅ @i18n!(ctx.1, "Draft")
|
||||
}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
@if article.cover_id.is_some() {
|
||||
<meta property="og:image" content="@Html(article.cover_url(ctx.0).unwrap_or_default())"/>
|
||||
}
|
||||
<meta property="og:url" content="@uri!(posts::details: blog = &blog.fqn, slug = &article.slug, responding_to = _)"/>
|
||||
<meta property="og:url" content="@url!(custom_domain = blog.custom_domain, posts::details: common=[slug = &article.slug, responding_to = NoValue], normal=[blog = &blog.fqn])"/>
|
||||
<meta property="og:description" content="@article.subtitle"/>
|
||||
}, {
|
||||
<a href="@uri!(blogs::details: name = &blog.fqn, page = _)">@blog.title</a>
|
||||
<a href="@url!(custom_domain = &blog.custom_domain, blogs::details: common=[page = None], normal=[name = &blog.fqn])">@blog.title</a>
|
||||
}, {
|
||||
<div class="h-entry">
|
||||
<header
|
||||
@@ -28,8 +28,8 @@
|
||||
@if article.cover_id.is_some() { style="background-image: url('@article.cover_url(ctx.0).unwrap_or_default()'" }
|
||||
>
|
||||
<div>
|
||||
<h1 class="article p-name">@article.title</h1>
|
||||
<div class="article-info">
|
||||
<h1 class="article p-name" dir="auto">@article.title</h1>
|
||||
<div class="article-info" dir="auto">
|
||||
<span class="author">
|
||||
@Html(i18n!(ctx.1, "Written by {0}"; format!("<a href=\"{}\">{}</a>",
|
||||
uri!(user::details: name = &author.fqn),
|
||||
@@ -38,7 +38,7 @@
|
||||
—
|
||||
<span class="date dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span><a class="u-url" href="@article.ap_url"></a>
|
||||
</div>
|
||||
<h2 class="article p-summary">@article.subtitle</h2>
|
||||
<h2 class="article p-summary" dir="auto">@article.subtitle</h2>
|
||||
</div>
|
||||
@if article.cover_id.is_some() {
|
||||
<div class="shadow"></div>
|
||||
@@ -46,21 +46,21 @@
|
||||
}
|
||||
</header>
|
||||
|
||||
<article class="e-content">
|
||||
<article class="e-content" dir="auto">
|
||||
@Html(&article.content)
|
||||
</article>
|
||||
<div class="article-meta">
|
||||
<section class="split">
|
||||
<ul class="tags">
|
||||
<ul class="tags" dir="auto">
|
||||
@for tag in tags {
|
||||
@if !tag.is_hashtag {
|
||||
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = _)">@tag.tag</a></li>
|
||||
<li><a class="p-category" href="@uri!(tags::tag: name = &tag.tag, page = None)">@tag.tag</a></li>
|
||||
} else {
|
||||
<span class="hidden p-category">@tag.tag</span>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
<p class="right">
|
||||
<p class="right" dir="auto">
|
||||
@if article.license.is_empty() {
|
||||
@i18n!(ctx.1, "All rights reserved."; &article.license)
|
||||
} else {
|
||||
@@ -116,7 +116,7 @@
|
||||
</section>
|
||||
}
|
||||
<section class="banner">
|
||||
<div class="flex p-author h-card user">
|
||||
<div class="flex p-author h-card user" dir="auto">
|
||||
@avatar(ctx.0, &author, Size::Medium, true, ctx.1)
|
||||
<div class="grow">
|
||||
<h2 class="p-name">
|
||||
@@ -132,7 +132,7 @@
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<section class="comments">
|
||||
<section class="comments" dir="auto">
|
||||
<h2>@i18n!(ctx.1, "Comments")</h2>
|
||||
|
||||
@if ctx.2.is_some() {
|
||||
@@ -143,7 +143,7 @@
|
||||
@if let Some(ref prev) = previous_comment {
|
||||
<input type="hidden" name="responding_to" value="@prev.id"/>
|
||||
}
|
||||
<textarea id="plume-editor" name="content">@comment_form.content</textarea>
|
||||
<textarea id="plume-editor" name="content" dir="auto">@comment_form.content</textarea>
|
||||
<input type="submit" value="@i18n!(ctx.1, "Submit comment")" />
|
||||
</form>
|
||||
}
|
||||
@@ -153,7 +153,7 @@
|
||||
@:comment(ctx, &comm, Some(&article.ap_url), &blog.fqn, &article.slug)
|
||||
}
|
||||
} else {
|
||||
<p class="center">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
||||
<p class="center" dir="auto">@i18n!(ctx.1, "No comments yet. Be the first to react!")</p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
@(ctx: BaseContext, title: String, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>, content_len: u64)
|
||||
|
||||
@:base(ctx, title.clone(), {}, {}, {
|
||||
<h1 id="plume-editor-title">@title</h1>
|
||||
<div id="plume-editor" style="display: none;">
|
||||
<h1 id="plume-editor-title" dir="auto">@title</h1>
|
||||
<div id="plume-editor" style="display: none;" dir="auto">
|
||||
<header>
|
||||
<button id="publish" class="button">@i18n!(ctx.1, "Publish")</button>
|
||||
<p id="char-count">@content_len</p>
|
||||
@@ -32,10 +32,10 @@
|
||||
@format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or_else(|| Cow::from("Unknown error")))
|
||||
}
|
||||
|
||||
<label for="editor-content">@i18n!(ctx.1, "Content")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
||||
<textarea id="editor-content" name="content" rows="20">@Html(&form.content)</textarea>
|
||||
<small id="editor-left">@content_len</small>
|
||||
<p>
|
||||
<label for="editor-content" dir="auto">@i18n!(ctx.1, "Content")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
||||
<textarea id="editor-content" name="content" rows="20" dir="auto">@Html(&form.content)</textarea>
|
||||
<small id="editor-left" dir="auto">@content_len</small>
|
||||
<p dir="auto">
|
||||
@i18n!(ctx.1, "You can upload media to your gallery, and then copy their Markdown code into your articles to insert them.")
|
||||
<a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload media")</a>
|
||||
</p>
|
||||
@@ -47,7 +47,7 @@
|
||||
@:image_select(ctx, "cover", i18n!(ctx.1, "Illustration"), true, medias, form.cover)
|
||||
|
||||
@if is_draft {
|
||||
<label for="draft">
|
||||
<label for="draft" dir="auto">
|
||||
<input type="checkbox" name="draft" id="draft" checked>
|
||||
@i18n!(ctx.1, "This is a draft, don't publish it yet.")
|
||||
</label>
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
@(ctx: BaseContext, post: Post, login_form: LoginForm, login_errs: ValidationErrors, remote_form: RemoteForm, remote_errs: ValidationErrors)
|
||||
|
||||
@:remote_interact_base(ctx, i18n!(ctx.1, "Interact with {}"; post.title.clone()), i18n!(ctx.1, "Log in to interact"), i18n!(ctx.1, "Enter your full username to interact"), {
|
||||
<h1>@i18n!(ctx.1, "Interact with {}"; post.title.clone())</h1>
|
||||
<h1 dir="auto">@i18n!(ctx.1, "Interact with {}"; post.title.clone())</h1>
|
||||
@:post_card(ctx, post)
|
||||
}, login_form, login_errs, remote_form, remote_errs)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@use template_utils::*;
|
||||
@use templates::base;
|
||||
|
||||
@(ctx: BaseContext)
|
||||
|
||||
@:base(ctx, i18n!(ctx.1, "Password reset"), {}, {}, {
|
||||
<h1>@i18n!(ctx.1, "This token has expired")</h1>
|
||||
<p>@i18n!(ctx.1, "Please start the process again by clicking") <a href="/password-reset">@i18n!(ctx.1, "here")</a>.</p>
|
||||
})
|
||||
Reference in New Issue
Block a user