Skip to content

Commit

Permalink
Hide cw pictures behind a summary/details (#483)
Browse files Browse the repository at this point in the history
* Hide cw pictures behind a summary/details
* refactor md_to_html a bit and add cw support
* use random id for cw checkbox
  • Loading branch information
trinity-1686a authored Apr 6, 2019
1 parent eabe73d commit 12c2078
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 62 deletions.
146 changes: 105 additions & 41 deletions plume-common/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,53 +56,117 @@ fn to_inline(tag: Tag) -> Tag {
}
}

fn flatten_text<'a>(state: &mut Option<String>, evt: Event<'a>) -> Option<Vec<Event<'a>>> {
let (s, res) = match evt {
Event::Text(txt) => match state.take() {
Some(mut prev_txt) => {
prev_txt.push_str(&txt);
(Some(prev_txt), vec![])
}
None => (Some(txt.into_owned()), vec![]),
},
e => match state.take() {
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
None => (None, vec![e]),
},
};
*state = s;
Some(res)
}

fn inline_tags<'a>(
(state, inline): &mut (Vec<Tag<'a>>, bool),
evt: Event<'a>,
) -> Option<Event<'a>> {
if *inline {
let new_evt = match evt {
Event::Start(t) => {
let tag = to_inline(t);
state.push(tag.clone());
Event::Start(tag)
}
Event::End(t) => match state.pop() {
Some(other) => Event::End(other),
None => Event::End(t),
},
e => e,
};
Some(new_evt)
} else {
Some(evt)
}
}

pub type MediaProcessor<'a> = Box<'a + Fn(i32) -> Option<(String, Option<String>)>>;

fn process_image<'a, 'b>(
evt: Event<'a>,
inline: bool,
processor: &Option<MediaProcessor<'b>>,
) -> Event<'a> {
if let Some(ref processor) = *processor {
match evt {
Event::Start(Tag::Image(id, title)) => {
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
if inline || cw.is_none() {
Event::Start(Tag::Image(Cow::Owned(url), title))
} else {
// there is a cw, and where are not inline
Event::Html(Cow::Owned(format!(
r#"<label for="postcontent-cw-{id}">
<input type="checkbox" id="postcontent-cw-{id}" checked="checked" class="cw-checkbox">
<span class="cw-container">
<span class="cw-text">
{cw}
</span>
<img src="{url}" alt=""#,
id = random_hex(),
cw = cw.unwrap(),
url = url
)))
}
} else {
Event::Start(Tag::Image(id, title))
}
}
Event::End(Tag::Image(id, title)) => {
if let Some((url, cw)) = id.parse::<i32>().ok().and_then(processor.as_ref()) {
if inline || cw.is_none() {
Event::End(Tag::Image(Cow::Owned(url), title))
} else {
Event::Html(Cow::Borrowed(
r#""/>
</span>
</label>"#,
))
}
} else {
Event::End(Tag::Image(id, title))
}
}
e => e,
}
} else {
evt
}
}

/// Returns (HTML, mentions, hashtags)
pub fn md_to_html(
pub fn md_to_html<'a>(
md: &str,
base_url: &str,
inline: bool,
media_processor: Option<MediaProcessor<'a>>,
) -> (String, HashSet<String>, HashSet<String>) {
let parser = Parser::new_ext(md, Options::all());

let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
.scan(None, |state: &mut Option<String>, evt| {
let (s, res) = match evt {
Event::Text(txt) => match state.take() {
Some(mut prev_txt) => {
prev_txt.push_str(&txt);
(Some(prev_txt), vec![])
}
None => (Some(txt.into_owned()), vec![]),
},
e => match state.take() {
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
None => (None, vec![e]),
},
};
*state = s;
Some(res)
})
// Flatten text because pulldown_cmark break #hashtag in two individual text elements
.scan(None, flatten_text)
.flat_map(IntoIterator::into_iter)
.map(|evt| process_image(evt, inline, &media_processor))
// Ignore headings, images, and tables if inline = true
.scan(vec![], |state: &mut Vec<Tag>, evt| {
if inline {
let new_evt = match evt {
Event::Start(t) => {
let tag = to_inline(t);
state.push(tag.clone());
Event::Start(tag)
}
Event::End(t) => match state.pop() {
Some(other) => Event::End(other),
None => Event::End(t),
},
e => e,
};
Some(new_evt)
} else {
Some(evt)
}
})
.scan((vec![], inline), inline_tags)
.map(|evt| match evt {
Event::Text(txt) => {
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
Expand Down Expand Up @@ -273,7 +337,7 @@ mod tests {

for (md, mentions) in tests {
assert_eq!(
md_to_html(md, "", false).1,
md_to_html(md, "", false, None).1,
mentions
.into_iter()
.map(|s| s.to_string())
Expand All @@ -298,7 +362,7 @@ mod tests {

for (md, mentions) in tests {
assert_eq!(
md_to_html(md, "", false).2,
md_to_html(md, "", false, None).2,
mentions
.into_iter()
.map(|s| s.to_string())
Expand All @@ -310,11 +374,11 @@ mod tests {
#[test]
fn test_inline() {
assert_eq!(
md_to_html("# Hello", "", false).0,
md_to_html("# Hello", "", false, None).0,
String::from("<h1>Hello</h1>\n")
);
assert_eq!(
md_to_html("# Hello", "", true).0,
md_to_html("# Hello", "", true, None).0,
String::from("<p>Hello</p>\n")
);
}
Expand Down
7 changes: 5 additions & 2 deletions plume-models/src/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::collections::HashSet;

use comment_seers::{CommentSeers, NewCommentSeers};
use instance::Instance;
use medias::Media;
use mentions::Mention;
use notifications::*;
use plume_common::activity_pub::{
Expand Down Expand Up @@ -102,14 +103,16 @@ impl Comment {
.unwrap_or(false)
}

pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
pub fn to_activity<'b>(&self, conn: &'b Connection) -> Result<Note> {
let author = User::get(conn, self.author_id)?;

let (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(),
&Instance::get_local(conn)?.public_domain,
true,
Some(Media::get_media_processor(conn, vec![&author])),
);

let author = User::get(conn, self.author_id)?;
let mut note = Note::default();
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];

Expand Down
15 changes: 13 additions & 2 deletions plume-models/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::iter::Iterator;

use ap_url;
use medias::Media;
use plume_common::utils::md_to_html;
use safe_string::SafeString;
use schema::{instances, users};
Expand Down Expand Up @@ -128,8 +129,18 @@ impl Instance {
short_description: SafeString,
long_description: SafeString,
) -> Result<()> {
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);
let (sd, _, _) = md_to_html(
short_description.as_ref(),
&self.public_domain,
true,
Some(Media::get_media_processor(conn, vec![])),
);
let (ld, _, _) = md_to_html(
long_description.as_ref(),
&self.public_domain,
false,
Some(Media::get_media_processor(conn, vec![])),
);
diesel::update(self)
.set((
instances::name.eq(name),
Expand Down
18 changes: 15 additions & 3 deletions plume-models/src/medias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use guid_create::GUID;
use reqwest;
use std::{fs, path::Path};

use plume_common::activity_pub::Id;
use plume_common::{activity_pub::Id, utils::MediaProcessor};

use instance::Instance;
use safe_string::SafeString;
Expand Down Expand Up @@ -124,10 +124,9 @@ impl Media {
}

pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
let url = self.url(conn)?;
Ok(match self.category() {
MediaCategory::Image => {
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url))
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), self.id))
}
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
MediaCategory::Unknown => SafeString::new(""),
Expand Down Expand Up @@ -225,6 +224,19 @@ impl Media {
},
)
}

pub fn get_media_processor<'a>(conn: &'a Connection, user: Vec<&User>) -> MediaProcessor<'a> {
let uid = user.iter().map(|u| u.id).collect::<Vec<_>>();
Box::new(move |id| {
let media = Media::get(conn, id).ok()?;
// if owner is user or check is disabled
if uid.contains(&media.owner_id) || uid.is_empty() {
Some((media.url(conn).ok()?, media.content_warning))
} else {
None
}
})
}
}

#[cfg(test)]
Expand Down
16 changes: 9 additions & 7 deletions plume-models/src/posts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,19 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
let domain = &Instance::get_local(&conn)
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
.public_domain;
let author = User::get(
conn,
user_id.expect("<Post as Provider>::create: no user_id error"),
)
.map_err(|_| ApiError::NotFound("Author not found".into()))?;

let (content, mentions, hashtags) = md_to_html(
query.source.clone().unwrap_or_default().clone().as_ref(),
domain,
false,
Some(Media::get_media_processor(conn, vec![&author])),
);

let author = User::get(
conn,
user_id.expect("<Post as Provider>::create: no user_id error"),
)
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
let blog = match query.blog_id {
Some(x) => x,
None => {
Expand Down Expand Up @@ -757,7 +759,7 @@ impl Post {
post.license = license;
}

let mut txt_hashtags = md_to_html(&post.source, "", false)
let mut txt_hashtags = md_to_html(&post.source, "", false, None)
.2
.into_iter()
.map(|s| s.to_camel_case())
Expand Down Expand Up @@ -995,7 +997,7 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
}

// save mentions and tags
let mut hashtags = md_to_html(&post.source, "", false)
let mut hashtags = md_to_html(&post.source, "", false, None)
.2
.into_iter()
.map(|s| s.to_camel_case())
Expand Down
20 changes: 18 additions & 2 deletions plume-models/src/safe_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,31 @@ lazy_static! {
static ref CLEAN: Builder<'static> = {
let mut b = Builder::new();
b.add_generic_attributes(iter::once("id"))
.add_tags(&["iframe", "video", "audio"])
.add_tags(&["iframe", "video", "audio", "label", "input"])
.id_prefix(Some("postcontent-"))
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
.add_tag_attributes(
"iframe",
["width", "height", "src", "frameborder"].iter().cloned(),
)
.add_tag_attributes("video", ["src", "title", "controls"].iter())
.add_tag_attributes("audio", ["src", "title", "controls"].iter());
.add_tag_attributes("audio", ["src", "title", "controls"].iter())
.add_tag_attributes("label", ["for"].iter())
.add_tag_attributes("input", ["type", "checked"].iter())
.add_allowed_classes("input", ["cw-checkbox"].iter())
.add_allowed_classes("span", ["cw-container", "cw-text"].iter())
.attribute_filter(|elem, att, val| match (elem, att) {
("input", "type") => Some("checkbox".into()),
("input", "checked") => Some("checked".into()),
("label", "for") => {
if val.starts_with("postcontent-cw-") {
Some(val.into())
} else {
None
}
}
_ => Some(val.into()),
});
b
};
}
Expand Down
10 changes: 8 additions & 2 deletions plume-models/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,13 @@ impl User {
.set((
users::display_name.eq(name),
users::email.eq(email),
users::summary_html.eq(utils::md_to_html(&summary, "", false).0),
users::summary_html.eq(utils::md_to_html(
&summary,
"",
false,
Some(Media::get_media_processor(conn, vec![self])),
)
.0),
users::summary.eq(summary),
))
.execute(conn)?;
Expand Down Expand Up @@ -868,7 +874,7 @@ impl NewUser {
display_name,
is_admin,
summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false).0),
summary_html: SafeString::new(&utils::md_to_html(&summary, "", false, None).0),
email: Some(email),
hashed_password: Some(password),
instance_id: Instance::get_local(conn)?.id,
Expand Down
Loading

0 comments on commit 12c2078

Please sign in to comment.