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?

![image](https://user-images.githubusercontent.com/16254623/53894510-7d853300-4030-11e9-8a2c-f5c0b0c7f512.png)
![image](https://user-images.githubusercontent.com/16254623/53894539-8b3ab880-4030-11e9-8113-685a27be8d7c.png)

Fixes #453
Fixes #454
This commit is contained in:
Baptiste Gelez
2019-03-22 19:51:36 +01:00
committed by GitHub
parent 6cd9c8a01a
commit bdfad844d7
41 changed files with 1391 additions and 456 deletions
+100 -4
View File
@@ -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))?;
+1
View File
@@ -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
View File
@@ -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(),
+10 -4
View File
@@ -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![])?;
+2 -2
View File
@@ -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),
+5 -5
View File
@@ -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;")
+3 -2
View File
@@ -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())
+3
View File
@@ -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>,
}
}
+4 -3
View File
@@ -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,
+2 -1
View File
@@ -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
}