Comment visibility #364

Merged
Plume_migration_agent merged 6 commits from comment-visibility into master 5 years ago

@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
ALTER TABLE comments DROP COLUMN public_visibility;
DROP TABLE comment_seers;

@ -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)
)

@ -0,0 +1,28 @@
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
-- This file should undo anything in `up.sql`
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
CREATE TABLE comments2 (
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
content TEXT NOT NULL DEFAULT '',
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
in_response_to_id INTEGER REFERENCES comments(id),
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
author_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
ap_url VARCHAR,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
sensitive BOOLEAN NOT NULL DEFAULT 'f',
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
spoiler_text TEXT NOT NULL DEFAULT ''
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
);
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
INSERT INTO comments2 SELECT
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
id,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
content,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
in_response_to_id,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
post_id,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
author_id,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
creation_date,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
ap_url,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
sensitive,
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
spoiler_text
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
FROM comments;
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
DROP TABLE comments;
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
ALTER TABLE comments2 RENAME TO comments;
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵
DROP TABLE comment_seers;
igalic commented 5 years ago (Migrated from github.com)
Review

if you put these one into each line, my short attention span will be able to follow it 😵

if you put these one into each line, my short attention span will be able to follow it 😵

@ -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)
)

@ -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()
}
}

@ -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::*;
@ -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;
@ -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)]
@ -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 {
@ -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)
@ -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
Review

things between here and next message were added

things between here and next message were added
};
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)
Review

things between here and previous message were added

things between here and previous message were added
};
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
}
);
}
}
@ -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 {

@ -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;

@ -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,
}
}
@ -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));
@ -223,6 +234,7 @@ allow_tables_to_appear_in_same_query!(
blog_authors,
blogs,
comments,
comment_seers,
follows,
instances,
likes,

@ -26,7 +26,7 @@ use rocket::{
request::{self, FromRequest, Request},
};
use serde_json;
use std::cmp::PartialEq;
use std::{cmp::PartialEq, hash::{Hash, Hasher}};
use url::Url;
use webfinger::*;
@ -900,6 +900,7 @@ impl IntoId for User {
}
}
impl Eq for User {}
impl Object for User {}
impl Actor for User {}
@ -956,6 +957,12 @@ impl PartialEq for User {
}
}
impl Hash for User {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl NewUser {
/// Creates a new local user
pub fn new_local(

@ -45,7 +45,8 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
author_id: user.id,
ap_url: None,
sensitive: !form.warning.is_empty(),
spoiler_text: form.warning.clone()
spoiler_text: form.warning.clone(),
public_visibility: true
}).update_ap_url(&*conn);
let new_comment = comm.create_activity(&*conn);
@ -63,7 +64,7 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
})
.map_err(|errors| {
// TODO: de-duplicate this code
let comments = Comment::list_by_post(&*conn, post.id);
let comments = CommentTree::from_post(&*conn, &post, Some(&user));
let previous = form.responding_to.map(|r| Comment::get(&*conn, r)
.expect("comments::create: Error retrieving previous comment"));
@ -75,7 +76,7 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
&*form,
errors,
Tag::for_post(&*conn, post.id),
comments.into_iter().filter(|c| c.in_response_to_id.is_none()).collect::<Vec<Comment>>(),
comments,
previous,
post.count_likes(&*conn),
post.count_reshares(&*conn),

@ -11,7 +11,7 @@ use plume_common::utils;
use plume_models::{
blogs::*,
db_conn::DbConn,
comments::Comment,
comments::{Comment, CommentTree},
instance::Instance,
medias::Media,
mentions::Mention,
@ -31,7 +31,7 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
let blog = Blog::find_by_fqn(&*conn, &blog).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
if post.published || post.get_authors(&*conn).into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
let comments = Comment::list_by_post(&*conn, post.id);
let comments = CommentTree::from_post(&*conn, &post, user.as_ref());
let previous = responding_to.map(|r| Comment::get(&*conn, r)
.expect("posts::details_reponse: Error retrieving previous comment"));
@ -64,7 +64,7 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
},
ValidationErrors::default(),
Tag::for_post(&*conn, post.id),
comments.into_iter().filter(|c| c.in_response_to_id.is_none()).collect::<Vec<Comment>>(),
comments,
previous,
post.count_likes(&*conn),
post.count_reshares(&*conn),

@ -1,10 +1,11 @@
@use template_utils::*;
@use plume_models::comments::Comment;
@use plume_models::users::User;
@use plume_models::comments::CommentTree;
@use routes::*;
@(ctx: BaseContext, comm: &Comment, author: User, in_reply_to: Option<&str>, blog: &str, slug: &str)
@(ctx: BaseContext, comment_tree: &CommentTree, in_reply_to: Option<&str>, blog: &str, slug: &str)
@if let Some(ref comm) = Some(&comment_tree.comment) {
@if let Some(author) = Some(comm.get_author(ctx.0)) {
<div class="comment u-comment h-cite" id="comment-@comm.id">
<a class="author u-author h-card" href="@uri!(user::details: name = author.get_fqn(ctx.0))">
@avatar(ctx.0, &author, Size::Small, true, ctx.1)
@ -33,7 +34,8 @@
<input onclick="return confirm('Are you sure you?')" type="submit" value="@i18n!(ctx.1, "Delete this comment")">
</form>
}
@for res in comm.get_responses(ctx.0) {
@:comment(ctx, &res, res.get_author(ctx.0), comm.ap_url.as_ref().map(|u| &**u), blog, slug)
@for res in &comment_tree.responses {
@:comment(ctx, res, comm.ap_url.as_ref().map(|u| &**u), blog, slug)
}
</div>
}}

@ -1,7 +1,7 @@
@use templates::{base, partials::comment};
@use template_utils::*;
@use plume_models::blogs::Blog;
@use plume_models::comments::Comment;
@use plume_models::comments::{Comment, CommentTree};
@use plume_models::posts::Post;
@use plume_models::tags::Tag;
@use plume_models::users::User;
@ -9,7 +9,7 @@
@use routes::comments::NewCommentForm;
@use routes::*;
@(ctx: BaseContext, article: Post, blog: Blog, comment_form: &NewCommentForm, comment_errors: ValidationErrors, tags: Vec<Tag>, comments: Vec<Comment>, previous_comment: Option<Comment>, n_likes: i64, n_reshares: i64, has_liked: bool, has_reshared: bool, is_following: bool, author: User)
@(ctx: BaseContext, article: Post, blog: Blog, comment_form: &NewCommentForm, comment_errors: ValidationErrors, tags: Vec<Tag>, comments: Vec<CommentTree>, previous_comment: Option<Comment>, n_likes: i64, n_reshares: i64, has_liked: bool, has_reshared: bool, is_following: bool, author: User)
@:base(ctx, &article.title.clone(), {
<meta property="og:title" content="@article.title"/>
@ -146,7 +146,7 @@
@if !comments.is_empty() {
<div class="list">
@for comm in comments {
@:comment(ctx, &comm, comm.get_author(ctx.0), Some(&article.ap_url), &blog.get_fqn(ctx.0), &article.slug)
@:comment(ctx, &comm, Some(&article.ap_url), &blog.get_fqn(ctx.0), &article.slug)
}
</div>
} else {

Loading…
Cancel
Save