diff --git a/README.md b/README.md index e4a7d2f6e6a..e9be459db04 100644 --- a/README.md +++ b/README.md @@ -157,15 +157,15 @@ If you'd like to add translations, take a look a look at the [English translatio lang | done | missing --- | --- | --- -de | 93% | avatar,upload_avatar,show_avatars,docs,old_password,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -eo | 80% | number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,are_you_sure,yes,no,email_already_exists -es | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -fr | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -it | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -nl | 99% | donate_to_lemmy,donate,email_already_exists -ru | 77% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists -sv | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -zh | 75% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists +de | 88% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +eo | 76% | number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +es | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +fr | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +it | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +nl | 93% | create_private_message,send_secure_message,send_message,message,message_sent,messages,matrix_user_id,private_message_disclaimer,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +ru | 72% | cross_posts,cross_post,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +sv | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +zh | 70% | cross_posts,cross_post,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index d891697464c..c2df6223e6d 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -12,5 +12,5 @@ - [Contributing](contributing.md) - [Docker Development](contributing_docker_development.md) - [Local Development](contributing_local_development.md) - - [Websocket API](contributing_websocket_api.md) + - [Websocket/HTTP API](contributing_websocket_http_api.md) - [ActivityPub API Outline](contributing_apub_api_outline.md) diff --git a/docs/src/contributing_websocket_api.md b/docs/src/contributing_websocket_http_api.md similarity index 68% rename from docs/src/contributing_websocket_api.md rename to docs/src/contributing_websocket_http_api.md index 16383d532b4..9e87d4faa16 100644 --- a/docs/src/contributing_websocket_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -5,126 +5,171 @@ - [Data types](#data-types) - [Basic usage](#basic-usage) - * [WebSocket Endpoint](#websocket-endpoint) - * [Testing with Websocat](#testing-with-websocat) - * [Testing with the WebSocket JavaScript API](#testing-with-the-websocket-javascript-api) + * [WebSocket](#websocket) + + [Testing with Websocat](#testing-with-websocat) + + [Testing with the WebSocket JavaScript API](#testing-with-the-websocket-javascript-api) + * [HTTP](#http) + + [Testing with Curl](#testing-with-curl) + - [Get Example](#get-example) + - [Post Example](#post-example) - [Rate limits](#rate-limits) - [Errors](#errors) - [API documentation](#api-documentation) * [Sort Types](#sort-types) + * [Websocket vs HTTP](#websocket-vs-http) * [User / Authentication / Admin actions](#user--authentication--admin-actions) + [Login](#login) - [Request](#request) - [Response](#response) + - [HTTP](#http-1) + [Register](#register) - [Request](#request-1) - [Response](#response-1) + - [HTTP](#http-2) + [Get User Details](#get-user-details) - [Request](#request-2) - [Response](#response-2) + - [HTTP](#http-3) + [Save User Settings](#save-user-settings) - [Request](#request-3) - [Response](#response-3) + - [HTTP](#http-4) + [Get Replies / Inbox](#get-replies--inbox) - [Request](#request-4) - [Response](#response-4) + - [HTTP](#http-5) + [Get User Mentions](#get-user-mentions) - [Request](#request-5) - [Response](#response-5) - + [Mark All As Read](#mark-all-as-read) + - [HTTP](#http-6) + + [Edit User Mention](#edit-user-mention) - [Request](#request-6) - [Response](#response-6) - + [Delete Account](#delete-account) + - [HTTP](#http-7) + + [Mark All As Read](#mark-all-as-read) - [Request](#request-7) - [Response](#response-7) - + [Add admin](#add-admin) + - [HTTP](#http-8) + + [Delete Account](#delete-account) - [Request](#request-8) - [Response](#response-8) - + [Ban user](#ban-user) + - [HTTP](#http-9) + + [Add admin](#add-admin) - [Request](#request-9) - [Response](#response-9) - * [Site](#site) - + [List Categories](#list-categories) + - [HTTP](#http-10) + + [Ban user](#ban-user) - [Request](#request-10) - [Response](#response-10) - + [Search](#search) + - [HTTP](#http-11) + * [Site](#site) + + [List Categories](#list-categories) - [Request](#request-11) - [Response](#response-11) - + [Get Modlog](#get-modlog) + - [HTTP](#http-12) + + [Search](#search) - [Request](#request-12) - [Response](#response-12) - + [Create Site](#create-site) + - [HTTP](#http-13) + + [Get Modlog](#get-modlog) - [Request](#request-13) - [Response](#response-13) - + [Edit Site](#edit-site) + - [HTTP](#http-14) + + [Create Site](#create-site) - [Request](#request-14) - [Response](#response-14) - + [Get Site](#get-site) + - [HTTP](#http-15) + + [Edit Site](#edit-site) - [Request](#request-15) - [Response](#response-15) - + [Transfer Site](#transfer-site) + - [HTTP](#http-16) + + [Get Site](#get-site) - [Request](#request-16) - [Response](#response-16) - * [Community](#community) - + [Get Community](#get-community) + - [HTTP](#http-17) + + [Transfer Site](#transfer-site) - [Request](#request-17) - [Response](#response-17) - + [Create Community](#create-community) + - [HTTP](#http-18) + * [Community](#community) + + [Get Community](#get-community) - [Request](#request-18) - [Response](#response-18) - + [List Communities](#list-communities) + - [HTTP](#http-19) + + [Create Community](#create-community) - [Request](#request-19) - [Response](#response-19) - + [Ban from Community](#ban-from-community) + - [HTTP](#http-20) + + [List Communities](#list-communities) - [Request](#request-20) - [Response](#response-20) - + [Add Mod to Community](#add-mod-to-community) + - [HTTP](#http-21) + + [Ban from Community](#ban-from-community) - [Request](#request-21) - [Response](#response-21) - + [Edit Community](#edit-community) + - [HTTP](#http-22) + + [Add Mod to Community](#add-mod-to-community) - [Request](#request-22) - [Response](#response-22) - + [Follow Community](#follow-community) + - [HTTP](#http-23) + + [Edit Community](#edit-community) - [Request](#request-23) - [Response](#response-23) - + [Get Followed Communities](#get-followed-communities) + - [HTTP](#http-24) + + [Follow Community](#follow-community) - [Request](#request-24) - [Response](#response-24) - + [Transfer Community](#transfer-community) + - [HTTP](#http-25) + + [Get Followed Communities](#get-followed-communities) - [Request](#request-25) - [Response](#response-25) - * [Post](#post) - + [Create Post](#create-post) + - [HTTP](#http-26) + + [Transfer Community](#transfer-community) - [Request](#request-26) - [Response](#response-26) - + [Get Post](#get-post) + - [HTTP](#http-27) + * [Post](#post) + + [Create Post](#create-post) - [Request](#request-27) - [Response](#response-27) - + [Get Posts](#get-posts) + - [HTTP](#http-28) + + [Get Post](#get-post) - [Request](#request-28) - [Response](#response-28) - + [Create Post Like](#create-post-like) + - [HTTP](#http-29) + + [Get Posts](#get-posts) - [Request](#request-29) - [Response](#response-29) - + [Edit Post](#edit-post) + - [HTTP](#http-30) + + [Create Post Like](#create-post-like) - [Request](#request-30) - [Response](#response-30) - + [Save Post](#save-post) + - [HTTP](#http-31) + + [Edit Post](#edit-post) - [Request](#request-31) - [Response](#response-31) - * [Comment](#comment) - + [Create Comment](#create-comment) + - [HTTP](#http-32) + + [Save Post](#save-post) - [Request](#request-32) - [Response](#response-32) - + [Edit Comment](#edit-comment) + - [HTTP](#http-33) + * [Comment](#comment) + + [Create Comment](#create-comment) - [Request](#request-33) - [Response](#response-33) - + [Save Comment](#save-comment) + - [HTTP](#http-34) + + [Edit Comment](#edit-comment) - [Request](#request-34) - [Response](#response-34) - + [Create Comment Like](#create-comment-like) + - [HTTP](#http-35) + + [Save Comment](#save-comment) - [Request](#request-35) - [Response](#response-35) + - [HTTP](#http-36) + + [Create Comment Like](#create-comment-like) + - [Request](#request-36) + - [Response](#response-36) + - [HTTP](#http-37) * [RSS / Atom feeds](#rss--atom-feeds) + [All](#all) + [Community](#community-1) @@ -144,13 +189,13 @@ Request and response strings are in [JSON format](https://www.json.org). -### WebSocket Endpoint +### WebSocket Connect to ws://***host***/api/v1/ws to get started. If the ***`host`*** supports secure connections, you can use wss://***host***/api/v1/ws. -### Testing with Websocat +#### Testing with Websocat [Websocat link](https://github.com/vi/websocat) @@ -159,7 +204,7 @@ If the ***`host`*** supports secure connections, you can use wss://***host A simple test command: `{"op": "ListCategories"}` -### Testing with the WebSocket JavaScript API +#### Testing with the WebSocket JavaScript API [WebSocket JavaScript API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) ```javascript @@ -171,6 +216,32 @@ ws.onopen = function () { })); }; ``` +### HTTP + +Endpoints are at http://***host***/api/v1/***endpoint***. They'll be listed below for each action. + +#### Testing with Curl + +##### Get Example + +``` +curl /community/list?sort=Hot +``` + +##### Post Example + +``` +curl -i -H \ +"Content-Type: application/json" \ +-X POST \ +-d '{ + "comment_id": X, + "post_id": X, + "score": X, + "auth": "..." +}' \ +/comment/like +``` ## Rate limits @@ -201,6 +272,11 @@ These go wherever there is a `sort` field. The available sort types are: - `TopYear` - the most upvoted posts/communities of the current year. - `TopAll` - the most upvoted posts/communities on the current instance. +### Websocket vs HTTP + +- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`. +- For example, an http login will be a `POST` `{username_or_email: X, password: X}` + ### User / Authentication / Admin actions #### Login @@ -220,13 +296,19 @@ The `jwt` string should be stored and used anywhere `auth` is called for. ##### Response ```rust { - op: String, - jwt: String + op: "Login", + data: { + jwt: String, + } } ``` +##### HTTP + +`POST /user/login` #### Register + Only the first user will be able to be the admin. ##### Request @@ -245,11 +327,17 @@ Only the first user will be able to be the admin. ##### Response ```rust { - op: String, - jwt: String + op: "Register", + data: { + jwt: String, + } } ``` +##### HTTP + +`POST /user/register` + #### Get User Details ##### Request ```rust @@ -270,14 +358,20 @@ Only the first user will be able to be the admin. ##### Response ```rust { - op: String, - user: UserView, - follows: Vec, - moderates: Vec, - comments: Vec, - posts: Vec, + op: "GetUserDetails", + data: { + user: UserView, + follows: Vec, + moderates: Vec, + comments: Vec, + posts: Vec, + } } ``` +##### HTTP + +`GET /user` + #### Save User Settings ##### Request ```rust @@ -295,10 +389,16 @@ Only the first user will be able to be the admin. ##### Response ```rust { - op: String, - jwt: String + op: "SaveUserSettings", + data: { + jwt: String + } } ``` +##### HTTP + +`PUT /save_user_settings` + #### Get Replies / Inbox ##### Request ```rust @@ -316,10 +416,16 @@ Only the first user will be able to be the admin. ##### Response ```rust { - op: String, - replies: Vec, + op: "GetReplies", + data: { + replies: Vec, + } } ``` +##### HTTP + +`GET /user/replies` + #### Get User Mentions ##### Request @@ -338,11 +444,42 @@ Only the first user will be able to be the admin. ##### Response ```rust { - op: String, - mentions: Vec, + op: "GetUserMentions", + data: { + mentions: Vec, + } } ``` +##### HTTP + +`GET /user/mentions` + +#### Edit User Mention +##### Request +```rust +{ + op: "EditUserMention", + data: { + user_mention_id: i32, + read: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "EditUserMention", + data: { + mention: UserMentionView, + } +} +``` +##### HTTP + +`PUT /user/mention` + #### Mark All As Read Marks all user replies and mentions as read. @@ -359,11 +496,17 @@ Marks all user replies and mentions as read. ##### Response ```rust { - op: String, - replies: Vec, + op: "MarkAllAsRead", + data: { + replies: Vec, + } } ``` +##### HTTP + +`POST /user/mark_all_as_read` + #### Delete Account *Permananently deletes your posts and comments* @@ -381,11 +524,17 @@ Marks all user replies and mentions as read. ##### Response ```rust { - op: String, - jwt: String, + op: "DeleteAccount", + data: { + jwt: String, + } } ``` +##### HTTP + +`POST /user/delete_account` + #### Add admin ##### Request ```rust @@ -401,10 +550,15 @@ Marks all user replies and mentions as read. ##### Response ```rust { - op: String, - admins: Vec, + op: "AddAdmin", + data: { + admins: Vec, + } } ``` +##### HTTP + +`POST /admin/add` #### Ban user ##### Request @@ -423,11 +577,16 @@ Marks all user replies and mentions as read. ##### Response ```rust { - op: String, - user: UserView, - banned: bool, + op: "BanUser", + data: { + user: UserView, + banned: bool, + } } ``` +##### HTTP + +`POST /user/ban` ### Site #### List Categories @@ -440,13 +599,19 @@ Marks all user replies and mentions as read. ##### Response ```rust { - op: String, - categories: Vec + op: "ListCategories", + data: { + categories: Vec + } } ``` +##### HTTP + +`GET /categories` #### Search -Search types are `Both, Comments, Posts`. + +Search types are `All, Comments, Posts, Communities, Users, Url` ##### Request ```rust @@ -459,17 +624,26 @@ Search types are `Both, Comments, Posts`. sort: String, page: Option, limit: Option, + auth?: Option, } } ``` ##### Response ```rust { - op: String, - comments: Vec, - posts: Vec, + op: "Search", + data: { + type_: String, + comments: Vec, + posts: Vec, + communities: Vec, + users: Vec, + } } ``` +##### HTTP + +`POST /search` #### Get Modlog ##### Request @@ -487,18 +661,24 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - removed_posts: Vec, - locked_posts: Vec, - removed_comments: Vec, - removed_communities: Vec, - banned_from_community: Vec, - banned: Vec, - added_to_community: Vec, - added: Vec, + op: "GetModlog", + data: { + removed_posts: Vec, + locked_posts: Vec, + removed_comments: Vec, + removed_communities: Vec, + banned_from_community: Vec, + banned: Vec, + added_to_community: Vec, + added: Vec, + } } ``` +##### HTTP + +`GET /modlog` + #### Create Site ##### Request ```rust @@ -514,11 +694,17 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - site: SiteView, + op: "CreateSite", + data: { + site: SiteView, + } } ``` +##### HTTP + +`POST /site` + #### Edit Site ##### Request ```rust @@ -534,10 +720,15 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - site: SiteView, + op: "EditSite", + data: { + site: SiteView, + } } ``` +##### HTTP + +`PUT /site` #### Get Site ##### Request @@ -549,12 +740,17 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - site: Option, - admins: Vec, - banned: Vec, + op: "GetSite", + data: { + site: Option, + admins: Vec, + banned: Vec, + } } ``` +##### HTTP + +`GET /site` #### Transfer Site ##### Request @@ -570,12 +766,17 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - site: Option, - admins: Vec, - banned: Vec, + op: "TransferSite", + data: { + site: Option, + admins: Vec, + banned: Vec, + } } ``` +##### HTTP + +`POST /site/transfer` ### Community #### Get Community @@ -593,12 +794,17 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - community: CommunityView, - moderators: Vec, - admins: Vec, + op: "GetCommunity", + data: { + community: CommunityView, + moderators: Vec, + admins: Vec, + } } ``` +##### HTTP + +`GET /community` #### Create Community ##### Request @@ -617,10 +823,15 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - community: CommunityView + op: "CreateCommunity", + data: { + community: CommunityView + } } ``` +##### HTTP + +`POST /community` #### List Communities ##### Request @@ -638,10 +849,15 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - communities: Vec + op: "ListCommunities", + data: { + communities: Vec + } } ``` +##### HTTP + +`GET /community/list` #### Ban from Community ##### Request @@ -661,11 +877,16 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - user: UserView, - banned: bool, + op: "BanFromCommunity", + data: { + user: UserView, + banned: bool, + } } ``` +##### HTTP + +`POST /community/ban_user` #### Add Mod to Community ##### Request @@ -683,10 +904,15 @@ Search types are `Both, Comments, Posts`. ##### Response ```rust { - op: String, - moderators: Vec, + op: "AddModToCommunity", + data: { + moderators: Vec, + } } ``` +##### HTTP + +`POST /community/mod` #### Edit Community Mods and admins can remove and lock a community, creators can delete it. @@ -712,10 +938,15 @@ Mods and admins can remove and lock a community, creators can delete it. ##### Response ```rust { - op: String, - community: CommunityView + op: "EditCommunity", + data: { + community: CommunityView + } } ``` +##### HTTP + +`PUT /community` #### Follow Community ##### Request @@ -732,10 +963,15 @@ Mods and admins can remove and lock a community, creators can delete it. ##### Response ```rust { - op: String, - community: CommunityView + op: "FollowCommunity", + data: { + community: CommunityView + } } ``` +##### HTTP + +`POST /community/follow` #### Get Followed Communities ##### Request @@ -750,10 +986,15 @@ Mods and admins can remove and lock a community, creators can delete it. ##### Response ```rust { - op: String, - communities: Vec + op: "GetFollowedCommunities", + data: { + communities: Vec + } } ``` +##### HTTP + +`GET /user/followed_communities` #### Transfer Community ##### Request @@ -770,12 +1011,17 @@ Mods and admins can remove and lock a community, creators can delete it. ##### Response ```rust { - op: String, - community: CommunityView, - moderators: Vec, - admins: Vec, + op: "TransferCommunity", + data: { + community: CommunityView, + moderators: Vec, + admins: Vec, + } } ``` +##### HTTP + +`POST /community/transfer` ### Post #### Create Post @@ -795,10 +1041,15 @@ Mods and admins can remove and lock a community, creators can delete it. ##### Response ```rust { - op: String, - post: PostView + op: "CreatePost", + data: { + post: PostView + } } ``` +##### HTTP + +`POST /post` #### Get Post ##### Request @@ -814,16 +1065,22 @@ Mods and admins can remove and lock a community, creators can delete it. ##### Response ```rust { - op: String, - post: PostView, - comments: Vec, - community: CommunityView, - moderators: Vec, - admins: Vec, + op: "GetPost", + data: { + post: PostView, + comments: Vec, + community: CommunityView, + moderators: Vec, + admins: Vec, + } } ``` +##### HTTP + +`GET /post` #### Get Posts + Post listing types are `All, Subscribed, Community` ##### Request @@ -843,12 +1100,18 @@ Post listing types are `All, Subscribed, Community` ##### Response ```rust { - op: String, - posts: Vec, + op: "GetPosts", + data: { + posts: Vec, + } } ``` +##### HTTP + +`GET /post/list` #### Create Post Like + `score` can be 0, -1, or 1 ##### Request @@ -865,12 +1128,18 @@ Post listing types are `All, Subscribed, Community` ##### Response ```rust { - op: String, - post: PostView + op: "CreatePostLike", + data: { + post: PostView + } } ``` +##### HTTP + +`POST /post/like` #### Edit Post + Mods and admins can remove and lock a post, creators can delete it. ##### Request @@ -895,11 +1164,17 @@ Mods and admins can remove and lock a post, creators can delete it. ##### Response ```rust { - op: String, - post: PostView + op: "EditPost", + data: { + post: PostView + } } ``` +##### HTTP + +`PUT /post` + #### Save Post ##### Request ```rust @@ -915,10 +1190,15 @@ Mods and admins can remove and lock a post, creators can delete it. ##### Response ```rust { - op: String, - post: PostView + op: "SavePost", + data: { + post: PostView + } } ``` +##### HTTP + +`POST /post/save` ### Comment #### Create Comment @@ -938,12 +1218,19 @@ Mods and admins can remove and lock a post, creators can delete it. ##### Response ```rust { - op: String, - comment: CommentView + op: "CreateComment", + data: { + comment: CommentView + } } ``` +##### HTTP + +`POST /comment` + #### Edit Comment + Mods and admins can remove a comment, creators can delete it. ##### Request @@ -967,10 +1254,15 @@ Mods and admins can remove a comment, creators can delete it. ##### Response ```rust { - op: String, - comment: CommentView + op: "EditComment", + data: { + comment: CommentView + } } ``` +##### HTTP + +`PUT /comment` #### Save Comment ##### Request @@ -987,12 +1279,18 @@ Mods and admins can remove a comment, creators can delete it. ##### Response ```rust { - op: String, - comment: CommentView + op: "SaveComment", + data: { + comment: CommentView + } } ``` +##### HTTP + +`POST /comment/save` #### Create Comment Like + `score` can be 0, -1, or 1 ##### Request @@ -1010,10 +1308,15 @@ Mods and admins can remove a comment, creators can delete it. ##### Response ```rust { - op: String, - comment: CommentView + op: "CreateCommentLike", + data: { + comment: CommentView + } } ``` +##### HTTP + +`POST /comment/like` ### RSS / Atom feeds diff --git a/server/.rustfmt.toml b/server/.rustfmt.toml index b1fce9c9a57..684a7f8a27a 100644 --- a/server/.rustfmt.toml +++ b/server/.rustfmt.toml @@ -1,2 +1,2 @@ tab_spaces = 2 -edition="2018" +edition="2018" \ No newline at end of file diff --git a/server/migrations/2020-01-21-001001_create_private_message/down.sql b/server/migrations/2020-01-21-001001_create_private_message/down.sql new file mode 100644 index 00000000000..0d951e3eafa --- /dev/null +++ b/server/migrations/2020-01-21-001001_create_private_message/down.sql @@ -0,0 +1,34 @@ +-- Drop the triggers +drop trigger refresh_private_message on private_message; +drop function refresh_private_message(); + +-- Drop the view and table +drop view private_message_view cascade; +drop table private_message; + +-- Rebuild the old views +drop view user_view cascade; +create view user_view as +select +u.id, +u.name, +u.avatar, +u.email, +u.fedi_name, +u.admin, +u.banned, +u.show_avatars, +u.send_notifications_to_email, +u.published, +(select count(*) from post p where p.creator_id = u.id) as number_of_posts, +(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score, +(select count(*) from comment c where c.creator_id = u.id) as number_of_comments, +(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score +from user_ u; + +create materialized view user_mview as select * from user_view; + +create unique index idx_user_mview_id on user_mview (id); + +-- Drop the columns +alter table user_ drop column matrix_user_id; diff --git a/server/migrations/2020-01-21-001001_create_private_message/up.sql b/server/migrations/2020-01-21-001001_create_private_message/up.sql new file mode 100644 index 00000000000..48e16dd83d6 --- /dev/null +++ b/server/migrations/2020-01-21-001001_create_private_message/up.sql @@ -0,0 +1,90 @@ +-- Creating private message +create table private_message ( + id serial primary key, + creator_id int references user_ on update cascade on delete cascade not null, + recipient_id int references user_ on update cascade on delete cascade not null, + content text not null, + deleted boolean default false not null, + read boolean default false not null, + published timestamp not null default now(), + updated timestamp +); + +-- Create the view and materialized view which has the avatar and creator name +create view private_message_view as +select +pm.*, +u.name as creator_name, +u.avatar as creator_avatar, +u2.name as recipient_name, +u2.avatar as recipient_avatar +from private_message pm +inner join user_ u on u.id = pm.creator_id +inner join user_ u2 on u2.id = pm.recipient_id; + +create materialized view private_message_mview as select * from private_message_view; + +create unique index idx_private_message_mview_id on private_message_mview (id); + +-- Create the triggers +create or replace function refresh_private_message() +returns trigger language plpgsql +as $$ +begin + refresh materialized view concurrently private_message_mview; + return null; +end $$; + +create trigger refresh_private_message +after insert or update or delete or truncate +on private_message +for each statement +execute procedure refresh_private_message(); + +-- Update user to include matrix id +alter table user_ add column matrix_user_id text unique; + +drop view user_view cascade; +create view user_view as +select +u.id, +u.name, +u.avatar, +u.email, +u.matrix_user_id, +u.fedi_name, +u.admin, +u.banned, +u.show_avatars, +u.send_notifications_to_email, +u.published, +(select count(*) from post p where p.creator_id = u.id) as number_of_posts, +(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score, +(select count(*) from comment c where c.creator_id = u.id) as number_of_comments, +(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score +from user_ u; + +create materialized view user_mview as select * from user_view; + +create unique index idx_user_mview_id on user_mview (id); + +-- This is what a group pm table would look like +-- Not going to do it now because of the complications +-- +-- create table private_message ( +-- id serial primary key, +-- creator_id int references user_ on update cascade on delete cascade not null, +-- content text not null, +-- deleted boolean default false not null, +-- published timestamp not null default now(), +-- updated timestamp +-- ); +-- +-- create table private_message_recipient ( +-- id serial primary key, +-- private_message_id int references private_message on update cascade on delete cascade not null, +-- recipient_id int references user_ on update cascade on delete cascade not null, +-- read boolean default false not null, +-- published timestamp not null default now(), +-- unique(private_message_id, recipient_id) +-- ) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 61cc9506334..8efb30fbde4 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -7,7 +7,7 @@ use diesel::PgConnection; pub struct CreateComment { content: String, parent_id: Option, - edit_id: Option, + edit_id: Option, // TODO this isn't used pub post_id: i32, auth: String, } @@ -15,7 +15,7 @@ pub struct CreateComment { #[derive(Serialize, Deserialize)] pub struct EditComment { content: String, - parent_id: Option, + parent_id: Option, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change edit_id: i32, creator_id: i32, pub post_id: i32, @@ -35,7 +35,6 @@ pub struct SaveComment { #[derive(Serialize, Deserialize, Clone)] pub struct CommentResponse { - op: String, pub comment: CommentView, } @@ -53,7 +52,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -63,12 +62,12 @@ impl Perform for Oper { // Check for a community ban let post = Post::read(&conn, data.post_id)?; if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban").into()); + return Err(APIError::err("community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } let content_slurs_removed = remove_slurs(&data.content.to_owned()); @@ -86,7 +85,7 @@ impl Perform for Oper { let inserted_comment = match Comment::create(&conn, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), }; // Scan the comment for user mentions, add those rows @@ -193,13 +192,12 @@ impl Perform for Oper { let _inserted_like = match CommentLike::like(&conn, &like_form) { Ok(like) => like, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), }; let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?; Ok(CommentResponse { - op: self.op.to_string(), comment: comment_view, }) } @@ -211,7 +209,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -231,17 +229,17 @@ impl Perform for Oper { editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect()); if !editors.contains(&user_id) { - return Err(APIError::err(&self.op, "no_comment_edit_allowed").into()); + return Err(APIError::err("no_comment_edit_allowed").into()); } // Check for a community ban if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban").into()); + return Err(APIError::err("community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } } @@ -264,7 +262,7 @@ impl Perform for Oper { let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; // Scan the comment for user mentions, add those rows @@ -310,7 +308,6 @@ impl Perform for Oper { let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?; Ok(CommentResponse { - op: self.op.to_string(), comment: comment_view, }) } @@ -322,7 +319,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -335,19 +332,18 @@ impl Perform for Oper { if data.save { match CommentSaved::save(&conn, &comment_saved_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), }; } else { match CommentSaved::unsave(&conn, &comment_saved_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), }; } let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?; Ok(CommentResponse { - op: self.op.to_string(), comment: comment_view, }) } @@ -359,7 +355,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -368,19 +364,19 @@ impl Perform for Oper { if data.score == -1 { let site = SiteView::read(&conn)?; if !site.enable_downvotes { - return Err(APIError::err(&self.op, "downvotes_disabled").into()); + return Err(APIError::err("downvotes_disabled").into()); } } // Check for a community ban let post = Post::read(&conn, data.post_id)?; if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban").into()); + return Err(APIError::err("community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } let like_form = CommentLikeForm { @@ -398,7 +394,7 @@ impl Perform for Oper { if do_add { let _inserted_like = match CommentLike::like(&conn, &like_form) { Ok(like) => like, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), }; } @@ -406,7 +402,6 @@ impl Perform for Oper { let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?; Ok(CommentResponse { - op: self.op.to_string(), comment: liked_comment, }) } diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 0bf846c3d9d..c765aa9dd84 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -11,7 +11,6 @@ pub struct GetCommunity { #[derive(Serialize, Deserialize)] pub struct GetCommunityResponse { - op: String, community: CommunityView, moderators: Vec, admins: Vec, @@ -29,7 +28,6 @@ pub struct CreateCommunity { #[derive(Serialize, Deserialize, Clone)] pub struct CommunityResponse { - op: String, pub community: CommunityView, } @@ -43,7 +41,6 @@ pub struct ListCommunities { #[derive(Serialize, Deserialize)] pub struct ListCommunitiesResponse { - op: String, communities: Vec, } @@ -59,7 +56,6 @@ pub struct BanFromCommunity { #[derive(Serialize, Deserialize)] pub struct BanFromCommunityResponse { - op: String, user: UserView, banned: bool, } @@ -74,7 +70,6 @@ pub struct AddModToCommunity { #[derive(Serialize, Deserialize)] pub struct AddModToCommunityResponse { - op: String, moderators: Vec, } @@ -107,7 +102,6 @@ pub struct GetFollowedCommunities { #[derive(Serialize, Deserialize)] pub struct GetFollowedCommunitiesResponse { - op: String, communities: Vec, } @@ -141,19 +135,19 @@ impl Perform for Oper { data.name.to_owned().unwrap_or_else(|| "main".to_string()), ) { Ok(community) => community.id, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()), + Err(_e) => return Err(APIError::err("couldnt_find_community").into()), } } }; let community_view = match CommunityView::read(&conn, community_id, user_id) { Ok(community) => community, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()), + Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; let moderators = match CommunityModeratorView::for_community(&conn, community_id) { Ok(moderators) => moderators, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()), + Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; let site_creator_id = Site::read(&conn, 1)?.creator_id; @@ -164,7 +158,6 @@ impl Perform for Oper { // Return the jwt Ok(GetCommunityResponse { - op: self.op.to_string(), community: community_view, moderators, admins, @@ -178,21 +171,21 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; if has_slurs(&data.name) || has_slurs(&data.title) || (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) { - return Err(APIError::err(&self.op, "no_slurs").into()); + return Err(APIError::err("no_slurs").into()); } let user_id = claims.id; // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } // When you create a community, make sure the user becomes a moderator and a follower @@ -210,7 +203,7 @@ impl Perform for Oper { let inserted_community = match Community::create(&conn, &community_form) { Ok(community) => community, - Err(_e) => return Err(APIError::err(&self.op, "community_already_exists").into()), + Err(_e) => return Err(APIError::err("community_already_exists").into()), }; let community_moderator_form = CommunityModeratorForm { @@ -221,9 +214,7 @@ impl Perform for Oper { let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { Ok(user) => user, - Err(_e) => { - return Err(APIError::err(&self.op, "community_moderator_already_exists").into()) - } + Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }; let community_follower_form = CommunityFollowerForm { @@ -234,13 +225,12 @@ impl Perform for Oper { let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()), + Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), }; let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?; Ok(CommunityResponse { - op: self.op.to_string(), community: community_view, }) } @@ -251,19 +241,19 @@ impl Perform for Oper { let data: &EditCommunity = &self.data; if has_slurs(&data.name) || has_slurs(&data.title) { - return Err(APIError::err(&self.op, "no_slurs").into()); + return Err(APIError::err("no_slurs").into()); } let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } // Verify its a mod @@ -276,7 +266,7 @@ impl Perform for Oper { ); editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect()); if !editors.contains(&user_id) { - return Err(APIError::err(&self.op, "no_community_edit_allowed").into()); + return Err(APIError::err("no_community_edit_allowed").into()); } let community_form = CommunityForm { @@ -293,7 +283,7 @@ impl Perform for Oper { let _updated_community = match Community::update(&conn, data.edit_id, &community_form) { Ok(community) => community, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()), + Err(_e) => return Err(APIError::err("couldnt_update_community").into()), }; // Mod tables @@ -315,7 +305,6 @@ impl Perform for Oper { let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?; Ok(CommunityResponse { - op: self.op.to_string(), community: community_view, }) } @@ -354,10 +343,7 @@ impl Perform for Oper { .list()?; // Return the jwt - Ok(ListCommunitiesResponse { - op: self.op.to_string(), - communities, - }) + Ok(ListCommunitiesResponse { communities }) } } @@ -367,7 +353,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -380,19 +366,18 @@ impl Perform for Oper { if data.follow { match CommunityFollower::follow(&conn, &community_follower_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()), + Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), }; } else { match CommunityFollower::ignore(&conn, &community_follower_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()), + Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), }; } let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?; Ok(CommunityResponse { - op: self.op.to_string(), community: community_view, }) } @@ -404,7 +389,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -412,14 +397,11 @@ impl Perform for Oper { let communities: Vec = match CommunityFollowerView::for_user(&conn, user_id) { Ok(communities) => communities, - Err(_e) => return Err(APIError::err(&self.op, "system_err_login").into()), + Err(_e) => return Err(APIError::err("system_err_login").into()), }; // Return the jwt - Ok(GetFollowedCommunitiesResponse { - op: self.op.to_string(), - communities, - }) + Ok(GetFollowedCommunitiesResponse { communities }) } } @@ -429,7 +411,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -442,12 +424,12 @@ impl Perform for Oper { if data.ban { match CommunityUserBan::ban(&conn, &community_user_ban_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()), + Err(_e) => return Err(APIError::err("community_user_already_banned").into()), }; } else { match CommunityUserBan::unban(&conn, &community_user_ban_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()), + Err(_e) => return Err(APIError::err("community_user_already_banned").into()), }; } @@ -470,7 +452,6 @@ impl Perform for Oper { let user_view = UserView::read(&conn, data.user_id)?; Ok(BanFromCommunityResponse { - op: self.op.to_string(), user: user_view, banned: data.ban, }) @@ -483,7 +464,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -496,16 +477,12 @@ impl Perform for Oper { if data.added { match CommunityModerator::join(&conn, &community_moderator_form) { Ok(user) => user, - Err(_e) => { - return Err(APIError::err(&self.op, "community_moderator_already_exists").into()) - } + Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }; } else { match CommunityModerator::leave(&conn, &community_moderator_form) { Ok(user) => user, - Err(_e) => { - return Err(APIError::err(&self.op, "community_moderator_already_exists").into()) - } + Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }; } @@ -520,10 +497,7 @@ impl Perform for Oper { let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?; - Ok(AddModToCommunityResponse { - op: self.op.to_string(), - moderators, - }) + Ok(AddModToCommunityResponse { moderators }) } } @@ -533,7 +507,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -548,7 +522,7 @@ impl Perform for Oper { // Make sure user is the creator, or an admin if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) { - return Err(APIError::err(&self.op, "not_an_admin").into()); + return Err(APIError::err("not_an_admin").into()); } let community_form = CommunityForm { @@ -565,7 +539,7 @@ impl Perform for Oper { let _updated_community = match Community::update(&conn, data.community_id, &community_form) { Ok(community) => community, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()), + Err(_e) => return Err(APIError::err("couldnt_update_community").into()), }; // You also have to re-do the community_moderator table, reordering it. @@ -588,9 +562,7 @@ impl Perform for Oper { let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { Ok(user) => user, - Err(_e) => { - return Err(APIError::err(&self.op, "community_moderator_already_exists").into()) - } + Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }; } @@ -605,17 +577,16 @@ impl Perform for Oper { let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) { Ok(community) => community, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()), + Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) { Ok(moderators) => moderators, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()), + Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; // Return the jwt Ok(GetCommunityResponse { - op: self.op.to_string(), community: community_view, moderators, admins, diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index e3580447685..cb09d7fa034 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -8,6 +8,8 @@ use crate::db::moderator_views::*; use crate::db::password_reset_request::*; use crate::db::post::*; use crate::db::post_view::*; +use crate::db::private_message::*; +use crate::db::private_message_view::*; use crate::db::site::*; use crate::db::site_view::*; use crate::db::user::*; @@ -26,73 +28,27 @@ pub mod post; pub mod site; pub mod user; -#[derive(EnumString, ToString, Debug)] -pub enum UserOperation { - Login, - Register, - CreateCommunity, - CreatePost, - ListCommunities, - ListCategories, - GetPost, - GetCommunity, - CreateComment, - EditComment, - SaveComment, - CreateCommentLike, - GetPosts, - CreatePostLike, - EditPost, - SavePost, - EditCommunity, - FollowCommunity, - GetFollowedCommunities, - GetUserDetails, - GetReplies, - GetUserMentions, - EditUserMention, - GetModlog, - BanFromCommunity, - AddModToCommunity, - CreateSite, - EditSite, - GetSite, - AddAdmin, - BanUser, - Search, - MarkAllAsRead, - SaveUserSettings, - TransferCommunity, - TransferSite, - DeleteAccount, - PasswordReset, - PasswordChange, -} - #[derive(Fail, Debug)] -#[fail(display = "{{\"op\":\"{}\", \"error\":\"{}\"}}", op, message)] +#[fail(display = "{{\"error\":\"{}\"}}", message)] pub struct APIError { - pub op: String, pub message: String, } impl APIError { - pub fn err(op: &UserOperation, msg: &str) -> Self { + pub fn err(msg: &str) -> Self { APIError { - op: op.to_string(), message: msg.to_string(), } } } pub struct Oper { - op: UserOperation, data: T, } impl Oper { - pub fn new(op: UserOperation, data: T) -> Oper { - Oper { op, data } + pub fn new(data: T) -> Oper { + Oper { data } } } diff --git a/server/src/api/post.rs b/server/src/api/post.rs index b0fcdd0c1b9..3f211453c9a 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -14,7 +14,6 @@ pub struct CreatePost { #[derive(Serialize, Deserialize, Clone)] pub struct PostResponse { - op: String, pub post: PostView, } @@ -26,7 +25,6 @@ pub struct GetPost { #[derive(Serialize, Deserialize)] pub struct GetPostResponse { - op: String, post: PostView, comments: Vec, community: CommunityView, @@ -46,7 +44,6 @@ pub struct GetPosts { #[derive(Serialize, Deserialize)] pub struct GetPostsResponse { - op: String, posts: Vec, } @@ -59,7 +56,6 @@ pub struct CreatePostLike { #[derive(Serialize, Deserialize)] pub struct CreatePostLikeResponse { - op: String, post: PostView, } @@ -93,23 +89,23 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) { - return Err(APIError::err(&self.op, "no_slurs").into()); + return Err(APIError::err("no_slurs").into()); } let user_id = claims.id; // Check for a community ban if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban").into()); + return Err(APIError::err("community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } let post_form = PostForm { @@ -128,7 +124,7 @@ impl Perform for Oper { let inserted_post = match Post::create(&conn, &post_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post").into()), + Err(_e) => return Err(APIError::err("couldnt_create_post").into()), }; // They like their own post by default @@ -141,19 +137,16 @@ impl Perform for Oper { // Only add the like if the score isnt 0 let _inserted_like = match PostLike::like(&conn, &like_form) { Ok(like) => like, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()), + Err(_e) => return Err(APIError::err("couldnt_like_post").into()), }; // Refetch the view let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()), + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), }; - Ok(PostResponse { - op: self.op.to_string(), - post: post_view, - }) + Ok(PostResponse { post: post_view }) } } @@ -174,7 +167,7 @@ impl Perform for Oper { let post_view = match PostView::read(&conn, data.id, user_id) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()), + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), }; let comments = CommentQueryBuilder::create(&conn) @@ -195,7 +188,6 @@ impl Perform for Oper { // Return the jwt Ok(GetPostResponse { - op: self.op.to_string(), post: post_view, comments, community, @@ -241,13 +233,10 @@ impl Perform for Oper { .list() { Ok(posts) => posts, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts").into()), + Err(_e) => return Err(APIError::err("couldnt_get_posts").into()), }; - Ok(GetPostsResponse { - op: self.op.to_string(), - posts, - }) + Ok(GetPostsResponse { posts }) } } @@ -257,7 +246,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -266,19 +255,19 @@ impl Perform for Oper { if data.score == -1 { let site = SiteView::read(&conn)?; if !site.enable_downvotes { - return Err(APIError::err(&self.op, "downvotes_disabled").into()); + return Err(APIError::err("downvotes_disabled").into()); } } // Check for a community ban let post = Post::read(&conn, data.post_id)?; if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban").into()); + return Err(APIError::err("community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } let like_form = PostLikeForm { @@ -295,20 +284,17 @@ impl Perform for Oper { if do_add { let _inserted_like = match PostLike::like(&conn, &like_form) { Ok(like) => like, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()), + Err(_e) => return Err(APIError::err("couldnt_like_post").into()), }; } let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()), + Err(_e) => return Err(APIError::err("couldnt_find_post").into()), }; // just output the score - Ok(CreatePostLikeResponse { - op: self.op.to_string(), - post: post_view, - }) + Ok(CreatePostLikeResponse { post: post_view }) } } @@ -316,12 +302,12 @@ impl Perform for Oper { fn perform(&self, conn: &PgConnection) -> Result { let data: &EditPost = &self.data; if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) { - return Err(APIError::err(&self.op, "no_slurs").into()); + return Err(APIError::err("no_slurs").into()); } let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -336,17 +322,17 @@ impl Perform for Oper { ); editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect()); if !editors.contains(&user_id) { - return Err(APIError::err(&self.op, "no_post_edit_allowed").into()); + return Err(APIError::err("no_post_edit_allowed").into()); } // Check for a community ban if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban").into()); + return Err(APIError::err("community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban").into()); + return Err(APIError::err("site_ban").into()); } let post_form = PostForm { @@ -365,7 +351,7 @@ impl Perform for Oper { let _updated_post = match Post::update(&conn, data.edit_id, &post_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()), + Err(_e) => return Err(APIError::err("couldnt_update_post").into()), }; // Mod tables @@ -399,10 +385,7 @@ impl Perform for Oper { let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?; - Ok(PostResponse { - op: self.op.to_string(), - post: post_view, - }) + Ok(PostResponse { post: post_view }) } } @@ -412,7 +395,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -425,20 +408,17 @@ impl Perform for Oper { if data.save { match PostSaved::save(&conn, &post_saved_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()), + Err(_e) => return Err(APIError::err("couldnt_save_post").into()), }; } else { match PostSaved::unsave(&conn, &post_saved_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()), + Err(_e) => return Err(APIError::err("couldnt_save_post").into()), }; } let post_view = PostView::read(&conn, data.post_id, Some(user_id))?; - Ok(PostResponse { - op: self.op.to_string(), - post: post_view, - }) + Ok(PostResponse { post: post_view }) } } diff --git a/server/src/api/site.rs b/server/src/api/site.rs index ce07724a38e..a5faf34dd82 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -7,7 +7,6 @@ pub struct ListCategories; #[derive(Serialize, Deserialize)] pub struct ListCategoriesResponse { - op: String, categories: Vec, } @@ -24,7 +23,6 @@ pub struct Search { #[derive(Serialize, Deserialize)] pub struct SearchResponse { - op: String, type_: String, comments: Vec, posts: Vec, @@ -42,7 +40,6 @@ pub struct GetModlog { #[derive(Serialize, Deserialize)] pub struct GetModlogResponse { - op: String, removed_posts: Vec, locked_posts: Vec, stickied_posts: Vec, @@ -79,13 +76,11 @@ pub struct GetSite; #[derive(Serialize, Deserialize)] pub struct SiteResponse { - op: String, site: SiteView, } #[derive(Serialize, Deserialize)] pub struct GetSiteResponse { - op: String, site: Option, admins: Vec, banned: Vec, @@ -105,10 +100,7 @@ impl Perform for Oper { let categories: Vec = Category::list_all(&conn)?; // Return the jwt - Ok(ListCategoriesResponse { - op: self.op.to_string(), - categories, - }) + Ok(ListCategoriesResponse { categories }) } } @@ -172,7 +164,6 @@ impl Perform for Oper { // Return the jwt Ok(GetModlogResponse { - op: self.op.to_string(), removed_posts, locked_posts, stickied_posts, @@ -192,20 +183,20 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; if has_slurs(&data.name) || (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) { - return Err(APIError::err(&self.op, "no_slurs").into()); + return Err(APIError::err("no_slurs").into()); } let user_id = claims.id; // Make sure user is an admin if !UserView::read(&conn, user_id)?.admin { - return Err(APIError::err(&self.op, "not_an_admin").into()); + return Err(APIError::err("not_an_admin").into()); } let site_form = SiteForm { @@ -220,15 +211,12 @@ impl Perform for Oper { match Site::create(&conn, &site_form) { Ok(site) => site, - Err(_e) => return Err(APIError::err(&self.op, "site_already_exists").into()), + Err(_e) => return Err(APIError::err("site_already_exists").into()), }; let site_view = SiteView::read(&conn)?; - Ok(SiteResponse { - op: self.op.to_string(), - site: site_view, - }) + Ok(SiteResponse { site: site_view }) } } @@ -238,20 +226,20 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; if has_slurs(&data.name) || (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) { - return Err(APIError::err(&self.op, "no_slurs").into()); + return Err(APIError::err("no_slurs").into()); } let user_id = claims.id; // Make sure user is an admin if !UserView::read(&conn, user_id)?.admin { - return Err(APIError::err(&self.op, "not_an_admin").into()); + return Err(APIError::err("not_an_admin").into()); } let found_site = Site::read(&conn, 1)?; @@ -268,15 +256,12 @@ impl Perform for Oper { match Site::update(&conn, 1, &site_form) { Ok(site) => site, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()), + Err(_e) => return Err(APIError::err("couldnt_update_site").into()), }; let site_view = SiteView::read(&conn)?; - Ok(SiteResponse { - op: self.op.to_string(), - site: site_view, - }) + Ok(SiteResponse { site: site_view }) } } @@ -301,7 +286,6 @@ impl Perform for Oper { let banned = UserView::banned(&conn)?; Ok(GetSiteResponse { - op: self.op.to_string(), site: site_view, admins, banned, @@ -419,7 +403,6 @@ impl Perform for Oper { // Return the jwt Ok(SearchResponse { - op: self.op.to_string(), type_: data.type_.to_owned(), comments, posts, @@ -435,7 +418,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -444,7 +427,7 @@ impl Perform for Oper { // Make sure user is the creator if read_site.creator_id != user_id { - return Err(APIError::err(&self.op, "not_an_admin").into()); + return Err(APIError::err("not_an_admin").into()); } let site_form = SiteForm { @@ -459,7 +442,7 @@ impl Perform for Oper { match Site::update(&conn, 1, &site_form) { Ok(site) => site, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()), + Err(_e) => return Err(APIError::err("couldnt_update_site").into()), }; // Mod tables @@ -484,7 +467,6 @@ impl Perform for Oper { let banned = UserView::banned(&conn)?; Ok(GetSiteResponse { - op: self.op.to_string(), site: Some(site_view), admins, banned, diff --git a/server/src/api/user.rs b/server/src/api/user.rs index ac700acad53..8d2db104cd2 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -30,6 +30,7 @@ pub struct SaveUserSettings { lang: String, avatar: Option, email: Option, + matrix_user_id: Option, new_password: Option, new_password_verify: Option, old_password: Option, @@ -40,7 +41,6 @@ pub struct SaveUserSettings { #[derive(Serialize, Deserialize)] pub struct LoginResponse { - op: String, jwt: String, } @@ -58,7 +58,6 @@ pub struct GetUserDetails { #[derive(Serialize, Deserialize)] pub struct GetUserDetailsResponse { - op: String, user: UserView, follows: Vec, moderates: Vec, @@ -69,13 +68,11 @@ pub struct GetUserDetailsResponse { #[derive(Serialize, Deserialize)] pub struct GetRepliesResponse { - op: String, replies: Vec, } #[derive(Serialize, Deserialize)] pub struct GetUserMentionsResponse { - op: String, mentions: Vec, } @@ -93,7 +90,6 @@ pub struct AddAdmin { #[derive(Serialize, Deserialize)] pub struct AddAdminResponse { - op: String, admins: Vec, } @@ -108,7 +104,6 @@ pub struct BanUser { #[derive(Serialize, Deserialize)] pub struct BanUserResponse { - op: String, user: UserView, banned: bool, } @@ -140,7 +135,6 @@ pub struct EditUserMention { #[derive(Serialize, Deserialize, Clone)] pub struct UserMentionResponse { - op: String, mention: UserMentionView, } @@ -156,9 +150,7 @@ pub struct PasswordReset { } #[derive(Serialize, Deserialize, Clone)] -pub struct PasswordResetResponse { - op: String, -} +pub struct PasswordResetResponse {} #[derive(Serialize, Deserialize)] pub struct PasswordChange { @@ -167,6 +159,40 @@ pub struct PasswordChange { password_verify: String, } +#[derive(Serialize, Deserialize)] +pub struct CreatePrivateMessage { + content: String, + recipient_id: i32, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct EditPrivateMessage { + edit_id: i32, + content: Option, + deleted: Option, + read: Option, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct GetPrivateMessages { + unread_only: bool, + page: Option, + limit: Option, + auth: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PrivateMessagesResponse { + messages: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PrivateMessageResponse { + message: PrivateMessageView, +} + impl Perform for Oper { fn perform(&self, conn: &PgConnection) -> Result { let data: &Login = &self.data; @@ -174,20 +200,17 @@ impl Perform for Oper { // Fetch that username / email let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()), + Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()), }; // Verify the password let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false); if !valid { - return Err(APIError::err(&self.op, "password_incorrect").into()); + return Err(APIError::err("password_incorrect").into()); } // Return the jwt - Ok(LoginResponse { - op: self.op.to_string(), - jwt: user.jwt(), - }) + Ok(LoginResponse { jwt: user.jwt() }) } } @@ -198,22 +221,22 @@ impl Perform for Oper { // Make sure site has open registration if let Ok(site) = SiteView::read(&conn) { if !site.open_registration { - return Err(APIError::err(&self.op, "registration_closed").into()); + return Err(APIError::err("registration_closed").into()); } } // Make sure passwords match if data.password != data.password_verify { - return Err(APIError::err(&self.op, "passwords_dont_match").into()); + return Err(APIError::err("passwords_dont_match").into()); } if has_slurs(&data.username) { - return Err(APIError::err(&self.op, "no_slurs").into()); + return Err(APIError::err("no_slurs").into()); } // Make sure there are no admins if data.admin && !UserView::admins(&conn)?.is_empty() { - return Err(APIError::err(&self.op, "admin_already_created").into()); + return Err(APIError::err("admin_already_created").into()); } // Register the new user @@ -221,6 +244,7 @@ impl Perform for Oper { name: data.username.to_owned(), fedi_name: Settings::get().hostname.to_owned(), email: data.email.to_owned(), + matrix_user_id: None, avatar: None, password_encrypted: data.password.to_owned(), preferred_username: None, @@ -248,7 +272,7 @@ impl Perform for Oper { "user_already_exists" }; - return Err(APIError::err(&self.op, err_type).into()); + return Err(APIError::err(err_type).into()); } }; @@ -280,7 +304,7 @@ impl Perform for Oper { let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()), + Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), }; // If its an admin, add them as a mod and follower to main @@ -293,15 +317,12 @@ impl Perform for Oper { let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { Ok(user) => user, - Err(_e) => { - return Err(APIError::err(&self.op, "community_moderator_already_exists").into()) - } + Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }; } // Return the jwt Ok(LoginResponse { - op: self.op.to_string(), jwt: inserted_user.jwt(), }) } @@ -313,7 +334,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -331,7 +352,7 @@ impl Perform for Oper { Some(new_password_verify) => { // Make sure passwords match if new_password != new_password_verify { - return Err(APIError::err(&self.op, "passwords_dont_match").into()); + return Err(APIError::err("passwords_dont_match").into()); } // Check the old password @@ -340,14 +361,14 @@ impl Perform for Oper { let valid: bool = verify(old_password, &read_user.password_encrypted).unwrap_or(false); if !valid { - return Err(APIError::err(&self.op, "password_incorrect").into()); + return Err(APIError::err("password_incorrect").into()); } User_::update_password(&conn, user_id, &new_password)?.password_encrypted } - None => return Err(APIError::err(&self.op, "password_incorrect").into()), + None => return Err(APIError::err("password_incorrect").into()), } } - None => return Err(APIError::err(&self.op, "passwords_dont_match").into()), + None => return Err(APIError::err("passwords_dont_match").into()), } } None => read_user.password_encrypted, @@ -357,6 +378,7 @@ impl Perform for Oper { name: read_user.name, fedi_name: read_user.fedi_name, email, + matrix_user_id: data.matrix_user_id.to_owned(), avatar: data.avatar.to_owned(), password_encrypted, preferred_username: read_user.preferred_username, @@ -383,13 +405,12 @@ impl Perform for Oper { "user_already_exists" }; - return Err(APIError::err(&self.op, err_type).into()); + return Err(APIError::err(err_type).into()); } }; // Return the jwt Ok(LoginResponse { - op: self.op.to_string(), jwt: updated_user.jwt(), }) } @@ -430,9 +451,7 @@ impl Perform for Oper { .unwrap_or_else(|| "admin".to_string()), ) { Ok(user) => user.id, - Err(_e) => { - return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()) - } + Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()), } } }; @@ -475,7 +494,6 @@ impl Perform for Oper { // Return the jwt Ok(GetUserDetailsResponse { - op: self.op.to_string(), user: user_view, follows, moderates, @@ -492,22 +510,24 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; // Make sure user is an admin if !UserView::read(&conn, user_id)?.admin { - return Err(APIError::err(&self.op, "not_an_admin").into()); + return Err(APIError::err("not_an_admin").into()); } let read_user = User_::read(&conn, data.user_id)?; + // TODO make addadmin easier let user_form = UserForm { name: read_user.name, fedi_name: read_user.fedi_name, email: read_user.email, + matrix_user_id: read_user.matrix_user_id, avatar: read_user.avatar, password_encrypted: read_user.password_encrypted, preferred_username: read_user.preferred_username, @@ -525,7 +545,7 @@ impl Perform for Oper { match User_::update(&conn, data.user_id, &user_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()), + Err(_e) => return Err(APIError::err("couldnt_update_user").into()), }; // Mod tables @@ -543,10 +563,7 @@ impl Perform for Oper { let creator_user = admins.remove(creator_index); admins.insert(0, creator_user); - Ok(AddAdminResponse { - op: self.op.to_string(), - admins, - }) + Ok(AddAdminResponse { admins }) } } @@ -556,22 +573,24 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; // Make sure user is an admin if !UserView::read(&conn, user_id)?.admin { - return Err(APIError::err(&self.op, "not_an_admin").into()); + return Err(APIError::err("not_an_admin").into()); } let read_user = User_::read(&conn, data.user_id)?; + // TODO make bans and addadmins easier let user_form = UserForm { name: read_user.name, fedi_name: read_user.fedi_name, email: read_user.email, + matrix_user_id: read_user.matrix_user_id, avatar: read_user.avatar, password_encrypted: read_user.password_encrypted, preferred_username: read_user.preferred_username, @@ -589,7 +608,7 @@ impl Perform for Oper { match User_::update(&conn, data.user_id, &user_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()), + Err(_e) => return Err(APIError::err("couldnt_update_user").into()), }; // Mod tables @@ -611,7 +630,6 @@ impl Perform for Oper { let user_view = UserView::read(&conn, data.user_id)?; Ok(BanUserResponse { - op: self.op.to_string(), user: user_view, banned: data.ban, }) @@ -624,7 +642,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -638,10 +656,7 @@ impl Perform for Oper { .limit(data.limit) .list()?; - Ok(GetRepliesResponse { - op: self.op.to_string(), - replies, - }) + Ok(GetRepliesResponse { replies }) } } @@ -651,7 +666,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -665,10 +680,7 @@ impl Perform for Oper { .limit(data.limit) .list()?; - Ok(GetUserMentionsResponse { - op: self.op.to_string(), - mentions, - }) + Ok(GetUserMentionsResponse { mentions }) } } @@ -678,7 +690,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -694,13 +706,12 @@ impl Perform for Oper { let _updated_user_mention = match UserMention::update(&conn, user_mention.id, &user_mention_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?; Ok(UserMentionResponse { - op: self.op.to_string(), mention: user_mention_view, }) } @@ -712,7 +723,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -737,7 +748,7 @@ impl Perform for Oper { let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; } @@ -758,14 +769,35 @@ impl Perform for Oper { let _updated_mention = match UserMention::update(&conn, mention.user_mention_id, &mention_form) { Ok(mention) => mention, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; } - Ok(GetRepliesResponse { - op: self.op.to_string(), - replies: vec![], - }) + // messages + let messages = PrivateMessageQueryBuilder::create(&conn, user_id) + .page(1) + .limit(999) + .unread_only(true) + .list()?; + + for message in &messages { + let private_message_form = PrivateMessageForm { + content: None, + creator_id: message.to_owned().creator_id, + recipient_id: message.to_owned().recipient_id, + deleted: None, + read: Some(true), + updated: None, + }; + + let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form) + { + Ok(message) => message, + Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), + }; + } + + Ok(GetRepliesResponse { replies: vec![] }) } } @@ -775,7 +807,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; @@ -785,7 +817,7 @@ impl Perform for Oper { // Verify the password let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false); if !valid { - return Err(APIError::err(&self.op, "password_incorrect").into()); + return Err(APIError::err("password_incorrect").into()); } // Comments @@ -808,7 +840,7 @@ impl Perform for Oper { let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; } @@ -836,12 +868,11 @@ impl Perform for Oper { let _updated_post = match Post::update(&conn, post.id, &post_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()), + Err(_e) => return Err(APIError::err("couldnt_update_post").into()), }; } Ok(LoginResponse { - op: self.op.to_string(), jwt: data.auth.to_owned(), }) } @@ -854,7 +885,7 @@ impl Perform for Oper { // Fetch that email let user: User_ = match User_::find_by_email(&conn, &data.email) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()), + Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()), }; // Generate a random token @@ -871,12 +902,10 @@ impl Perform for Oper { let html = &format!("

Password Reset Request for {}


Click here to reset your password", user.name, hostname, &token); match send_email(subject, user_email, &user.name, html) { Ok(_o) => _o, - Err(_e) => return Err(APIError::err(&self.op, &_e).into()), + Err(_e) => return Err(APIError::err(&_e).into()), }; - Ok(PasswordResetResponse { - op: self.op.to_string(), - }) + Ok(PasswordResetResponse {}) } } @@ -889,19 +918,156 @@ impl Perform for Oper { // Make sure passwords match if data.password != data.password_verify { - return Err(APIError::err(&self.op, "passwords_dont_match").into()); + return Err(APIError::err("passwords_dont_match").into()); } // Update the user with the new password let updated_user = match User_::update_password(&conn, user_id, &data.password) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()), + Err(_e) => return Err(APIError::err("couldnt_update_user").into()), }; // Return the jwt Ok(LoginResponse { - op: self.op.to_string(), jwt: updated_user.jwt(), }) } } + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &CreatePrivateMessage = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let hostname = &format!("https://{}", Settings::get().hostname); + + // Check for a site ban + if UserView::read(&conn, user_id)?.banned { + return Err(APIError::err("site_ban").into()); + } + + let content_slurs_removed = remove_slurs(&data.content.to_owned()); + + let private_message_form = PrivateMessageForm { + content: Some(content_slurs_removed.to_owned()), + creator_id: user_id, + recipient_id: data.recipient_id, + deleted: None, + read: None, + updated: None, + }; + + let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) { + Ok(private_message) => private_message, + Err(_e) => { + return Err(APIError::err("couldnt_create_private_message").into()); + } + }; + + // Send notifications to the recipient + let recipient_user = User_::read(&conn, data.recipient_id)?; + if recipient_user.send_notifications_to_email { + if let Some(email) = recipient_user.email { + let subject = &format!( + "{} - Private Message from {}", + Settings::get().hostname, + claims.username + ); + let html = &format!( + "

Private Message


{} - {}

inbox", + claims.username, &content_slurs_removed, hostname + ); + match send_email(subject, &email, &recipient_user.name, html) { + Ok(_o) => _o, + Err(e) => eprintln!("{}", e), + }; + } + } + + let message = PrivateMessageView::read(&conn, inserted_private_message.id)?; + + Ok(PrivateMessageResponse { message }) + } +} + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &EditPrivateMessage = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?; + + // Check for a site ban + if UserView::read(&conn, user_id)?.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check to make sure they are the creator (or the recipient marking as read + if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id) + || orig_private_message.creator_id.eq(&user_id)) + { + return Err(APIError::err("no_private_message_edit_allowed").into()); + } + + let content_slurs_removed = match &data.content { + Some(content) => Some(remove_slurs(content)), + None => None, + }; + + let private_message_form = PrivateMessageForm { + content: content_slurs_removed, + creator_id: orig_private_message.creator_id, + recipient_id: orig_private_message.recipient_id, + deleted: data.deleted.to_owned(), + read: data.read.to_owned(), + updated: if data.read.is_some() { + orig_private_message.updated + } else { + Some(naive_now()) + }, + }; + + let _updated_private_message = + match PrivateMessage::update(&conn, data.edit_id, &private_message_form) { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), + }; + + let message = PrivateMessageView::read(&conn, data.edit_id)?; + + Ok(PrivateMessageResponse { message }) + } +} + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &GetPrivateMessages = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let messages = PrivateMessageQueryBuilder::create(&conn, user_id) + .page(data.page) + .limit(data.limit) + .unread_only(data.unread_only) + .list()?; + + Ok(PrivateMessagesResponse { messages }) + } +} diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 2d2e5ad301c..c5a0b2f0298 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -22,6 +22,7 @@ mod tests { preferred_username: None, password_encrypted: "here".into(), email: None, + matrix_user_id: None, avatar: None, published: naive_now(), admin: false, diff --git a/server/src/db/comment.rs b/server/src/db/comment.rs index a9c7d81ddc0..efba07a5186 100644 --- a/server/src/db/comment.rs +++ b/server/src/db/comment.rs @@ -174,6 +174,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/comment_view.rs b/server/src/db/comment_view.rs index 3b06e8e347f..d4a65c9a970 100644 --- a/server/src/db/comment_view.rs +++ b/server/src/db/comment_view.rs @@ -398,6 +398,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/community.rs b/server/src/db/community.rs index b482ca4a91d..6350096358b 100644 --- a/server/src/db/community.rs +++ b/server/src/db/community.rs @@ -220,6 +220,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index fef3ffce5df..dacdb6f6a92 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -15,6 +15,8 @@ pub mod moderator_views; pub mod password_reset_request; pub mod post; pub mod post_view; +pub mod private_message; +pub mod private_message_view; pub mod site; pub mod site_view; pub mod user; diff --git a/server/src/db/moderator.rs b/server/src/db/moderator.rs index 3c6233cb99d..4fd532afdbe 100644 --- a/server/src/db/moderator.rs +++ b/server/src/db/moderator.rs @@ -442,6 +442,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, @@ -463,6 +464,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/password_reset_request.rs b/server/src/db/password_reset_request.rs index fa060a591bf..6951fd39936 100644 --- a/server/src/db/password_reset_request.rs +++ b/server/src/db/password_reset_request.rs @@ -92,6 +92,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/post.rs b/server/src/db/post.rs index d3fba4dad0f..9e7a43410bd 100644 --- a/server/src/db/post.rs +++ b/server/src/db/post.rs @@ -187,6 +187,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index f6cc274f089..c80d1696720 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -339,6 +339,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, updated: None, admin: false, diff --git a/server/src/db/private_message.rs b/server/src/db/private_message.rs new file mode 100644 index 00000000000..cc073b594e2 --- /dev/null +++ b/server/src/db/private_message.rs @@ -0,0 +1,144 @@ +use super::*; +use crate::schema::private_message; + +#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] +#[table_name = "private_message"] +pub struct PrivateMessage { + pub id: i32, + pub creator_id: i32, + pub recipient_id: i32, + pub content: String, + pub deleted: bool, + pub read: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name = "private_message"] +pub struct PrivateMessageForm { + pub creator_id: i32, + pub recipient_id: i32, + pub content: Option, + pub deleted: Option, + pub read: Option, + pub updated: Option, +} + +impl Crud for PrivateMessage { + fn read(conn: &PgConnection, private_message_id: i32) -> Result { + use crate::schema::private_message::dsl::*; + private_message.find(private_message_id).first::(conn) + } + + fn delete(conn: &PgConnection, private_message_id: i32) -> Result { + use crate::schema::private_message::dsl::*; + diesel::delete(private_message.find(private_message_id)).execute(conn) + } + + fn create(conn: &PgConnection, private_message_form: &PrivateMessageForm) -> Result { + use crate::schema::private_message::dsl::*; + insert_into(private_message) + .values(private_message_form) + .get_result::(conn) + } + + fn update( + conn: &PgConnection, + private_message_id: i32, + private_message_form: &PrivateMessageForm, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(private_message_form) + .get_result::(conn) + } +} + +#[cfg(test)] +mod tests { + use super::super::user::*; + use super::*; + #[test] + fn test_crud() { + let conn = establish_unpooled_connection(); + + let creator_form = UserForm { + name: "creator_pm".into(), + fedi_name: "rrf".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + matrix_user_id: None, + avatar: None, + admin: false, + banned: false, + updated: None, + show_nsfw: false, + theme: "darkly".into(), + default_sort_type: SortType::Hot as i16, + default_listing_type: ListingType::Subscribed as i16, + lang: "browser".into(), + show_avatars: true, + send_notifications_to_email: false, + }; + + let inserted_creator = User_::create(&conn, &creator_form).unwrap(); + + let recipient_form = UserForm { + name: "recipient_pm".into(), + fedi_name: "rrf".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + matrix_user_id: None, + avatar: None, + admin: false, + banned: false, + updated: None, + show_nsfw: false, + theme: "darkly".into(), + default_sort_type: SortType::Hot as i16, + default_listing_type: ListingType::Subscribed as i16, + lang: "browser".into(), + show_avatars: true, + send_notifications_to_email: false, + }; + + let inserted_recipient = User_::create(&conn, &recipient_form).unwrap(); + + let private_message_form = PrivateMessageForm { + content: Some("A test private message".into()), + creator_id: inserted_creator.id, + recipient_id: inserted_recipient.id, + deleted: None, + read: None, + updated: None, + }; + + let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap(); + + let expected_private_message = PrivateMessage { + id: inserted_private_message.id, + content: "A test private message".into(), + creator_id: inserted_creator.id, + recipient_id: inserted_recipient.id, + deleted: false, + read: false, + updated: None, + published: inserted_private_message.published, + }; + + let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap(); + let updated_private_message = + PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap(); + let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap(); + User_::delete(&conn, inserted_creator.id).unwrap(); + User_::delete(&conn, inserted_recipient.id).unwrap(); + + assert_eq!(expected_private_message, read_private_message); + assert_eq!(expected_private_message, updated_private_message); + assert_eq!(expected_private_message, inserted_private_message); + assert_eq!(1, num_deleted); + } +} diff --git a/server/src/db/private_message_view.rs b/server/src/db/private_message_view.rs new file mode 100644 index 00000000000..59a573f4cca --- /dev/null +++ b/server/src/db/private_message_view.rs @@ -0,0 +1,140 @@ +use super::*; +use diesel::pg::Pg; + +// The faked schema since diesel doesn't do views +table! { + private_message_view (id) { + id -> Int4, + creator_id -> Int4, + recipient_id -> Int4, + content -> Text, + deleted -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + creator_name -> Varchar, + creator_avatar -> Nullable, + recipient_name -> Varchar, + recipient_avatar -> Nullable, + } +} + +table! { + private_message_mview (id) { + id -> Int4, + creator_id -> Int4, + recipient_id -> Int4, + content -> Text, + deleted -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + creator_name -> Varchar, + creator_avatar -> Nullable, + recipient_name -> Varchar, + recipient_avatar -> Nullable, + } +} + +#[derive( + Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, +)] +#[table_name = "private_message_view"] +pub struct PrivateMessageView { + pub id: i32, + pub creator_id: i32, + pub recipient_id: i32, + pub content: String, + pub deleted: bool, + pub read: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub creator_name: String, + pub creator_avatar: Option, + pub recipient_name: String, + pub recipient_avatar: Option, +} + +pub struct PrivateMessageQueryBuilder<'a> { + conn: &'a PgConnection, + query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>, + for_recipient_id: i32, + unread_only: bool, + page: Option, + limit: Option, +} + +impl<'a> PrivateMessageQueryBuilder<'a> { + pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self { + use super::private_message_view::private_message_mview::dsl::*; + + let query = private_message_mview.into_boxed(); + + PrivateMessageQueryBuilder { + conn, + query, + for_recipient_id, + unread_only: false, + page: None, + limit: None, + } + } + + pub fn unread_only(mut self, unread_only: bool) -> Self { + self.unread_only = unread_only; + self + } + + pub fn page>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn list(self) -> Result, Error> { + use super::private_message_view::private_message_mview::dsl::*; + + let mut query = self.query; + + // If its unread, I only want the ones to me + if self.unread_only { + query = query + .filter(read.eq(false)) + .filter(recipient_id.eq(self.for_recipient_id)); + } + // Otherwise, I want the ALL view to show both sent and received + else { + query = query.filter( + recipient_id + .eq(self.for_recipient_id) + .or(creator_id.eq(self.for_recipient_id)), + ) + } + + let (limit, offset) = limit_and_offset(self.page, self.limit); + + query + .limit(limit) + .offset(offset) + .order_by(published.desc()) + .load::(self.conn) + } +} + +impl PrivateMessageView { + pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result { + use super::private_message_view::private_message_view::dsl::*; + + let mut query = private_message_view.into_boxed(); + + query = query + .filter(id.eq(from_private_message_id)) + .order_by(published.desc()); + + query.first::(conn) + } +} diff --git a/server/src/db/user.rs b/server/src/db/user.rs index 71b63d742c8..b36c07bea70 100644 --- a/server/src/db/user.rs +++ b/server/src/db/user.rs @@ -26,6 +26,7 @@ pub struct User_ { pub lang: String, pub show_avatars: bool, pub send_notifications_to_email: bool, + pub matrix_user_id: Option, } #[derive(Insertable, AsChangeset, Clone)] @@ -47,6 +48,7 @@ pub struct UserForm { pub lang: String, pub show_avatars: bool, pub send_notifications_to_email: bool, + pub matrix_user_id: Option, } impl Crud for User_ { @@ -184,6 +186,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, @@ -206,6 +209,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/user_mention.rs b/server/src/db/user_mention.rs index 21dd1675d35..3b10fd0ff42 100644 --- a/server/src/db/user_mention.rs +++ b/server/src/db/user_mention.rs @@ -68,6 +68,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, @@ -89,6 +90,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/user_view.rs b/server/src/db/user_view.rs index 23e47d4bef4..3ea506e7f9c 100644 --- a/server/src/db/user_view.rs +++ b/server/src/db/user_view.rs @@ -8,6 +8,7 @@ table! { name -> Varchar, avatar -> Nullable, email -> Nullable, + matrix_user_id -> Nullable, fedi_name -> Varchar, admin -> Bool, banned -> Bool, @@ -27,6 +28,7 @@ table! { name -> Varchar, avatar -> Nullable, email -> Nullable, + matrix_user_id -> Nullable, fedi_name -> Varchar, admin -> Bool, banned -> Bool, @@ -49,6 +51,7 @@ pub struct UserView { pub name: String, pub avatar: Option, pub email: Option, + pub matrix_user_id: Option, pub fedi_name: String, pub admin: bool, pub banned: bool, diff --git a/server/src/lib.rs b/server/src/lib.rs index 4f5554fa268..a9bd5dac7d3 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -105,7 +105,7 @@ pub fn send_email( let mut mailer = SmtpClient::new_simple(&email_config.smtp_server) .unwrap() - .hello_name(ClientId::Domain("localhost".to_string())) + .hello_name(ClientId::Domain(Settings::get().hostname.to_owned())) .credentials(Credentials::new( email_config.smtp_login.to_owned(), email_config.smtp_password.to_owned(), @@ -117,6 +117,8 @@ pub fn send_email( let result = mailer.send(email.into()); + mailer.close(); + match result { Ok(_) => Ok(()), Err(_) => Err("no_email_setup".to_string()), diff --git a/server/src/main.rs b/server/src/main.rs index 636182aa45c..601c2e0dc01 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,7 +6,7 @@ use actix::prelude::*; use actix_web::*; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::PgConnection; -use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket}; +use lemmy_server::routes::{api, federation, feeds, index, nodeinfo, webfinger, websocket}; use lemmy_server::settings::Settings; use lemmy_server::websocket::server::*; use std::io; @@ -44,6 +44,7 @@ async fn main() -> io::Result<()> { .data(pool.clone()) .data(server.clone()) // The routes + .configure(api::config) .configure(federation::config) .configure(feeds::config) .configure(index::config) diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs new file mode 100644 index 00000000000..5121d2401f0 --- /dev/null +++ b/server/src/routes/api.rs @@ -0,0 +1,103 @@ +use crate::api::comment::*; +use crate::api::community::*; +use crate::api::post::*; +use crate::api::site::*; +use crate::api::user::*; +use crate::api::{Oper, Perform}; +use actix_web::{web, HttpResponse}; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::PgConnection; +use failure::Error; +use serde::Serialize; + +type DbParam = web::Data>>; + +#[rustfmt::skip] +pub fn config(cfg: &mut web::ServiceConfig) { + cfg + // Site + .route("/api/v1/site", web::get().to(route_get::)) + .route("/api/v1/categories", web::get().to(route_get::)) + .route("/api/v1/modlog", web::get().to(route_get::)) + .route("/api/v1/search", web::get().to(route_get::)) + // Community + .route("/api/v1/community", web::post().to(route_post::)) + .route("/api/v1/community", web::get().to(route_get::)) + .route("/api/v1/community", web::put().to(route_post::)) + .route("/api/v1/community/list", web::get().to(route_get::)) + .route("/api/v1/community/follow", web::post().to(route_post::)) + // Post + .route("/api/v1/post", web::post().to(route_post::)) + .route("/api/v1/post", web::put().to(route_post::)) + .route("/api/v1/post", web::get().to(route_get::)) + .route("/api/v1/post/list", web::get().to(route_get::)) + .route("/api/v1/post/like", web::post().to(route_post::)) + .route("/api/v1/post/save", web::put().to(route_post::)) + // Comment + .route("/api/v1/comment", web::post().to(route_post::)) + .route("/api/v1/comment", web::put().to(route_post::)) + .route("/api/v1/comment/like", web::post().to(route_post::)) + .route("/api/v1/comment/save", web::put().to(route_post::)) + // User + .route("/api/v1/user", web::get().to(route_get::)) + .route("/api/v1/user/mention", web::get().to(route_get::)) + .route("/api/v1/user/mention", web::put().to(route_post::)) + .route("/api/v1/user/replies", web::get().to(route_get::)) + .route("/api/v1/user/followed_communities", web::get().to(route_get::)) + // Mod actions + .route("/api/v1/community/transfer", web::post().to(route_post::)) + .route("/api/v1/community/ban_user", web::post().to(route_post::)) + .route("/api/v1/community/mod", web::post().to(route_post::)) + // Admin actions + .route("/api/v1/site", web::post().to(route_post::)) + .route("/api/v1/site", web::put().to(route_post::)) + .route("/api/v1/site/transfer", web::post().to(route_post::)) + .route("/api/v1/admin/add", web::post().to(route_post::)) + .route("/api/v1/user/ban", web::post().to(route_post::)) + // User account actions + .route("/api/v1/user/login", web::post().to(route_post::)) + .route("/api/v1/user/register", web::post().to(route_post::)) + .route("/api/v1/user/delete_account", web::post().to(route_post::)) + .route("/api/v1/user/password_reset", web::post().to(route_post::)) + .route("/api/v1/user/password_change", web::post().to(route_post::)) + .route("/api/v1/user/mark_all_as_read", web::post().to(route_post::)) + .route("/api/v1/user/save_user_settings", web::put().to(route_post::)); +} + +fn perform(data: Request, db: DbParam) -> Result +where + Response: Serialize, + Oper: Perform, +{ + let conn = match db.get() { + Ok(c) => c, + Err(e) => return Err(format_err!("{}", e)), + }; + let oper: Oper = Oper::new(data); + let response = oper.perform(&conn); + Ok(HttpResponse::Ok().json(response?)) +} + +async fn route_get( + data: web::Query, + db: DbParam, +) -> Result +where + Data: Serialize, + Response: Serialize, + Oper: Perform, +{ + perform::(data.0, db) +} + +async fn route_post( + data: web::Json, + db: DbParam, +) -> Result +where + Data: Serialize, + Response: Serialize, + Oper: Perform, +{ + perform::(data.0, db) +} diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs index 2453a1b24bb..b044833efb1 100644 --- a/server/src/routes/index.rs +++ b/server/src/routes/index.rs @@ -12,6 +12,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("/login", web::get().to(index)) .route("/create_post", web::get().to(index)) .route("/create_community", web::get().to(index)) + .route("/create_private_message", web::get().to(index)) .route("/communities/page/{page}", web::get().to(index)) .route("/communities", web::get().to(index)) .route("/post/{id}/comment/{id2}", web::get().to(index)) diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 6556c8d5885..27d9ea1be59 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,3 +1,4 @@ +pub mod api; pub mod federation; pub mod feeds; pub mod index; diff --git a/server/src/schema.rs b/server/src/schema.rs index 61957067c51..5330ed070de 100644 --- a/server/src/schema.rs +++ b/server/src/schema.rs @@ -238,6 +238,19 @@ table! { } } +table! { + private_message (id) { + id -> Int4, + creator_id -> Int4, + recipient_id -> Int4, + content -> Text, + deleted -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + } +} + table! { site (id) { id -> Int4, @@ -272,6 +285,7 @@ table! { lang -> Varchar, show_avatars -> Bool, send_notifications_to_email -> Bool, + matrix_user_id -> Nullable, } } @@ -357,6 +371,7 @@ allow_tables_to_appear_in_same_query!( post_like, post_read, post_saved, + private_message, site, user_, user_ban, diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index 74f47ad347d..021bcb41ee9 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -1 +1,47 @@ pub mod server; + +#[derive(EnumString, ToString, Debug)] +pub enum UserOperation { + Login, + Register, + CreateCommunity, + CreatePost, + ListCommunities, + ListCategories, + GetPost, + GetCommunity, + CreateComment, + EditComment, + SaveComment, + CreateCommentLike, + GetPosts, + CreatePostLike, + EditPost, + SavePost, + EditCommunity, + FollowCommunity, + GetFollowedCommunities, + GetUserDetails, + GetReplies, + GetUserMentions, + EditUserMention, + GetModlog, + BanFromCommunity, + AddModToCommunity, + CreateSite, + EditSite, + GetSite, + AddAdmin, + BanUser, + Search, + MarkAllAsRead, + SaveUserSettings, + TransferCommunity, + TransferSite, + DeleteAccount, + PasswordReset, + PasswordChange, + CreatePrivateMessage, + EditPrivateMessage, + GetPrivateMessages, +} diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 957c5f64329..b1d4f1387ec 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -3,7 +3,7 @@ //! room through `ChatServer`. use actix::prelude::*; -use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; use diesel::PgConnection; use failure::Error; use rand::{rngs::ThreadRng, Rng}; @@ -19,6 +19,7 @@ use crate::api::post::*; use crate::api::site::*; use crate::api::user::*; use crate::api::*; +use crate::websocket::UserOperation; use crate::Settings; /// Chat server sends this messages to session @@ -201,7 +202,6 @@ impl ChatServer { ); Err( APIError { - op: "Rate Limit".to_string(), message: format!("Too many requests. {} per {} seconds", rate, per), } .into(), @@ -295,11 +295,42 @@ impl Handler for ChatServer { } } +#[derive(Serialize)] +struct WebsocketResponse { + op: String, + data: T, +} + +fn to_json_string(op: &UserOperation, data: T) -> Result +where + T: Serialize, +{ + let response = WebsocketResponse { + op: op.to_string(), + data, + }; + Ok(serde_json::to_string(&response)?) +} + +fn do_user_operation<'a, Data, Response>( + op: UserOperation, + data: &str, + conn: &PooledConnection>, +) -> Result +where + for<'de> Data: Deserialize<'de> + 'a, + Response: Serialize, + Oper: Perform, +{ + let parsed_data: Data = serde_json::from_str(data)?; + let res = Oper::new(parsed_data).perform(&conn)?; + to_json_string(&op, &res) +} + fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { let json: Value = serde_json::from_str(&msg.msg)?; let data = &json["data"].to_string(); let op = &json["op"].as_str().ok_or(APIError { - op: "Unknown op type".to_string(), message: "Unknown op type".to_string(), })?; @@ -307,245 +338,194 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { - let login: Login = serde_json::from_str(data)?; - let res = Oper::new(user_operation, login).perform(&conn)?; - Ok(serde_json::to_string(&res)?) - } + UserOperation::Login => do_user_operation::(user_operation, data, &conn), UserOperation::Register => { - let register: Register = serde_json::from_str(data)?; - let res = Oper::new(user_operation, register).perform(&conn); - if res.is_ok() { - chat.check_rate_limit_register(msg.id)?; - } - Ok(serde_json::to_string(&res?)?) + chat.check_rate_limit_register(msg.id)?; + do_user_operation::(user_operation, data, &conn) } UserOperation::GetUserDetails => { - let get_user_details: GetUserDetails = serde_json::from_str(data)?; - let res = Oper::new(user_operation, get_user_details).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::SaveUserSettings => { - let save_user_settings: SaveUserSettings = serde_json::from_str(data)?; - let res = Oper::new(user_operation, save_user_settings).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::AddAdmin => { - let add_admin: AddAdmin = serde_json::from_str(data)?; - let res = Oper::new(user_operation, add_admin).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::BanUser => { - let ban_user: BanUser = serde_json::from_str(data)?; - let res = Oper::new(user_operation, ban_user).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::GetReplies => { - let get_replies: GetReplies = serde_json::from_str(data)?; - let res = Oper::new(user_operation, get_replies).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::GetUserMentions => { - let get_user_mentions: GetUserMentions = serde_json::from_str(data)?; - let res = Oper::new(user_operation, get_user_mentions).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::EditUserMention => { - let edit_user_mention: EditUserMention = serde_json::from_str(data)?; - let res = Oper::new(user_operation, edit_user_mention).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::MarkAllAsRead => { - let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?; - let res = Oper::new(user_operation, mark_all_as_read).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::GetCommunity => { - let get_community: GetCommunity = serde_json::from_str(data)?; - let res = Oper::new(user_operation, get_community).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::ListCommunities => { - let list_communities: ListCommunities = serde_json::from_str(data)?; - let res = Oper::new(user_operation, list_communities).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::CreateCommunity => { chat.check_rate_limit_register(msg.id)?; - let create_community: CreateCommunity = serde_json::from_str(data)?; - let res = Oper::new(user_operation, create_community).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::EditCommunity => { let edit_community: EditCommunity = serde_json::from_str(data)?; - let res = Oper::new(user_operation, edit_community).perform(&conn)?; + let res = Oper::new(edit_community).perform(&conn)?; let mut community_sent: CommunityResponse = res.clone(); community_sent.community.user_id = None; community_sent.community.subscribed = None; - let community_sent_str = serde_json::to_string(&community_sent)?; + let community_sent_str = to_json_string(&user_operation, &community_sent)?; chat.send_community_message(community_sent.community.id, &community_sent_str, msg.id)?; - Ok(serde_json::to_string(&res)?) + to_json_string(&user_operation, &res) } UserOperation::FollowCommunity => { - let follow_community: FollowCommunity = serde_json::from_str(data)?; - let res = Oper::new(user_operation, follow_community).perform(&conn)?; - Ok(serde_json::to_string(&res)?) - } - UserOperation::GetFollowedCommunities => { - let followed_communities: GetFollowedCommunities = serde_json::from_str(data)?; - let res = Oper::new(user_operation, followed_communities).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } + UserOperation::GetFollowedCommunities => do_user_operation::< + GetFollowedCommunities, + GetFollowedCommunitiesResponse, + >(user_operation, data, &conn), UserOperation::BanFromCommunity => { let ban_from_community: BanFromCommunity = serde_json::from_str(data)?; let community_id = ban_from_community.community_id; - let res = Oper::new(user_operation, ban_from_community).perform(&conn)?; - let res_str = serde_json::to_string(&res)?; + let res = Oper::new(ban_from_community).perform(&conn)?; + let res_str = to_json_string(&user_operation, &res)?; chat.send_community_message(community_id, &res_str, msg.id)?; Ok(res_str) } UserOperation::AddModToCommunity => { let mod_add_to_community: AddModToCommunity = serde_json::from_str(data)?; let community_id = mod_add_to_community.community_id; - let res = Oper::new(user_operation, mod_add_to_community).perform(&conn)?; - let res_str = serde_json::to_string(&res)?; + let res = Oper::new(mod_add_to_community).perform(&conn)?; + let res_str = to_json_string(&user_operation, &res)?; chat.send_community_message(community_id, &res_str, msg.id)?; Ok(res_str) } UserOperation::ListCategories => { - let list_categories: ListCategories = ListCategories; - let res = Oper::new(user_operation, list_categories).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::CreatePost => { chat.check_rate_limit_post(msg.id)?; - let create_post: CreatePost = serde_json::from_str(data)?; - let res = Oper::new(user_operation, create_post).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::GetPost => { let get_post: GetPost = serde_json::from_str(data)?; chat.join_room(get_post.id, msg.id); - let res = Oper::new(user_operation, get_post).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + let res = Oper::new(get_post).perform(&conn)?; + to_json_string(&user_operation, &res) } UserOperation::GetPosts => { - let get_posts: GetPosts = serde_json::from_str(data)?; - let res = Oper::new(user_operation, get_posts).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::CreatePostLike => { chat.check_rate_limit_message(msg.id)?; - let create_post_like: CreatePostLike = serde_json::from_str(data)?; - let res = Oper::new(user_operation, create_post_like).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::EditPost => { let edit_post: EditPost = serde_json::from_str(data)?; - let res = Oper::new(user_operation, edit_post).perform(&conn)?; + let res = Oper::new(edit_post).perform(&conn)?; let mut post_sent = res.clone(); post_sent.post.my_vote = None; - let post_sent_str = serde_json::to_string(&post_sent)?; + let post_sent_str = to_json_string(&user_operation, &post_sent)?; chat.send_room_message(post_sent.post.id, &post_sent_str, msg.id); - Ok(serde_json::to_string(&res)?) + to_json_string(&user_operation, &res) } UserOperation::SavePost => { - let save_post: SavePost = serde_json::from_str(data)?; - let res = Oper::new(user_operation, save_post).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::CreateComment => { chat.check_rate_limit_message(msg.id)?; let create_comment: CreateComment = serde_json::from_str(data)?; let post_id = create_comment.post_id; - let res = Oper::new(user_operation, create_comment).perform(&conn)?; + let res = Oper::new(create_comment).perform(&conn)?; let mut comment_sent = res.clone(); comment_sent.comment.my_vote = None; comment_sent.comment.user_id = None; - let comment_sent_str = serde_json::to_string(&comment_sent)?; + let comment_sent_str = to_json_string(&user_operation, &comment_sent)?; chat.send_room_message(post_id, &comment_sent_str, msg.id); - Ok(serde_json::to_string(&res)?) + to_json_string(&user_operation, &res) } UserOperation::EditComment => { let edit_comment: EditComment = serde_json::from_str(data)?; let post_id = edit_comment.post_id; - let res = Oper::new(user_operation, edit_comment).perform(&conn)?; + let res = Oper::new(edit_comment).perform(&conn)?; let mut comment_sent = res.clone(); comment_sent.comment.my_vote = None; comment_sent.comment.user_id = None; - let comment_sent_str = serde_json::to_string(&comment_sent)?; + let comment_sent_str = to_json_string(&user_operation, &comment_sent)?; chat.send_room_message(post_id, &comment_sent_str, msg.id); - Ok(serde_json::to_string(&res)?) + to_json_string(&user_operation, &res) } UserOperation::SaveComment => { - let save_comment: SaveComment = serde_json::from_str(data)?; - let res = Oper::new(user_operation, save_comment).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::CreateCommentLike => { chat.check_rate_limit_message(msg.id)?; let create_comment_like: CreateCommentLike = serde_json::from_str(data)?; let post_id = create_comment_like.post_id; - let res = Oper::new(user_operation, create_comment_like).perform(&conn)?; + let res = Oper::new(create_comment_like).perform(&conn)?; let mut comment_sent = res.clone(); comment_sent.comment.my_vote = None; comment_sent.comment.user_id = None; - let comment_sent_str = serde_json::to_string(&comment_sent)?; + let comment_sent_str = to_json_string(&user_operation, &comment_sent)?; chat.send_room_message(post_id, &comment_sent_str, msg.id); - Ok(serde_json::to_string(&res)?) + to_json_string(&user_operation, &res) } UserOperation::GetModlog => { - let get_modlog: GetModlog = serde_json::from_str(data)?; - let res = Oper::new(user_operation, get_modlog).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::CreateSite => { - let create_site: CreateSite = serde_json::from_str(data)?; - let res = Oper::new(user_operation, create_site).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::EditSite => { - let edit_site: EditSite = serde_json::from_str(data)?; - let res = Oper::new(user_operation, edit_site).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::GetSite => { let online: usize = chat.sessions.len(); let get_site: GetSite = serde_json::from_str(data)?; - let mut res = Oper::new(user_operation, get_site).perform(&conn)?; + let mut res = Oper::new(get_site).perform(&conn)?; res.online = online; - Ok(serde_json::to_string(&res)?) + to_json_string(&user_operation, &res) } UserOperation::Search => { - let search: Search = serde_json::from_str(data)?; - let res = Oper::new(user_operation, search).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::TransferCommunity => { - let transfer_community: TransferCommunity = serde_json::from_str(data)?; - let res = Oper::new(user_operation, transfer_community).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::TransferSite => { - let transfer_site: TransferSite = serde_json::from_str(data)?; - let res = Oper::new(user_operation, transfer_site).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::DeleteAccount => { - let delete_account: DeleteAccount = serde_json::from_str(data)?; - let res = Oper::new(user_operation, delete_account).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::PasswordReset => { - let password_reset: PasswordReset = serde_json::from_str(data)?; - let res = Oper::new(user_operation, password_reset).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) } UserOperation::PasswordChange => { - let password_change: PasswordChange = serde_json::from_str(data)?; - let res = Oper::new(user_operation, password_change).perform(&conn)?; - Ok(serde_json::to_string(&res)?) + do_user_operation::(user_operation, data, &conn) + } + UserOperation::CreatePrivateMessage => { + chat.check_rate_limit_message(msg.id)?; + do_user_operation::(user_operation, data, &conn) + } + UserOperation::EditPrivateMessage => { + do_user_operation::(user_operation, data, &conn) + } + UserOperation::GetPrivateMessages => { + do_user_operation::(user_operation, data, &conn) } } } diff --git a/ui/assets/css/toastify.css b/ui/assets/css/toastify.css new file mode 100644 index 00000000000..8804e229698 --- /dev/null +++ b/ui/assets/css/toastify.css @@ -0,0 +1,78 @@ +/*! + * Toastify js 1.6.2 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ + +.toastify { + padding: 12px 20px; + color: #ffffff; + display: inline-block; + box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3); + background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5); + background: linear-gradient(135deg, #73a5ff, #5477f5); + position: fixed; + opacity: 0; + transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); + border-radius: 2px; + cursor: pointer; + text-decoration: none; + max-width: calc(50% - 20px); + z-index: 2147483647; +} + +.toastify.on { + opacity: 1; +} + +.toast-close { + opacity: 0.4; + padding: 0 5px; +} + +.toastify-right { + right: 15px; +} + +.toastify-left { + left: 15px; +} + +.toastify-top { + top: -150px; +} + +.toastify-bottom { + bottom: -150px; +} + +.toastify-rounded { + border-radius: 25px; +} + +.toastify-avatar { + width: 1.5em; + height: 1.5em; + margin: 0 5px; + border-radius: 2px; +} + +.toastify-center { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; +} + +@media only screen and (max-width: 360px) { + .toastify-right, .toastify-left { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + } +} diff --git a/ui/package.json b/ui/package.json index ea6343da9af..41f47088f88 100644 --- a/ui/package.json +++ b/ui/package.json @@ -36,6 +36,7 @@ "prettier": "^1.18.2", "rxjs": "^6.4.0", "terser": "^4.6.0", + "toastify-js": "^1.6.2", "tributejs": "^4.1.1", "twemoji": "^12.1.2", "ws": "^7.0.0" diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index f58168992a2..b8ea0a5a3f5 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -10,12 +10,13 @@ import { } from '../interfaces'; import { Subscription } from 'rxjs'; import { + wsJsonToRes, capitalizeFirstLetter, mentionDropdownFetchLimit, - msgOp, mdToHtml, randomStr, markdownHelpUrl, + toast, } from '../utils'; import { WebSocketService, UserService } from '../services'; import autosize from 'autosize'; @@ -293,7 +294,7 @@ export class CommentForm extends Component { .catch(error => { i.state.imageLoading = false; i.setState(i.state); - alert(error); + toast(error, 'danger'); }); } @@ -311,10 +312,10 @@ export class CommentForm extends Component { this.userSub = WebSocketService.Instance.subject.subscribe( msg => { - let op: UserOperation = msgOp(msg); - if (op == UserOperation.Search) { - let res: SearchResponse = msg; - let users = res.users.map(u => { + let res = wsJsonToRes(msg); + if (res.op == UserOperation.Search) { + let data = res.data as SearchResponse; + let users = data.users.map(u => { return { key: u.name }; }); cb(users); @@ -343,10 +344,10 @@ export class CommentForm extends Component { this.communitySub = WebSocketService.Instance.subject.subscribe( msg => { - let op: UserOperation = msgOp(msg); - if (op == UserOperation.Search) { - let res: SearchResponse = msg; - let communities = res.communities.map(u => { + let res = wsJsonToRes(msg); + if (res.op == UserOperation.Search) { + let data = res.data as SearchResponse; + let communities = data.communities.map(u => { return { key: u.name }; }); cb(communities); diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index baaf63e90e3..046fc88d3f0 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -293,6 +293,16 @@ export class CommentNode extends Component { )} + {!this.myComment && ( +
  • + + {i18n.t('message').toLowerCase()} + +
  • + )}
  • { WebSocketService.Instance.listCommunities(listCommunitiesForm); } - parseMessage(msg: any) { + parseMessage(msg: WebSocketJsonResponse) { console.log(msg); - let op: UserOperation = msgOp(msg); - if (msg.error) { - alert(i18n.t(msg.error)); + let res = wsJsonToRes(msg); + if (res.error) { + toast(i18n.t(msg.error), 'danger'); return; - } else if (op == UserOperation.ListCommunities) { - let res: ListCommunitiesResponse = msg; - this.state.communities = res.communities; + } else if (res.op == UserOperation.ListCommunities) { + let data = res.data as ListCommunitiesResponse; + this.state.communities = data.communities; this.state.communities.sort( (a, b) => b.number_of_subscribers - a.number_of_subscribers ); @@ -248,11 +249,11 @@ export class Communities extends Component { this.setState(this.state); let table = document.querySelector('#community_table'); Sortable.initTable(table); - } else if (op == UserOperation.FollowCommunity) { - let res: CommunityResponse = msg; - let found = this.state.communities.find(c => c.id == res.community.id); - found.subscribed = res.community.subscribed; - found.number_of_subscribers = res.community.number_of_subscribers; + } else if (res.op == UserOperation.FollowCommunity) { + let data = res.data as CommunityResponse; + let found = this.state.communities.find(c => c.id == data.community.id); + found.subscribed = data.community.subscribed; + found.number_of_subscribers = data.community.number_of_subscribers; this.setState(this.state); } } diff --git a/ui/src/components/community-form.tsx b/ui/src/components/community-form.tsx index 2085da2895a..4dc7bfcbb23 100644 --- a/ui/src/components/community-form.tsx +++ b/ui/src/components/community-form.tsx @@ -8,10 +8,11 @@ import { ListCategoriesResponse, CommunityResponse, GetSiteResponse, + WebSocketJsonResponse, } from '../interfaces'; import { WebSocketService } from '../services'; -import { msgOp, capitalizeFirstLetter } from '../utils'; -import * as autosize from 'autosize'; +import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils'; +import autosize from 'autosize'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -67,14 +68,7 @@ export class CommunityForm extends Component< } this.subscription = WebSocketService.Instance.subject - .pipe( - retryWhen(errors => - errors.pipe( - delay(3000), - take(10) - ) - ) - ) + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .subscribe( msg => this.parseMessage(msg), err => console.error(err), @@ -246,34 +240,34 @@ export class CommunityForm extends Component< i.props.onCancel(); } - parseMessage(msg: any) { - let op: UserOperation = msgOp(msg); + parseMessage(msg: WebSocketJsonResponse) { + let res = wsJsonToRes(msg); console.log(msg); - if (msg.error) { - alert(i18n.t(msg.error)); + if (res.error) { + toast(i18n.t(msg.error), 'danger'); this.state.loading = false; this.setState(this.state); return; - } else if (op == UserOperation.ListCategories) { - let res: ListCategoriesResponse = msg; - this.state.categories = res.categories; + } else if (res.op == UserOperation.ListCategories) { + let data = res.data as ListCategoriesResponse; + this.state.categories = data.categories; if (!this.props.community) { - this.state.communityForm.category_id = res.categories[0].id; + this.state.communityForm.category_id = data.categories[0].id; } this.setState(this.state); - } else if (op == UserOperation.CreateCommunity) { - let res: CommunityResponse = msg; + } else if (res.op == UserOperation.CreateCommunity) { + let data = res.data as CommunityResponse; this.state.loading = false; - this.props.onCreate(res.community); + this.props.onCreate(data.community); } - // TODO is ths necessary - else if (op == UserOperation.EditCommunity) { - let res: CommunityResponse = msg; + // TODO is this necessary + else if (res.op == UserOperation.EditCommunity) { + let data = res.data as CommunityResponse; this.state.loading = false; - this.props.onEdit(res.community); - } else if (op == UserOperation.GetSite) { - let res: GetSiteResponse = msg; - this.state.enable_nsfw = res.site.enable_nsfw; + this.props.onEdit(data.community); + } else if (res.op == UserOperation.GetSite) { + let data = res.data as GetSiteResponse; + this.state.enable_nsfw = data.site.enable_nsfw; this.setState(this.state); } } diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 873b5a8a82b..9d02dd86616 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -14,16 +14,18 @@ import { ListingType, GetPostsResponse, CreatePostLikeResponse, + WebSocketJsonResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { PostListings } from './post-listings'; import { SortSelect } from './sort-select'; import { Sidebar } from './sidebar'; import { - msgOp, + wsJsonToRes, routeSortTypeToEnum, fetchLimit, postRefetchSeconds, + toast, } from '../utils'; import { T } from 'inferno-i18next'; import { i18n } from '../i18next'; @@ -253,43 +255,43 @@ export class Community extends Component { WebSocketService.Instance.getPosts(getPostsForm); } - parseMessage(msg: any) { + parseMessage(msg: WebSocketJsonResponse) { console.log(msg); - let op: UserOperation = msgOp(msg); - if (msg.error) { - alert(i18n.t(msg.error)); + let res = wsJsonToRes(msg); + if (res.error) { + toast(i18n.t(msg.error), 'danger'); this.context.router.history.push('/'); return; - } else if (op == UserOperation.GetCommunity) { - let res: GetCommunityResponse = msg; - this.state.community = res.community; - this.state.moderators = res.moderators; - this.state.admins = res.admins; + } else if (res.op == UserOperation.GetCommunity) { + let data = res.data as GetCommunityResponse; + this.state.community = data.community; + this.state.moderators = data.moderators; + this.state.admins = data.admins; document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`; this.setState(this.state); this.keepFetchingPosts(); - } else if (op == UserOperation.EditCommunity) { - let res: CommunityResponse = msg; - this.state.community = res.community; + } else if (res.op == UserOperation.EditCommunity) { + let data = res.data as CommunityResponse; + this.state.community = data.community; this.setState(this.state); - } else if (op == UserOperation.FollowCommunity) { - let res: CommunityResponse = msg; - this.state.community.subscribed = res.community.subscribed; + } else if (res.op == UserOperation.FollowCommunity) { + let data = res.data as CommunityResponse; + this.state.community.subscribed = data.community.subscribed; this.state.community.number_of_subscribers = - res.community.number_of_subscribers; + data.community.number_of_subscribers; this.setState(this.state); - } else if (op == UserOperation.GetPosts) { - let res: GetPostsResponse = msg; - this.state.posts = res.posts; + } else if (res.op == UserOperation.GetPosts) { + let data = res.data as GetPostsResponse; + this.state.posts = data.posts; this.state.loading = false; this.setState(this.state); - } else if (op == UserOperation.CreatePostLike) { - let res: CreatePostLikeResponse = msg; - let found = this.state.posts.find(c => c.id == res.post.id); - found.my_vote = res.post.my_vote; - found.score = res.post.score; - found.upvotes = res.post.upvotes; - found.downvotes = res.post.downvotes; + } else if (res.op == UserOperation.CreatePostLike) { + let data = res.data as CreatePostLikeResponse; + let found = this.state.posts.find(c => c.id == data.post.id); + found.my_vote = data.post.my_vote; + found.score = data.post.score; + found.upvotes = data.post.upvotes; + found.downvotes = data.post.downvotes; this.setState(this.state); } } diff --git a/ui/src/components/create-private-message.tsx b/ui/src/components/create-private-message.tsx new file mode 100644 index 00000000000..7160bc52be6 --- /dev/null +++ b/ui/src/components/create-private-message.tsx @@ -0,0 +1,53 @@ +import { Component } from 'inferno'; +import { PrivateMessageForm } from './private-message-form'; +import { WebSocketService } from '../services'; +import { PrivateMessageFormParams } from '../interfaces'; +import { toast } from '../utils'; +import { i18n } from '../i18next'; + +export class CreatePrivateMessage extends Component { + constructor(props: any, context: any) { + super(props, context); + this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind( + this + ); + } + + componentDidMount() { + document.title = `${i18n.t('create_private_message')} - ${ + WebSocketService.Instance.site.name + }`; + } + + render() { + return ( +
    +
    +
    +
    {i18n.t('create_private_message')}
    + +
    +
    +
    + ); + } + + get params(): PrivateMessageFormParams { + let urlParams = new URLSearchParams(this.props.location.search); + let params: PrivateMessageFormParams = { + recipient_id: Number(urlParams.get('recipient_id')), + }; + + return params; + } + + handlePrivateMessageCreate() { + toast(i18n.t('message_sent')); + + // Navigate to the front + this.props.history.push(`/`); + } +} diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index a302b834590..5c3ff6d2b32 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -12,10 +12,16 @@ import { GetUserMentionsResponse, UserMentionResponse, CommentResponse, + WebSocketJsonResponse, + PrivateMessage as PrivateMessageI, + GetPrivateMessagesForm, + PrivateMessagesResponse, + PrivateMessageResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; -import { msgOp, fetchLimit } from '../utils'; +import { wsJsonToRes, fetchLimit, isCommentType, toast } from '../utils'; import { CommentNodes } from './comment-nodes'; +import { PrivateMessage } from './private-message'; import { SortSelect } from './sort-select'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -26,9 +32,10 @@ enum UnreadOrAll { } enum UnreadType { - Both, + All, Replies, Mentions, + Messages, } interface InboxState { @@ -36,6 +43,7 @@ interface InboxState { unreadType: UnreadType; replies: Array; mentions: Array; + messages: Array; sort: SortType; page: number; } @@ -44,9 +52,10 @@ export class Inbox extends Component { private subscription: Subscription; private emptyState: InboxState = { unreadOrAll: UnreadOrAll.Unread, - unreadType: UnreadType.Both, + unreadType: UnreadType.All, replies: [], mentions: [], + messages: [], sort: SortType.New, page: 1, }; @@ -103,7 +112,10 @@ export class Inbox extends Component { - {this.state.replies.length + this.state.mentions.length > 0 && + {this.state.replies.length + + this.state.mentions.length + + this.state.messages.length > + 0 && this.state.unreadOrAll == UnreadOrAll.Unread && (
    • @@ -114,9 +126,10 @@ export class Inbox extends Component {
    )} {this.selects()} - {this.state.unreadType == UnreadType.Both && this.both()} + {this.state.unreadType == UnreadType.All && this.all()} {this.state.unreadType == UnreadType.Replies && this.replies()} {this.state.unreadType == UnreadType.Mentions && this.mentions()} + {this.state.unreadType == UnreadType.Messages && this.messages()} {this.paginator()} @@ -150,8 +163,8 @@ export class Inbox extends Component { -