Edit blogs, and add blog icons and banners (#460)
Also adds a parameter to `md_to_html` to only render inline elements (so that we don't have titles or images in blog descriptions). And moves the delete button for the blog on the edition page. I still have to update the SQLite migration once others PRs with migrations will be merged. Also, there will be a problem when you edit a blog while not owning its banner or icon: when validating they will be reset to their default values… I don't see a good solution to this until we have a better way to handle uploads with Rocket (the same is probably happening for articles btw). And the icon/banner are not federated yet, I don't know if I should add it to this PR or if it can come after?   Fixes #453 Fixes #454
This commit is contained in:
+100
-4
@@ -1,4 +1,4 @@
|
||||
use activitypub::{actor::Group, collection::OrderedCollection, CustomObject};
|
||||
use activitypub::{actor::Group, collection::OrderedCollection, object::Image, CustomObject};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use openssl::{
|
||||
@@ -16,10 +16,11 @@ use url::Url;
|
||||
use webfinger::*;
|
||||
|
||||
use instance::*;
|
||||
use medias::Media;
|
||||
use plume_common::activity_pub::{
|
||||
ap_accept_header,
|
||||
inbox::{Deletable, WithInbox},
|
||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey,
|
||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
};
|
||||
use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
@@ -31,6 +32,7 @@ use {Connection, Error, Result, CONFIG};
|
||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||
|
||||
#[derive(Queryable, Identifiable, Clone, AsChangeset)]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
pub struct Blog {
|
||||
pub id: i32,
|
||||
pub actor_id: String,
|
||||
@@ -44,6 +46,9 @@ pub struct Blog {
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: String,
|
||||
pub fqn: String,
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Default, Insertable)]
|
||||
@@ -58,6 +63,9 @@ pub struct NewBlog {
|
||||
pub ap_url: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: String,
|
||||
pub summary_html: SafeString,
|
||||
pub icon_id: Option<i32>,
|
||||
pub banner_id: Option<i32>,
|
||||
}
|
||||
|
||||
const BLOG_PREFIX: &str = "~";
|
||||
@@ -189,6 +197,39 @@ impl Blog {
|
||||
},
|
||||
)
|
||||
})?;
|
||||
|
||||
let icon_id = acct
|
||||
.object
|
||||
.object_props
|
||||
.icon_image()
|
||||
.ok()
|
||||
.and_then(|icon| {
|
||||
let owner: String = icon.object_props.attributed_to_link::<Id>().ok()?.into();
|
||||
Media::save_remote(
|
||||
conn,
|
||||
icon.object_props.url_string().ok()?,
|
||||
&User::from_url(conn, &owner).ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(|m| m.id);
|
||||
|
||||
let banner_id = acct
|
||||
.object
|
||||
.object_props
|
||||
.image_image()
|
||||
.ok()
|
||||
.and_then(|banner| {
|
||||
let owner: String = banner.object_props.attributed_to_link::<Id>().ok()?.into();
|
||||
Media::save_remote(
|
||||
conn,
|
||||
banner.object_props.url_string().ok()?,
|
||||
&User::from_url(conn, &owner).ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(|m| m.id);
|
||||
|
||||
Blog::insert(
|
||||
conn,
|
||||
NewBlog {
|
||||
@@ -204,11 +245,14 @@ impl Blog {
|
||||
.public_key_publickey()?
|
||||
.public_key_pem_string()?,
|
||||
private_key: None,
|
||||
banner_id,
|
||||
icon_id,
|
||||
summary_html: SafeString::new(&acct.object.object_props.summary_string()?),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, _conn: &Connection) -> Result<CustomGroup> {
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||
let mut blog = Group::default();
|
||||
blog.ap_actor_props
|
||||
.set_preferred_username_string(self.actor_id.clone())?;
|
||||
@@ -217,7 +261,47 @@ impl Blog {
|
||||
.set_outbox_string(self.outbox_url.clone())?;
|
||||
blog.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())?;
|
||||
blog.object_props.set_summary_string(self.summary.clone())?;
|
||||
blog.object_props
|
||||
.set_summary_string(self.summary_html.to_string())?;
|
||||
blog.ap_object_props.set_source_object(Source {
|
||||
content: self.summary.clone(),
|
||||
media_type: String::from("text/markdown"),
|
||||
})?;
|
||||
|
||||
let mut icon = Image::default();
|
||||
icon.object_props.set_url_string(
|
||||
self.icon_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
icon.object_props.set_attributed_to_link(
|
||||
self.icon_id
|
||||
.and_then(|id| {
|
||||
Media::get(conn, id)
|
||||
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| Id::new(String::new())),
|
||||
)?;
|
||||
blog.object_props.set_icon_object(icon)?;
|
||||
|
||||
let mut banner = Image::default();
|
||||
banner.object_props.set_url_string(
|
||||
self.banner_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
banner.object_props.set_attributed_to_link(
|
||||
self.banner_id
|
||||
.and_then(|id| {
|
||||
Media::get(conn, id)
|
||||
.and_then(|m| Ok(User::get(conn, m.owner_id)?.into_id()))
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| Id::new(String::new())),
|
||||
)?;
|
||||
blog.object_props.set_image_object(banner)?;
|
||||
|
||||
blog.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
@@ -296,6 +380,18 @@ impl Blog {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn icon_url(&self, conn: &Connection) -> String {
|
||||
self.icon_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||
}
|
||||
|
||||
pub fn banner_url(&self, conn: &Connection) -> Option<String> {
|
||||
self.banner_id
|
||||
.and_then(|i| Media::get(conn, i).ok())
|
||||
.and_then(|c| c.url(conn).ok())
|
||||
}
|
||||
|
||||
pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
|
||||
for post in Post::get_for_blog(conn, &self)? {
|
||||
post.delete(&(conn, searcher))?;
|
||||
|
||||
@@ -106,6 +106,7 @@ impl Comment {
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
self.content.get().as_ref(),
|
||||
&Instance::get_local(conn)?.public_domain,
|
||||
true,
|
||||
);
|
||||
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
|
||||
+31
-23
@@ -1,9 +1,9 @@
|
||||
use std::env::{self, var};
|
||||
use rocket::Config as RocketConfig;
|
||||
use rocket::config::Limits;
|
||||
use rocket::Config as RocketConfig;
|
||||
use std::env::{self, var};
|
||||
|
||||
#[cfg(not(test))]
|
||||
const DB_NAME: &str = "plume";
|
||||
const DB_NAME: &str = "plume";
|
||||
#[cfg(test)]
|
||||
const DB_NAME: &str = "plume_tests";
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct Config {
|
||||
pub logo: LogoConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RocketError {
|
||||
InvalidEnv,
|
||||
InvalidAddress,
|
||||
@@ -27,18 +27,31 @@ fn get_rocket_config() -> Result<RocketConfig, RocketError> {
|
||||
let mut c = RocketConfig::active().map_err(|_| RocketError::InvalidEnv)?;
|
||||
|
||||
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
||||
let port = var("ROCKET_PORT").ok().map(|s| s.parse::<u16>().unwrap()).unwrap_or(7878);
|
||||
let port = var("ROCKET_PORT")
|
||||
.ok()
|
||||
.map(|s| s.parse::<u16>().unwrap())
|
||||
.unwrap_or(7878);
|
||||
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| RocketError::InvalidSecretKey)?;
|
||||
let form_size = var("FORM_SIZE").unwrap_or_else(|_| "32".to_owned()).parse::<u64>().unwrap();
|
||||
let activity_size = var("ACTIVITY_SIZE").unwrap_or_else(|_| "1024".to_owned()).parse::<u64>().unwrap();
|
||||
let form_size = var("FORM_SIZE")
|
||||
.unwrap_or_else(|_| "32".to_owned())
|
||||
.parse::<u64>()
|
||||
.unwrap();
|
||||
let activity_size = var("ACTIVITY_SIZE")
|
||||
.unwrap_or_else(|_| "1024".to_owned())
|
||||
.parse::<u64>()
|
||||
.unwrap();
|
||||
|
||||
c.set_address(address).map_err(|_| RocketError::InvalidAddress)?;
|
||||
c.set_address(address)
|
||||
.map_err(|_| RocketError::InvalidAddress)?;
|
||||
c.set_port(port);
|
||||
c.set_secret_key(secret_key).map_err(|_| RocketError::InvalidSecretKey) ?;
|
||||
c.set_secret_key(secret_key)
|
||||
.map_err(|_| RocketError::InvalidSecretKey)?;
|
||||
|
||||
c.set_limits(Limits::new()
|
||||
.limit("forms", form_size * 1024)
|
||||
.limit("json", activity_size * 1024));
|
||||
c.set_limits(
|
||||
Limits::new()
|
||||
.limit("forms", form_size * 1024)
|
||||
.limit("json", activity_size * 1024),
|
||||
);
|
||||
|
||||
Ok(c)
|
||||
}
|
||||
@@ -168,20 +181,15 @@ impl Default for LogoConfig {
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: Config = Config {
|
||||
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
||||
"127.0.0.1:{}",
|
||||
var("ROCKET_PORT").unwrap_or_else(|_| "8000".to_owned()
|
||||
))),
|
||||
"127.0.0.1:{}",
|
||||
var("ROCKET_PORT").unwrap_or_else(|_| "8000".to_owned())
|
||||
)),
|
||||
db_name: DB_NAME,
|
||||
#[cfg(feature = "postgres")]
|
||||
database_url: var("DATABASE_URL").unwrap_or_else(|_| format!(
|
||||
"postgres://plume:plume@localhost/{}",
|
||||
DB_NAME
|
||||
)),
|
||||
database_url: var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
||||
#[cfg(feature = "sqlite")]
|
||||
database_url: var("DATABASE_URL").unwrap_or_else(|_| format!(
|
||||
"{}.sqlite",
|
||||
DB_NAME
|
||||
)),
|
||||
database_url: var("DATABASE_URL").unwrap_or_else(|_| format!("{}.sqlite", DB_NAME)),
|
||||
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
|
||||
rocket: get_rocket_config(),
|
||||
logo: LogoConfig::default(),
|
||||
|
||||
@@ -34,14 +34,16 @@ pub struct NewFollow {
|
||||
}
|
||||
|
||||
impl Follow {
|
||||
insert!(follows, NewFollow, |inserted, conn| {
|
||||
if inserted.ap_url.is_empty() {
|
||||
insert!(
|
||||
follows,
|
||||
NewFollow,
|
||||
|inserted, conn| if inserted.ap_url.is_empty() {
|
||||
inserted.ap_url = ap_url(&format!("{}/follows/{}", CONFIG.base_url, inserted.id));
|
||||
inserted.save_changes(conn).map_err(Error::from)
|
||||
} else {
|
||||
Ok(inserted)
|
||||
}
|
||||
});
|
||||
);
|
||||
get!(follows);
|
||||
find_by!(follows, find_by_ap_url, ap_url as &str);
|
||||
|
||||
@@ -88,7 +90,11 @@ impl Follow {
|
||||
)?;
|
||||
|
||||
let mut accept = Accept::default();
|
||||
let accept_id = ap_url(&format!("{}/follow/{}/accept", CONFIG.base_url.as_str(), &res.id));
|
||||
let accept_id = ap_url(&format!(
|
||||
"{}/follow/{}/accept",
|
||||
CONFIG.base_url.as_str(),
|
||||
&res.id
|
||||
));
|
||||
accept.object_props.set_id_string(accept_id)?;
|
||||
accept.object_props.set_to_link(from.clone().into_id())?;
|
||||
accept.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
|
||||
@@ -128,8 +128,8 @@ impl Instance {
|
||||
short_description: SafeString,
|
||||
long_description: SafeString,
|
||||
) -> Result<()> {
|
||||
let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain);
|
||||
let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain);
|
||||
let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain, true);
|
||||
let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain, false);
|
||||
diesel::update(self)
|
||||
.set((
|
||||
instances::name.eq(name),
|
||||
|
||||
@@ -235,9 +235,9 @@ macro_rules! get {
|
||||
/// ```
|
||||
macro_rules! insert {
|
||||
($table:ident, $from:ident) => {
|
||||
insert!($table, $from, |x, _conn| { Ok(x) });
|
||||
insert!($table, $from, |x, _conn| Ok(x));
|
||||
};
|
||||
($table:ident, $from:ident, |$val:ident, $conn:ident | $after:block) => {
|
||||
($table:ident, $from:ident, |$val:ident, $conn:ident | $( $after:tt )+) => {
|
||||
last!($table);
|
||||
|
||||
pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> {
|
||||
@@ -247,7 +247,7 @@ macro_rules! insert {
|
||||
#[allow(unused_mut)]
|
||||
let mut $val = Self::last(conn)?;
|
||||
let $conn = conn;
|
||||
$after
|
||||
$( $after )+
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -310,8 +310,8 @@ mod tests {
|
||||
}
|
||||
|
||||
pub fn db() -> Conn {
|
||||
let conn =
|
||||
Conn::establish(CONFIG.database_url.as_str()).expect("Couldn't connect to the database");
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str())
|
||||
.expect("Couldn't connect to the database");
|
||||
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
||||
#[cfg(feature = "sqlite")]
|
||||
sql_query("PRAGMA foreign_keys = on;")
|
||||
|
||||
@@ -210,6 +210,7 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
query.source.clone().unwrap_or_default().clone().as_ref(),
|
||||
domain,
|
||||
false,
|
||||
);
|
||||
|
||||
let author = User::get(
|
||||
@@ -756,7 +757,7 @@ impl Post {
|
||||
post.license = license;
|
||||
}
|
||||
|
||||
let mut txt_hashtags = md_to_html(&post.source, "")
|
||||
let mut txt_hashtags = md_to_html(&post.source, "", false)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
@@ -994,7 +995,7 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||
}
|
||||
|
||||
// save mentions and tags
|
||||
let mut hashtags = md_to_html(&post.source, "")
|
||||
let mut hashtags = md_to_html(&post.source, "", false)
|
||||
.2
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
|
||||
@@ -44,6 +44,9 @@ table! {
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Text,
|
||||
fqn -> Text,
|
||||
summary_html -> Text,
|
||||
icon_id -> Nullable<Int4>,
|
||||
banner_id -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ impl User {
|
||||
.set((
|
||||
users::display_name.eq(name),
|
||||
users::email.eq(email),
|
||||
users::summary_html.eq(utils::md_to_html(&summary, "").0),
|
||||
users::summary_html.eq(utils::md_to_html(&summary, "", false).0),
|
||||
users::summary.eq(summary),
|
||||
))
|
||||
.execute(conn)?;
|
||||
@@ -683,7 +683,8 @@ impl User {
|
||||
.set_followers_string(self.followers_endpoint.clone())?;
|
||||
|
||||
let mut endpoints = Endpoint::default();
|
||||
endpoints.set_shared_inbox_string(ap_url(&format!("{}/inbox/", CONFIG.base_url.as_str())))?;
|
||||
endpoints
|
||||
.set_shared_inbox_string(ap_url(&format!("{}/inbox/", CONFIG.base_url.as_str())))?;
|
||||
actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
@@ -867,7 +868,7 @@ impl NewUser {
|
||||
display_name,
|
||||
is_admin,
|
||||
summary: summary.to_owned(),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, "").0),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false).0),
|
||||
email: Some(email),
|
||||
hashed_password: Some(password),
|
||||
instance_id: Instance::get_local(conn)?.id,
|
||||
|
||||
@@ -14,7 +14,8 @@ embed_migrations!("../migrations/sqlite");
|
||||
embed_migrations!("../migrations/postgres");
|
||||
|
||||
fn db() -> Conn {
|
||||
let conn = Conn::establish(CONFIG.database_url.as_str()).expect("Couldn't connect to the database");
|
||||
let conn =
|
||||
Conn::establish(CONFIG.database_url.as_str()).expect("Couldn't connect to the database");
|
||||
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
||||
conn
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user