502 lines
15 KiB
Rust
502 lines
15 KiB
Rust
use rocket::{
|
|
request::{Form, FormItems, FromForm, LenientForm},
|
|
response::{status, Flash, Redirect},
|
|
};
|
|
use rocket_contrib::json::Json;
|
|
use rocket_i18n::I18n;
|
|
use scheduled_thread_pool::ScheduledThreadPool;
|
|
use std::str::FromStr;
|
|
use validator::{Validate, ValidationErrors};
|
|
|
|
use crate::inbox;
|
|
use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
|
|
use crate::template_utils::{IntoContext, Ructe};
|
|
use plume_common::activity_pub::{broadcast, inbox::FromId};
|
|
use plume_models::{
|
|
admin::*,
|
|
blocklisted_emails::*,
|
|
comments::Comment,
|
|
db_conn::DbConn,
|
|
headers::Headers,
|
|
instance::*,
|
|
posts::Post,
|
|
safe_string::SafeString,
|
|
timeline::Timeline,
|
|
users::{Role, User},
|
|
Connection, Error, PlumeRocket, CONFIG,
|
|
};
|
|
|
|
#[get("/")]
|
|
pub fn index(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|
let inst = Instance::get_local()?;
|
|
let timelines = Timeline::list_all_for_user(&conn, rockets.user.clone().map(|u| u.id))?
|
|
.into_iter()
|
|
.filter_map(|t| {
|
|
if let Ok(latest) = t.get_latest(&conn, 12) {
|
|
Some((t, latest))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(render!(instance::index(
|
|
&(&conn, &rockets).to_context(),
|
|
inst,
|
|
User::count_local(&conn)?,
|
|
Post::count_local(&conn)?,
|
|
timelines
|
|
)))
|
|
}
|
|
|
|
#[get("/admin")]
|
|
pub fn admin(_admin: Admin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|
let local_inst = Instance::get_local()?;
|
|
Ok(render!(instance::admin(
|
|
&(&conn, &rockets).to_context(),
|
|
local_inst.clone(),
|
|
InstanceSettingsForm {
|
|
name: local_inst.name.clone(),
|
|
open_registrations: local_inst.open_registrations,
|
|
short_description: local_inst.short_description,
|
|
long_description: local_inst.long_description,
|
|
default_license: local_inst.default_license,
|
|
},
|
|
ValidationErrors::default()
|
|
)))
|
|
}
|
|
|
|
#[get("/admin", rank = 2)]
|
|
pub fn admin_mod(_mod: Moderator, conn: DbConn, rockets: PlumeRocket) -> Ructe {
|
|
render!(instance::admin_mod(&(&conn, &rockets).to_context()))
|
|
}
|
|
|
|
#[derive(Clone, FromForm, Validate)]
|
|
pub struct InstanceSettingsForm {
|
|
#[validate(length(min = 1))]
|
|
pub name: String,
|
|
pub open_registrations: bool,
|
|
pub short_description: SafeString,
|
|
pub long_description: SafeString,
|
|
#[validate(length(min = 1))]
|
|
pub default_license: String,
|
|
}
|
|
|
|
#[post("/admin", data = "<form>")]
|
|
pub fn update_settings(
|
|
_admin: Admin,
|
|
form: LenientForm<InstanceSettingsForm>,
|
|
conn: DbConn,
|
|
rockets: PlumeRocket,
|
|
) -> RespondOrRedirect {
|
|
if let Err(e) = form.validate() {
|
|
let local_inst =
|
|
Instance::get_local().expect("instance::update_settings: local instance error");
|
|
render!(instance::admin(
|
|
&(&conn, &rockets).to_context(),
|
|
local_inst,
|
|
form.clone(),
|
|
e
|
|
))
|
|
.into()
|
|
} else {
|
|
let instance =
|
|
Instance::get_local().expect("instance::update_settings: local instance error");
|
|
instance
|
|
.update(
|
|
&*conn,
|
|
form.name.clone(),
|
|
form.open_registrations,
|
|
form.short_description.clone(),
|
|
form.long_description.clone(),
|
|
form.default_license.clone(),
|
|
)
|
|
.expect("instance::update_settings: save error");
|
|
Flash::success(
|
|
Redirect::to(uri!(admin)),
|
|
i18n!(rockets.intl.catalog, "Instance settings have been saved."),
|
|
)
|
|
.into()
|
|
}
|
|
}
|
|
|
|
#[get("/admin/instances?<page>")]
|
|
pub fn admin_instances(
|
|
_mod: Moderator,
|
|
page: Option<Page>,
|
|
conn: DbConn,
|
|
rockets: PlumeRocket,
|
|
) -> Result<Ructe, ErrorPage> {
|
|
let page = page.unwrap_or_default();
|
|
let instances = Instance::page(&conn, page.limits())?;
|
|
Ok(render!(instance::list(
|
|
&(&conn, &rockets).to_context(),
|
|
Instance::get_local()?,
|
|
instances,
|
|
page.0,
|
|
Page::total(Instance::count(&conn)? as i32)
|
|
)))
|
|
}
|
|
|
|
#[post("/admin/instances/<id>/block")]
|
|
pub fn toggle_block(
|
|
_mod: Moderator,
|
|
conn: DbConn,
|
|
id: i32,
|
|
intl: I18n,
|
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
|
let inst = Instance::get(&conn, id)?;
|
|
let message = if inst.blocked {
|
|
i18n!(intl.catalog, "{} has been unblocked."; &inst.name)
|
|
} else {
|
|
i18n!(intl.catalog, "{} has been blocked."; &inst.name)
|
|
};
|
|
|
|
inst.toggle_block(&conn)?;
|
|
Ok(Flash::success(
|
|
Redirect::to(uri!(admin_instances: page = _)),
|
|
message,
|
|
))
|
|
}
|
|
|
|
#[get("/admin/users?<page>")]
|
|
pub fn admin_users(
|
|
_mod: Moderator,
|
|
page: Option<Page>,
|
|
conn: DbConn,
|
|
rockets: PlumeRocket,
|
|
) -> Result<Ructe, ErrorPage> {
|
|
let page = page.unwrap_or_default();
|
|
Ok(render!(instance::users(
|
|
&(&conn, &rockets).to_context(),
|
|
User::get_local_page(&conn, page.limits())?,
|
|
page.0,
|
|
Page::total(User::count_local(&conn)? as i32)
|
|
)))
|
|
}
|
|
pub struct BlocklistEmailDeletion {
|
|
ids: Vec<i32>,
|
|
}
|
|
impl<'f> FromForm<'f> for BlocklistEmailDeletion {
|
|
type Error = ();
|
|
fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result<BlocklistEmailDeletion, ()> {
|
|
let mut c: BlocklistEmailDeletion = BlocklistEmailDeletion { ids: Vec::new() };
|
|
for item in items {
|
|
let key = item.key.parse::<i32>();
|
|
if let Ok(i) = key {
|
|
c.ids.push(i);
|
|
}
|
|
}
|
|
Ok(c)
|
|
}
|
|
}
|
|
#[post("/admin/emails/delete", data = "<form>")]
|
|
pub fn delete_email_blocklist(
|
|
_mod: Moderator,
|
|
form: Form<BlocklistEmailDeletion>,
|
|
conn: DbConn,
|
|
rockets: PlumeRocket,
|
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
|
BlocklistedEmail::delete_entries(&conn, form.0.ids)?;
|
|
Ok(Flash::success(
|
|
Redirect::to(uri!(admin_email_blocklist: page = None)),
|
|
i18n!(rockets.intl.catalog, "Blocks deleted"),
|
|
))
|
|
}
|
|
|
|
#[post("/admin/emails/new", data = "<form>")]
|
|
pub fn add_email_blocklist(
|
|
_mod: Moderator,
|
|
form: LenientForm<NewBlocklistedEmail>,
|
|
conn: DbConn,
|
|
rockets: PlumeRocket,
|
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
|
let result = BlocklistedEmail::insert(&conn, form.0);
|
|
|
|
if let Err(Error::Db(_)) = result {
|
|
Ok(Flash::error(
|
|
Redirect::to(uri!(admin_email_blocklist: page = None)),
|
|
i18n!(rockets.intl.catalog, "Email already blocked"),
|
|
))
|
|
} else {
|
|
Ok(Flash::success(
|
|
Redirect::to(uri!(admin_email_blocklist: page = None)),
|
|
i18n!(rockets.intl.catalog, "Email Blocked"),
|
|
))
|
|
}
|
|
}
|
|
#[get("/admin/emails?<page>")]
|
|
pub fn admin_email_blocklist(
|
|
_mod: Moderator,
|
|
page: Option<Page>,
|
|
conn: DbConn,
|
|
rockets: PlumeRocket,
|
|
) -> Result<Ructe, ErrorPage> {
|
|
let page = page.unwrap_or_default();
|
|
Ok(render!(instance::emailblocklist(
|
|
&(&conn, &rockets).to_context(),
|
|
BlocklistedEmail::page(&conn, page.limits())?,
|
|
page.0,
|
|
Page::total(BlocklistedEmail::count(&conn)? as i32)
|
|
)))
|
|
}
|
|
|
|
/// A structure to handle forms that are a list of items on which actions are applied.
|
|
///
|
|
/// This is for instance the case of the user list in the administration.
|
|
pub struct MultiAction<T>
|
|
where
|
|
T: FromStr,
|
|
{
|
|
ids: Vec<i32>,
|
|
action: T,
|
|
}
|
|
|
|
impl<'f, T> FromForm<'f> for MultiAction<T>
|
|
where
|
|
T: FromStr,
|
|
{
|
|
type Error = ();
|
|
|
|
fn from_form(items: &mut FormItems<'_>, _strict: bool) -> Result<Self, Self::Error> {
|
|
let (ids, act) = items.fold((vec![], None), |(mut ids, act), item| {
|
|
let (name, val) = item.key_value_decoded();
|
|
|
|
if name == "action" {
|
|
(ids, T::from_str(&val).ok())
|
|
} else if let Ok(id) = name.parse::<i32>() {
|
|
ids.push(id);
|
|
(ids, act)
|
|
} else {
|
|
(ids, act)
|
|
}
|
|
});
|
|
|
|
if let Some(act) = act {
|
|
Ok(MultiAction { ids, action: act })
|
|
} else {
|
|
Err(())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum UserActions {
|
|
Admin,
|
|
RevokeAdmin,
|
|
Moderator,
|
|
RevokeModerator,
|
|
Ban,
|
|
}
|
|
|
|
impl FromStr for UserActions {
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"admin" => Ok(UserActions::Admin),
|
|
"un-admin" => Ok(UserActions::RevokeAdmin),
|
|
"moderator" => Ok(UserActions::Moderator),
|
|
"un-moderator" => Ok(UserActions::RevokeModerator),
|
|
"ban" => Ok(UserActions::Ban),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[post("/admin/users/edit", data = "<form>")]
|
|
pub fn edit_users(
|
|
moderator: Moderator,
|
|
form: LenientForm<MultiAction<UserActions>>,
|
|
conn: DbConn,
|
|
rockets: PlumeRocket,
|
|
) -> Result<Flash<Redirect>, ErrorPage> {
|
|
// you can't change your own rights
|
|
if form.ids.contains(&moderator.0.id) {
|
|
return Ok(Flash::error(
|
|
Redirect::to(uri!(admin_users: page = _)),
|
|
i18n!(rockets.intl.catalog, "You can't change your own rights."),
|
|
));
|
|
}
|
|
|
|
// moderators can't grant or revoke admin rights
|
|
if !moderator.0.is_admin() {
|
|
match form.action {
|
|
UserActions::Admin | UserActions::RevokeAdmin => {
|
|
return Ok(Flash::error(
|
|
Redirect::to(uri!(admin_users: page = _)),
|
|
i18n!(
|
|
rockets.intl.catalog,
|
|
"You are not allowed to take this action."
|
|
),
|
|
))
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let worker = &*rockets.worker;
|
|
match form.action {
|
|
UserActions::Admin => {
|
|
for u in form.ids.clone() {
|
|
User::get(&conn, u)?.set_role(&conn, Role::Admin)?;
|
|
}
|
|
}
|
|
UserActions::Moderator => {
|
|
for u in form.ids.clone() {
|
|
User::get(&conn, u)?.set_role(&conn, Role::Moderator)?;
|
|
}
|
|
}
|
|
UserActions::RevokeAdmin | UserActions::RevokeModerator => {
|
|
for u in form.ids.clone() {
|
|
User::get(&conn, u)?.set_role(&conn, Role::Normal)?;
|
|
}
|
|
}
|
|
UserActions::Ban => {
|
|
for u in form.ids.clone() {
|
|
ban(u, &conn, worker)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Flash::success(
|
|
Redirect::to(uri!(admin_users: page = _)),
|
|
i18n!(rockets.intl.catalog, "Done."),
|
|
))
|
|
}
|
|
|
|
fn ban(id: i32, conn: &Connection, worker: &ScheduledThreadPool) -> Result<(), ErrorPage> {
|
|
let u = User::get(&*conn, id)?;
|
|
u.delete(&*conn)?;
|
|
if Instance::get_local()
|
|
.map(|i| u.instance_id == i.id)
|
|
.unwrap_or(false)
|
|
{
|
|
BlocklistedEmail::insert(
|
|
conn,
|
|
NewBlocklistedEmail {
|
|
email_address: u.email.clone().unwrap(),
|
|
note: "Banned".to_string(),
|
|
notify_user: false,
|
|
notification_text: "".to_owned(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
let target = User::one_by_instance(&*conn)?;
|
|
let delete_act = u.delete_activity(&*conn)?;
|
|
worker.execute(move || broadcast(&u, delete_act, target, CONFIG.proxy().cloned()));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[post("/inbox", data = "<data>")]
|
|
pub fn shared_inbox(
|
|
conn: DbConn,
|
|
data: inbox::SignedJson<serde_json::Value>,
|
|
headers: Headers<'_>,
|
|
) -> Result<String, status::BadRequest<&'static str>> {
|
|
inbox::handle_incoming(conn, data, headers)
|
|
}
|
|
|
|
#[get("/remote_interact?<target>")]
|
|
pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redirect> {
|
|
if User::find_by_fqn(&conn, &target).is_ok() {
|
|
return Some(Redirect::to(uri!(super::user::details: name = target)));
|
|
}
|
|
|
|
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
|
|
return Some(Redirect::to(uri!(
|
|
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
|
|
slug = &post.slug,
|
|
responding_to = _
|
|
)));
|
|
}
|
|
|
|
if let Ok(comment) = Comment::from_id(&conn, &target, None, CONFIG.proxy()) {
|
|
if comment.can_see(&conn, user.as_ref()) {
|
|
let post = comment.get_post(&conn).expect("Can't retrieve post");
|
|
return Some(Redirect::to(uri!(
|
|
super::posts::details: blog =
|
|
post.get_blog(&conn).expect("Can't retrieve blog").fqn,
|
|
slug = &post.slug,
|
|
responding_to = comment.id
|
|
)));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[get("/nodeinfo/<version>")]
|
|
pub fn nodeinfo(conn: DbConn, version: String) -> Result<Json<serde_json::Value>, ErrorPage> {
|
|
if version != "2.0" && version != "2.1" {
|
|
return Err(ErrorPage::from(Error::NotFound));
|
|
}
|
|
|
|
let local_inst = Instance::get_local()?;
|
|
let mut doc = json!({
|
|
"version": version,
|
|
"software": {
|
|
"name": env!("CARGO_PKG_NAME"),
|
|
"version": env!("CARGO_PKG_VERSION"),
|
|
},
|
|
"protocols": ["activitypub"],
|
|
"services": {
|
|
"inbound": [],
|
|
"outbound": []
|
|
},
|
|
"openRegistrations": local_inst.open_registrations,
|
|
"usage": {
|
|
"users": {
|
|
"total": User::count_local(&conn)?
|
|
},
|
|
"localPosts": Post::count_local(&conn)?,
|
|
"localComments": Comment::count_local(&conn)?
|
|
},
|
|
"metadata": {
|
|
"nodeName": local_inst.name,
|
|
"nodeDescription": local_inst.short_description
|
|
}
|
|
});
|
|
|
|
if version == "2.1" {
|
|
doc["software"]["repository"] = json!(env!("CARGO_PKG_REPOSITORY"));
|
|
}
|
|
|
|
Ok(Json(doc))
|
|
}
|
|
|
|
#[get("/about")]
|
|
pub fn about(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
|
Ok(render!(instance::about(
|
|
&(&conn, &rockets).to_context(),
|
|
Instance::get_local()?,
|
|
Instance::get_local()?.main_admin(&conn)?,
|
|
User::count_local(&conn)?,
|
|
Post::count_local(&conn)?,
|
|
Instance::count(&conn)? - 1
|
|
)))
|
|
}
|
|
|
|
#[get("/privacy")]
|
|
pub fn privacy(conn: DbConn, rockets: PlumeRocket) -> Ructe {
|
|
render!(instance::privacy(&(&conn, &rockets).to_context()))
|
|
}
|
|
|
|
#[get("/manifest.json")]
|
|
pub fn web_manifest() -> Result<Json<serde_json::Value>, ErrorPage> {
|
|
let instance = Instance::get_local()?;
|
|
Ok(Json(json!({
|
|
"name": &instance.name,
|
|
"description": &instance.short_description,
|
|
"start_url": String::from("/"),
|
|
"scope": String::from("/"),
|
|
"display": String::from("standalone"),
|
|
"background_color": String::from("#f4f4f4"),
|
|
"theme_color": String::from("#7765e3"),
|
|
"categories": [String::from("social")],
|
|
"icons": CONFIG.logo.other.iter()
|
|
.map(|i| i.with_prefix(&uri!(static_files: file = "").to_string()))
|
|
.collect::<Vec<_>>()
|
|
})))
|
|
}
|