Skip to content

Commit

Permalink
Comment visibility (#364)
Browse files Browse the repository at this point in the history
Add some support for comment visibility, fix #217 

This add a new column to comment, denoting if they are public or not, and a new table linking private comments to those allowed to read them. There is currently no way to write a private comment from Plume.
Git is having a hard time what happened in Comment::from_activity, but most of it is just re-indentation because a new block was needed to please the borrow checker. I've marked with comments where things actually changed.
At this point only mentioned users can see private comments, even when posted as "follower only" or equivalent.

What should we do when someone isn't allowed to see a comment? Hide the whole thread, or just the comment? If hiding just the comment, should we mark there is a comment one can't see, but answers they can, or put other comments like if they answered to the same comment the hidden one do?
  • Loading branch information
trinity-1686a authored and elegaanz committed Dec 24, 2018
1 parent 2621549 commit fdfeeed
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
ALTER TABLE comments DROP COLUMN public_visibility;

DROP TABLE comment_seers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Your SQL goes here
ALTER TABLE comments ADD public_visibility BOOLEAN NOT NULL DEFAULT 't';

CREATE TABLE comment_seers (
id SERIAL PRIMARY KEY,
comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE NOT NULL,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
UNIQUE (comment_id, user_id)
)
28 changes: 28 additions & 0 deletions migrations/sqlite/2018-12-17-221135_comment_visibility/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- This file should undo anything in `up.sql`
CREATE TABLE comments2 (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL DEFAULT '',
in_response_to_id INTEGER REFERENCES comments(id),
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL,
author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ap_url VARCHAR,
sensitive BOOLEAN NOT NULL DEFAULT 'f',
spoiler_text TEXT NOT NULL DEFAULT ''
);

INSERT INTO comments2 SELECT
id,
content,
in_response_to_id,
post_id,
author_id,
creation_date,
ap_url,
sensitive,
spoiler_text
FROM comments;
DROP TABLE comments;
ALTER TABLE comments2 RENAME TO comments;

DROP TABLE comment_seers;
9 changes: 9 additions & 0 deletions migrations/sqlite/2018-12-17-221135_comment_visibility/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Your SQL goes here
ALTER TABLE comments ADD public_visibility BOOLEAN NOT NULL DEFAULT 't';

CREATE TABLE comment_seers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
comment_id INTEGER REFERENCES comments(id) ON DELETE CASCADE NOT NULL,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
UNIQUE (comment_id, user_id)
)
32 changes: 32 additions & 0 deletions plume-models/src/comment_seers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};

use comments::Comment;
use schema::comment_seers;
use users::User;
use Connection;

#[derive(Queryable, Serialize, Clone)]
pub struct CommentSeers {
pub id: i32,
pub comment_id: i32,
pub user_id: i32,
}

#[derive(Insertable, Default)]
#[table_name = "comment_seers"]
pub struct NewCommentSeers {
pub comment_id: i32,
pub user_id: i32,
}

impl CommentSeers {
insert!(comment_seers, NewCommentSeers);

pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> bool {
!comment_seers::table.filter(comment_seers::comment_id.eq(c.id))
.filter(comment_seers::user_id.eq(u.id))
.load::<CommentSeers>(conn)
.expect("Comment::get_responses: loading error")
.is_empty()
}
}
193 changes: 141 additions & 52 deletions plume-models/src/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use serde_json;

use std::collections::HashSet;

use instance::Instance;
use mentions::Mention;
use notifications::*;
Expand All @@ -11,6 +13,7 @@ use plume_common::activity_pub::{
Id, IntoId, PUBLIC_VISIBILTY,
};
use plume_common::utils;
use comment_seers::{CommentSeers, NewCommentSeers};
use posts::Post;
use safe_string::SafeString;
use schema::comments;
Expand All @@ -28,6 +31,7 @@ pub struct Comment {
pub ap_url: Option<String>,
pub sensitive: bool,
pub spoiler_text: String,
pub public_visibility: bool,
}

#[derive(Insertable, Default)]
Expand All @@ -40,6 +44,7 @@ pub struct NewComment {
pub ap_url: Option<String>,
pub sensitive: bool,
pub spoiler_text: String,
pub public_visibility: bool,
}

impl Comment {
Expand Down Expand Up @@ -90,6 +95,11 @@ impl Comment {
format!("{}comment/{}", self.get_post(conn).ap_url, self.id)
}

pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool {
self.public_visibility ||
user.as_ref().map(|u| CommentSeers::can_see(conn, self, u)).unwrap_or(false)
}

pub fn to_activity(&self, conn: &Connection) -> Note {
let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(),
&Instance::get_local(conn)
Expand Down Expand Up @@ -179,59 +189,113 @@ impl Comment {

impl FromActivity<Note, Connection> for Comment {
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment {
let previous_url = note
.object_props
.in_reply_to
.clone()
.expect("Comment::from_activity: not an answer error");
let previous_url = previous_url
.as_str()
.expect("Comment::from_activity: in_reply_to parsing error");
let previous_comment = Comment::find_by_ap_url(conn, previous_url);

let comm = Comment::insert(
conn,
NewComment {
content: SafeString::new(
&note
let comm = {
let previous_url = note
.object_props
.in_reply_to
.as_ref()
.expect("Comment::from_activity: not an answer error")
.as_str()
.expect("Comment::from_activity: in_reply_to parsing error");
let previous_comment = Comment::find_by_ap_url(conn, previous_url);

let is_public = |v: &Option<serde_json::Value>| match v.as_ref().unwrap_or(&serde_json::Value::Null) {
serde_json::Value::Array(v) => v.iter().filter_map(serde_json::Value::as_str).any(|s| s==PUBLIC_VISIBILTY),
serde_json::Value::String(s) => s == PUBLIC_VISIBILTY,
_ => false,
};

let public_visibility = is_public(&note.object_props.to) ||
is_public(&note.object_props.bto) ||
is_public(&note.object_props.cc) ||
is_public(&note.object_props.bcc);

let comm = Comment::insert(
conn,
NewComment {
content: SafeString::new(
&note
.object_props
.content_string()
.expect("Comment::from_activity: content deserialization error"),
),
spoiler_text: note
.object_props
.content_string()
.expect("Comment::from_activity: content deserialization error"),
),
spoiler_text: note
.object_props
.summary_string()
.unwrap_or_default(),
ap_url: note.object_props.id_string().ok(),
in_response_to_id: previous_comment.clone().map(|c| c.id),
post_id: previous_comment.map(|c| c.post_id).unwrap_or_else(|| {
Post::find_by_ap_url(conn, previous_url)
.expect("Comment::from_activity: post error")
.id
}),
author_id: User::from_url(conn, actor.as_ref())
.expect("Comment::from_activity: author error")
.id,
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
},
);

// save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags {
serde_json::from_value::<link::Mention>(tag)
.map(|m| {
let author = &Post::get(conn, comm.post_id)
.expect("Comment::from_activity: error")
.get_authors(conn)[0];
let not_author = m
.link_props
.href_string()
.expect("Comment::from_activity: no href error")
!= author.ap_url.clone();
Mention::from_activity(conn, &m, comm.id, false, not_author)
})
.ok();
.summary_string()
.unwrap_or_default(),
ap_url: note.object_props.id_string().ok(),
in_response_to_id: previous_comment.clone().map(|c| c.id),
post_id: previous_comment.map(|c| c.post_id).unwrap_or_else(|| {
Post::find_by_ap_url(conn, previous_url)
.expect("Comment::from_activity: post error")
.id
}),
author_id: User::from_url(conn, actor.as_ref())
.expect("Comment::from_activity: author error")
.id,
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
public_visibility
},
);

// save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags {
serde_json::from_value::<link::Mention>(tag)
.map(|m| {
let author = &Post::get(conn, comm.post_id)
.expect("Comment::from_activity: error")
.get_authors(conn)[0];
let not_author = m
.link_props
.href_string()
.expect("Comment::from_activity: no href error")
!= author.ap_url.clone();
Mention::from_activity(conn, &m, comm.id, false, not_author)
})
.ok();
}
}
comm
};


if !comm.public_visibility {
let receivers_ap_url = |v: Option<serde_json::Value>| {
let filter = |e: serde_json::Value| if let serde_json::Value::String(s) = e { Some(s) } else { None };
match v.unwrap_or(serde_json::Value::Null) {
serde_json::Value::Array(v) => v,
v => vec![v],
}.into_iter().filter_map(filter)
};

let mut note = note;

let to = receivers_ap_url(note.object_props.to.take());
let cc = receivers_ap_url(note.object_props.cc.take());
let bto = receivers_ap_url(note.object_props.bto.take());
let bcc = receivers_ap_url(note.object_props.bcc.take());

let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc)
.collect::<HashSet<_>>()//remove duplicates (don't do a query more than once)
.into_iter()
.map(|v| if let Some(user) = User::from_url(conn,&v) {
vec![user]
} else {
vec![]// TODO try to fetch collection
})
.flatten()
.filter(|u| u.get_instance(conn).local)
.collect::<HashSet<User>>();//remove duplicates (prevent db error)

for user in &receivers_ap_url {
CommentSeers::insert(
conn,
NewCommentSeers {
comment_id: comm.id,
user_id: user.id
}
);
}
}

Expand All @@ -255,6 +319,31 @@ impl Notify<Connection> for Comment {
}
}

pub struct CommentTree {
pub comment: Comment,
pub responses: Vec<CommentTree>,
}

impl CommentTree {
pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Vec<Self> {
Comment::list_by_post(conn, p.id).into_iter()
.filter(|c| c.in_response_to_id.is_none())
.filter(|c| c.can_see(conn, user))
.map(|c| Self::from_comment(conn, c, user))
.collect()
}

pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Self {
let responses = comment.get_responses(conn).into_iter()
.filter(|c| c.can_see(conn, user))
.map(|c| Self::from_comment(conn, c, user))
.collect();
CommentTree {
comment,
responses,
}
}
}

impl<'a> Deletable<Connection, Delete> for Comment {
fn delete(&self, conn: &Connection) -> Delete {
Expand Down
1 change: 1 addition & 0 deletions plume-models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ pub mod apps;
pub mod blog_authors;
pub mod blogs;
pub mod comments;
pub mod comment_seers;
pub mod db_conn;
pub mod follows;
pub mod headers;
Expand Down
12 changes: 12 additions & 0 deletions plume-models/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ table! {
ap_url -> Nullable<Varchar>,
sensitive -> Bool,
spoiler_text -> Text,
public_visibility -> Bool,
}
}

table! {
comment_seers (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
}
}

Expand Down Expand Up @@ -200,6 +209,8 @@ joinable!(api_tokens -> users (user_id));
joinable!(blog_authors -> blogs (blog_id));
joinable!(blog_authors -> users (author_id));
joinable!(blogs -> instances (instance_id));
joinable!(comment_seers -> comments (comment_id));
joinable!(comment_seers -> users (user_id));
joinable!(comments -> posts (post_id));
joinable!(comments -> users (author_id));
joinable!(likes -> posts (post_id));
Expand All @@ -223,6 +234,7 @@ allow_tables_to_appear_in_same_query!(
blog_authors,
blogs,
comments,
comment_seers,
follows,
instances,
likes,
Expand Down
Loading

0 comments on commit fdfeeed

Please sign in to comment.