diff --git a/API.md b/API.md index fb23354..d631b2b 100644 --- a/API.md +++ b/API.md @@ -52,6 +52,7 @@ "inbox": "https://rust-reddit-fediverse/api/v1/user/sally_smith/inbox", "outbox": "https://rust-reddit-fediverse/api/v1/user/sally_smith/outbox", "liked": "https://rust-reddit-fediverse/api/v1/user/sally_smith/liked", + // TODO disliked? "following": "https://rust-reddit-fediverse/api/v1/user/sally_smith/following", "name": "sally_smith", "preferredUsername": "Sally", @@ -62,7 +63,7 @@ "width": 32, "height": 32 }, - "startTime": "2014-12-31T23:00:00-08:00", + "published": "2014-12-31T23:00:00-08:00", "summary"?: "This is sally's profile." } ``` @@ -78,7 +79,7 @@ "http://joe.example.org", ], "followers": "https://rust-reddit-fediverse/api/v1/community/today_i_learned/followers", - "startTime": "2014-12-31T23:00:00-08:00", + "published": "2014-12-31T23:00:00-08:00", "summary"?: "The group's tagline", "attachment: [{}] // TBD, these would be where strong types for custom styles, and images would work. } @@ -95,7 +96,7 @@ "name": "The title of a post, maybe a link to imgur", "url": "https://news.blah.com" "attributedTo": "http://joe.example.org", // The poster - "startTime": "2014-12-31T23:00:00-08:00", + "published": "2014-12-31T23:00:00-08:00", } ``` @@ -116,11 +117,11 @@ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "id": "https://rust-reddit-fediverse/api/v1/comment/1", - "name": "A note", - "content": "Looks like it is going to rain today. Bring an umbrella @sally!" + "mediaType": "text/markdown", + "content": "Looks like it is going to rain today. Bring an umbrella *if necessary*!" "attributedTo": john_id, "inReplyTo": "comment or post id", - "startTime": "2014-12-31T23:00:00-08:00", + "published": "2014-12-31T23:00:00-08:00", "updated"?: "2014-12-12T12:12:12Z" "replies" // TODO, not sure if these objects should embed all replies in them or not. "to": [sally_id, group_id] diff --git a/README.md b/README.md index 9b61044..99a8ad4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ We have a twitter alternative (mastodon), a facebook alternative (friendica), so [ActivityPub API.md](API.md) - ## Goals - Come up with a name / codename. - Must have communities. @@ -21,7 +20,9 @@ We have a twitter alternative (mastodon), a facebook alternative (friendica), so - Backend: Actix, Diesel. - Frontend: inferno, typescript and bootstrap for now. - Should it allow bots? -- Should the comments / votes be static, or feel like a chat, like [flowchat?](https://flow-chat.com). +- Should the comments / votes be static, or feel like a chat, like [flowchat?](https://flow-chat.com). + - Two pane model - Right pane is live comments, left pane is live tree view. + - On mobile, allow you to switch between them. Default? ## Resources / Potential Libraries - Use the [activitypub crate.](https://docs.rs/activitypub/0.1.4/activitypub/) @@ -31,3 +32,9 @@ We have a twitter alternative (mastodon), a facebook alternative (friendica), so - [Diesel to Postgres data types](https://kotiri.com/2018/01/31/postgresql-diesel-rust-types.html) - [helpful diesel examples](http://siciarz.net/24-days-rust-diesel/) - [Mastodan public key server example](https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/) +- [Recursive query for adjacency list for nested comments](https://stackoverflow.com/questions/192220/what-is-the-most-efficient-elegant-way-to-parse-a-flat-table-into-a-tree/192462#192462) + +## TODOs +- Endpoints +- DB +- Followers / following diff --git a/server/.gitignore b/server/.gitignore index fedaa2b..93c43d0 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,2 +1,3 @@ /target .env +.idea diff --git a/server/migrations/2019-03-03-163336_create_post/down.sql b/server/migrations/2019-03-03-163336_create_post/down.sql index a578396..acc0b5d 100644 --- a/server/migrations/2019-03-03-163336_create_post/down.sql +++ b/server/migrations/2019-03-03-163336_create_post/down.sql @@ -1,3 +1,2 @@ drop table post_like; -drop table post_dislike; drop table post; diff --git a/server/migrations/2019-03-03-163336_create_post/up.sql b/server/migrations/2019-03-03-163336_create_post/up.sql index 16ef545..a617ea3 100644 --- a/server/migrations/2019-03-03-163336_create_post/up.sql +++ b/server/migrations/2019-03-03-163336_create_post/up.sql @@ -9,15 +9,9 @@ create table post ( create table post_like ( id serial primary key, + post_id int references post on update cascade on delete cascade not null, fedi_user_id text not null, - post_id int references post on update cascade on delete cascade, - published timestamp not null default now() -); - -create table post_dislike ( - id serial primary key, - fedi_user_id text not null, - post_id int references post on update cascade on delete cascade, + score smallint not null, -- -1, or 1 for dislike, like, no row for no opinion published timestamp not null default now() ); diff --git a/server/migrations/2019-03-05-233828_create_comment/down.sql b/server/migrations/2019-03-05-233828_create_comment/down.sql new file mode 100644 index 0000000..5b92a44 --- /dev/null +++ b/server/migrations/2019-03-05-233828_create_comment/down.sql @@ -0,0 +1,2 @@ +drop table comment_like; +drop table comment; diff --git a/server/migrations/2019-03-05-233828_create_comment/up.sql b/server/migrations/2019-03-05-233828_create_comment/up.sql new file mode 100644 index 0000000..63fc758 --- /dev/null +++ b/server/migrations/2019-03-05-233828_create_comment/up.sql @@ -0,0 +1,17 @@ +create table comment ( + id serial primary key, + content text not null, + attributed_to text not null, + post_id int references post on update cascade on delete cascade not null, + parent_id int references comment on update cascade on delete cascade, + published timestamp not null default now(), + updated timestamp +); + +create table comment_like ( + id serial primary key, + comment_id int references comment on update cascade on delete cascade not null, + fedi_user_id text not null, + score smallint not null, -- -1, or 1 for dislike, like, no row for no opinion + published timestamp not null default now() +); diff --git a/server/src/actions/comment.rs b/server/src/actions/comment.rs new file mode 100644 index 0000000..104c13f --- /dev/null +++ b/server/src/actions/comment.rs @@ -0,0 +1,177 @@ +extern crate diesel; +use schema::{comment, comment_like}; +use diesel::*; +use diesel::result::Error; +use {Crud, Likeable}; + +#[derive(Queryable, Identifiable, PartialEq, Debug)] +#[table_name="comment"] +pub struct Comment { + pub id: i32, + pub content: String, + pub attributed_to: String, + pub post_id: i32, + pub parent_id: Option, + pub published: chrono::NaiveDateTime, + pub updated: Option +} + +#[derive(Insertable, AsChangeset, Clone, Copy)] +#[table_name="comment"] +pub struct CommentForm<'a> { + pub content: &'a str, + pub attributed_to: &'a str, + pub post_id: &'a i32, + pub parent_id: Option<&'a i32>, + pub updated: Option<&'a chrono::NaiveDateTime> +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] +#[belongs_to(Comment)] +#[table_name = "comment_like"] +pub struct CommentLike { + pub id: i32, + pub comment_id: i32, + pub fedi_user_id: String, + pub score: i16, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone, Copy)] +#[table_name="comment_like"] +pub struct CommentLikeForm<'a> { + pub comment_id: &'a i32, + pub fedi_user_id: &'a str, + pub score: &'a i16 +} + +impl<'a> Crud> for Comment { + fn read(conn: &PgConnection, comment_id: i32) -> Comment { + use schema::comment::dsl::*; + comment.find(comment_id) + .first::(conn) + .expect("Error in query") + } + + fn delete(conn: &PgConnection, comment_id: i32) -> usize { + use schema::comment::dsl::*; + diesel::delete(comment.find(comment_id)) + .execute(conn) + .expect("Error deleting.") + } + + fn create(conn: &PgConnection, comment_form: CommentForm) -> Result { + use schema::comment::dsl::*; + insert_into(comment) + .values(comment_form) + .get_result::(conn) + } + + fn update(conn: &PgConnection, comment_id: i32, comment_form: CommentForm) -> Comment { + use schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(comment_form) + .get_result::(conn) + .expect(&format!("Unable to find {}", comment_id)) + } +} + +impl<'a> Likeable > for CommentLike { + fn like(conn: &PgConnection, comment_like_form: CommentLikeForm) -> Result { + use schema::comment_like::dsl::*; + insert_into(comment_like) + .values(comment_like_form) + .get_result::(conn) + } + fn remove(conn: &PgConnection, comment_like_form: CommentLikeForm) -> usize { + use schema::comment_like::dsl::*; + diesel::delete(comment_like + .filter(comment_id.eq(comment_like_form.comment_id)) + .filter(fedi_user_id.eq(comment_like_form.fedi_user_id))) + .execute(conn) + .expect("Error deleting.") + } +} + +#[cfg(test)] +mod tests { + use establish_connection; + use super::*; + use actions::post::*; + use Crud; + #[test] + fn test_crud() { + let conn = establish_connection(); + + let new_post = PostForm { + name: "A test post".into(), + url: "https://test.com".into(), + attributed_to: "test_user.com".into(), + updated: None + }; + + let inserted_post = Post::create(&conn, new_post).unwrap(); + + let comment_form = CommentForm { + content: "A test comment".into(), + attributed_to: "test_user.com".into(), + post_id: &inserted_post.id, + parent_id: None, + updated: None + }; + + let inserted_comment = Comment::create(&conn, comment_form).unwrap(); + + let expected_comment = Comment { + id: inserted_comment.id, + content: "A test comment".into(), + attributed_to: "test_user.com".into(), + post_id: inserted_post.id, + parent_id: None, + published: inserted_comment.published, + updated: None + }; + + let child_comment_form = CommentForm { + content: "A child comment".into(), + attributed_to: "test_user.com".into(), + post_id: &inserted_post.id, + parent_id: Some(&inserted_comment.id), + updated: None + }; + + let inserted_child_comment = Comment::create(&conn, child_comment_form).unwrap(); + + let comment_like_form = CommentLikeForm { + comment_id: &inserted_comment.id, + fedi_user_id: "test".into(), + score: &1 + }; + + let inserted_comment_like = CommentLike::like(&conn, comment_like_form).unwrap(); + + let expected_comment_like = CommentLike { + id: inserted_comment_like.id, + comment_id: inserted_comment.id, + fedi_user_id: "test".into(), + published: inserted_comment_like.published, + score: 1 + }; + + let read_comment = Comment::read(&conn, inserted_comment.id); + let updated_comment = Comment::update(&conn, inserted_comment.id, comment_form); + let like_removed = CommentLike::remove(&conn, comment_like_form); + let num_deleted = Comment::delete(&conn, inserted_comment.id); + Comment::delete(&conn, inserted_child_comment.id); + Post::delete(&conn, inserted_post.id); + + assert_eq!(expected_comment, read_comment); + assert_eq!(expected_comment, inserted_comment); + assert_eq!(expected_comment, updated_comment); + assert_eq!(expected_comment_like, inserted_comment_like); + assert_eq!(expected_comment.id, inserted_child_comment.parent_id.unwrap()); + assert_eq!(1, like_removed); + assert_eq!(1, num_deleted); + + } +} diff --git a/server/src/actions/mod.rs b/server/src/actions/mod.rs index df10dbb..1222730 100644 --- a/server/src/actions/mod.rs +++ b/server/src/actions/mod.rs @@ -1,3 +1,4 @@ pub mod user; pub mod community; pub mod post; +pub mod comment; diff --git a/server/src/actions/post.rs b/server/src/actions/post.rs index e69de29..dd80f58 100644 --- a/server/src/actions/post.rs +++ b/server/src/actions/post.rs @@ -0,0 +1,150 @@ +extern crate diesel; +use schema::{post, post_like}; +use diesel::*; +use diesel::result::Error; +use {Crud, Likeable}; + +#[derive(Queryable, Identifiable, PartialEq, Debug)] +#[table_name="post"] +pub struct Post { + pub id: i32, + pub name: String, + pub url: String, + pub attributed_to: String, + pub published: chrono::NaiveDateTime, + pub updated: Option +} + +#[derive(Insertable, AsChangeset, Clone, Copy)] +#[table_name="post"] +pub struct PostForm<'a> { + pub name: &'a str, + pub url: &'a str, + pub attributed_to: &'a str, + pub updated: Option<&'a chrono::NaiveDateTime> +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] +#[belongs_to(Post)] +#[table_name = "post_like"] +pub struct PostLike { + pub id: i32, + pub post_id: i32, + pub fedi_user_id: String, + pub score: i16, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone, Copy)] +#[table_name="post_like"] +pub struct PostLikeForm<'a> { + pub post_id: &'a i32, + pub fedi_user_id: &'a str, + pub score: &'a i16 +} + +impl<'a> Crud> for Post { + fn read(conn: &PgConnection, post_id: i32) -> Post { + use schema::post::dsl::*; + post.find(post_id) + .first::(conn) + .expect("Error in query") + } + + fn delete(conn: &PgConnection, post_id: i32) -> usize { + use schema::post::dsl::*; + diesel::delete(post.find(post_id)) + .execute(conn) + .expect("Error deleting.") + } + + fn create(conn: &PgConnection, new_post: PostForm) -> Result { + use schema::post::dsl::*; + insert_into(post) + .values(new_post) + .get_result::(conn) + } + + fn update(conn: &PgConnection, post_id: i32, new_post: PostForm) -> Post { + use schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(new_post) + .get_result::(conn) + .expect(&format!("Unable to find {}", post_id)) + } +} + +impl<'a> Likeable > for PostLike { + fn like(conn: &PgConnection, post_like_form: PostLikeForm) -> Result { + use schema::post_like::dsl::*; + insert_into(post_like) + .values(post_like_form) + .get_result::(conn) + } + fn remove(conn: &PgConnection, post_like_form: PostLikeForm) -> usize { + use schema::post_like::dsl::*; + diesel::delete(post_like + .filter(post_id.eq(post_like_form.post_id)) + .filter(fedi_user_id.eq(post_like_form.fedi_user_id))) + .execute(conn) + .expect("Error deleting.") + } +} + +#[cfg(test)] +mod tests { + use establish_connection; + use super::*; + use Crud; + #[test] + fn test_crud() { + let conn = establish_connection(); + + let new_post = PostForm { + name: "A test post".into(), + url: "https://test.com".into(), + attributed_to: "test_user.com".into(), + updated: None + }; + + let inserted_post = Post::create(&conn, new_post).unwrap(); + + let expected_post = Post { + id: inserted_post.id, + name: "A test post".into(), + url: "https://test.com".into(), + attributed_to: "test_user.com".into(), + published: inserted_post.published, + updated: None + }; + + let post_like_form = PostLikeForm { + post_id: &inserted_post.id, + fedi_user_id: "test".into(), + score: &1 + }; + + let inserted_post_like = PostLike::like(&conn, post_like_form).unwrap(); + + let expected_post_like = PostLike { + id: inserted_post_like.id, + post_id: inserted_post.id, + fedi_user_id: "test".into(), + published: inserted_post_like.published, + score: 1 + }; + + let read_post = Post::read(&conn, inserted_post.id); + let updated_post = Post::update(&conn, inserted_post.id, new_post); + let like_removed = PostLike::remove(&conn, post_like_form); + let num_deleted = Post::delete(&conn, inserted_post.id); + + assert_eq!(expected_post, read_post); + assert_eq!(expected_post, inserted_post); + assert_eq!(expected_post, updated_post); + assert_eq!(expected_post_like, inserted_post_like); + assert_eq!(1, like_removed); + assert_eq!(1, num_deleted); + + } +} diff --git a/server/src/actions/src/schema.rs b/server/src/actions/src/schema.rs deleted file mode 100644 index 580b82e..0000000 --- a/server/src/actions/src/schema.rs +++ /dev/null @@ -1,80 +0,0 @@ -table! { - community (id) { - id -> Int4, - name -> Varchar, - start_time -> Timestamp, - } -} - -table! { - community_follower (id) { - id -> Int4, - fedi_user_id -> Text, - community_id -> Nullable, - start_time -> Timestamp, - } -} - -table! { - community_user (id) { - id -> Int4, - fedi_user_id -> Text, - community_id -> Nullable, - start_time -> Timestamp, - } -} - -table! { - post (id) { - id -> Int4, - name -> Varchar, - url -> Text, - attributed_to -> Text, - start_time -> Timestamp, - } -} - -table! { - post_dislike (id) { - id -> Int4, - fedi_user_id -> Text, - post_id -> Nullable, - start_time -> Timestamp, - } -} - -table! { - post_like (id) { - id -> Int4, - fedi_user_id -> Text, - post_id -> Nullable, - start_time -> Timestamp, - } -} - -table! { - user_ (id) { - id -> Int4, - name -> Varchar, - preferred_username -> Nullable, - password_encrypted -> Text, - email -> Nullable, - icon -> Nullable, - start_time -> Timestamp, - } -} - -joinable!(community_follower -> community (community_id)); -joinable!(community_user -> community (community_id)); -joinable!(post_dislike -> post (post_id)); -joinable!(post_like -> post (post_id)); - -allow_tables_to_appear_in_same_query!( - community, - community_follower, - community_user, - post, - post_dislike, - post_like, - user_, -); diff --git a/server/src/apub.rs b/server/src/apub.rs index 4cfd108..16b8be1 100644 --- a/server/src/apub.rs +++ b/server/src/apub.rs @@ -48,11 +48,9 @@ mod tests { }; let person = expected_user.person(); - - // let expected_person = Person { - // }; - assert_eq!("http://0.0.0.0/api/v1/user/thom", person.object_props.id_string().unwrap()); + let json = serde_json::to_string_pretty(&person).unwrap(); + println!("{}", json); } } diff --git a/server/src/lib.rs b/server/src/lib.rs index e789897..b1a1f25 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -31,6 +31,11 @@ pub trait Joinable { fn leave(conn: &PgConnection, form: T) -> usize; } +pub trait Likeable { + fn like(conn: &PgConnection, form: T) -> Result where Self: Sized; + fn remove(conn: &PgConnection, form: T) -> usize; +} + pub fn establish_connection() -> PgConnection { let db_url = Settings::get().db_url; PgConnection::establish(&db_url) diff --git a/server/src/schema.rs b/server/src/schema.rs index 75cbad5..4ab54bc 100644 --- a/server/src/schema.rs +++ b/server/src/schema.rs @@ -1,3 +1,25 @@ +table! { + comment (id) { + id -> Int4, + content -> Text, + attributed_to -> Text, + post_id -> Int4, + parent_id -> Nullable, + published -> Timestamp, + updated -> Nullable, + } +} + +table! { + comment_like (id) { + id -> Int4, + comment_id -> Int4, + fedi_user_id -> Text, + score -> Int2, + published -> Timestamp, + } +} + table! { community (id) { id -> Int4, @@ -36,20 +58,12 @@ table! { } } -table! { - post_dislike (id) { - id -> Int4, - fedi_user_id -> Text, - post_id -> Nullable, - published -> Timestamp, - } -} - table! { post_like (id) { id -> Int4, + post_id -> Int4, fedi_user_id -> Text, - post_id -> Nullable, + score -> Int2, published -> Timestamp, } } @@ -67,17 +81,19 @@ table! { } } +joinable!(comment -> post (post_id)); +joinable!(comment_like -> comment (comment_id)); joinable!(community_follower -> community (community_id)); joinable!(community_user -> community (community_id)); -joinable!(post_dislike -> post (post_id)); joinable!(post_like -> post (post_id)); allow_tables_to_appear_in_same_query!( + comment, + comment_like, community, community_follower, community_user, post, - post_dislike, post_like, user_, );