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
<!-- translationsstop -->
- [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)
- [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)
Request and response strings are in [JSON format](https://www.json.org).
-### WebSocket Endpoint
+### WebSocket
Connect to <code>ws://***host***/api/v1/ws</code> to get started.
If the ***`host`*** supports secure connections, you can use <code>wss://***host***/api/v1/ws</code>.
-### Testing with Websocat
+#### Testing with Websocat
[Websocat link](https://github.com/vi/websocat)
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
}));
};
```
+### HTTP
+
+Endpoints are at <code>http://***host***/api/v1/***endpoint***</code>. 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
- `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
##### 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
##### Response
```rust
{
- op: String,
- jwt: String
+ op: "Register",
+ data: {
+ jwt: String,
+ }
}
```
+##### HTTP
+
+`POST /user/register`
+
#### Get User Details
##### Request
```rust
##### Response
```rust
{
- op: String,
- user: UserView,
- follows: Vec<CommunityFollowerView>,
- moderates: Vec<CommunityModeratorView>,
- comments: Vec<CommentView>,
- posts: Vec<PostView>,
+ op: "GetUserDetails",
+ data: {
+ user: UserView,
+ follows: Vec<CommunityFollowerView>,
+ moderates: Vec<CommunityModeratorView>,
+ comments: Vec<CommentView>,
+ posts: Vec<PostView>,
+ }
}
```
+##### HTTP
+
+`GET /user`
+
#### Save User Settings
##### Request
```rust
##### Response
```rust
{
- op: String,
- jwt: String
+ op: "SaveUserSettings",
+ data: {
+ jwt: String
+ }
}
```
+##### HTTP
+
+`PUT /save_user_settings`
+
#### Get Replies / Inbox
##### Request
```rust
##### Response
```rust
{
- op: String,
- replies: Vec<ReplyView>,
+ op: "GetReplies",
+ data: {
+ replies: Vec<ReplyView>,
+ }
}
```
+##### HTTP
+
+`GET /user/replies`
+
#### Get User Mentions
##### Request
##### Response
```rust
{
- op: String,
- mentions: Vec<UserMentionView>,
+ op: "GetUserMentions",
+ data: {
+ mentions: Vec<UserMentionView>,
+ }
+}
+```
+
+##### HTTP
+
+`GET /user/mentions`
+
+#### Edit User Mention
+##### Request
+```rust
+{
+ op: "EditUserMention",
+ data: {
+ user_mention_id: i32,
+ read: Option<bool>,
+ auth: String,
+ }
+}
+```
+##### Response
+```rust
+{
+ op: "EditUserMention",
+ data: {
+ mention: UserMentionView,
+ }
}
```
+##### HTTP
+
+`PUT /user/mention`
#### Mark All As Read
##### Response
```rust
{
- op: String,
- replies: Vec<ReplyView>,
+ op: "MarkAllAsRead",
+ data: {
+ replies: Vec<ReplyView>,
+ }
}
```
+##### HTTP
+
+`POST /user/mark_all_as_read`
+
#### Delete Account
*Permananently deletes your posts and comments*
##### Response
```rust
{
- op: String,
- jwt: String,
+ op: "DeleteAccount",
+ data: {
+ jwt: String,
+ }
}
```
+##### HTTP
+
+`POST /user/delete_account`
+
#### Add admin
##### Request
```rust
##### Response
```rust
{
- op: String,
- admins: Vec<UserView>,
+ op: "AddAdmin",
+ data: {
+ admins: Vec<UserView>,
+ }
}
```
+##### HTTP
+
+`POST /admin/add`
#### Ban user
##### Request
##### Response
```rust
{
- op: String,
- user: UserView,
- banned: bool,
+ op: "BanUser",
+ data: {
+ user: UserView,
+ banned: bool,
+ }
}
```
+##### HTTP
+
+`POST /user/ban`
### Site
#### List Categories
##### Response
```rust
{
- op: String,
- categories: Vec<Category>
+ op: "ListCategories",
+ data: {
+ categories: Vec<Category>
+ }
}
```
+##### HTTP
+
+`GET /categories`
#### Search
-Search types are `Both, Comments, Posts`.
+
+Search types are `All, Comments, Posts, Communities, Users, Url`
##### Request
```rust
sort: String,
page: Option<i64>,
limit: Option<i64>,
+ auth?: Option<String>,
}
}
```
##### Response
```rust
{
- op: String,
- comments: Vec<CommentView>,
- posts: Vec<PostView>,
+ op: "Search",
+ data: {
+ type_: String,
+ comments: Vec<CommentView>,
+ posts: Vec<PostView>,
+ communities: Vec<CommunityView>,
+ users: Vec<UserView>,
+ }
}
```
+##### HTTP
+
+`POST /search`
#### Get Modlog
##### Request
##### Response
```rust
{
- op: String,
- removed_posts: Vec<ModRemovePostView>,
- locked_posts: Vec<ModLockPostView>,
- removed_comments: Vec<ModRemoveCommentView>,
- removed_communities: Vec<ModRemoveCommunityView>,
- banned_from_community: Vec<ModBanFromCommunityView>,
- banned: Vec<ModBanView>,
- added_to_community: Vec<ModAddCommunityView>,
- added: Vec<ModAddView>,
+ op: "GetModlog",
+ data: {
+ removed_posts: Vec<ModRemovePostView>,
+ locked_posts: Vec<ModLockPostView>,
+ removed_comments: Vec<ModRemoveCommentView>,
+ removed_communities: Vec<ModRemoveCommunityView>,
+ banned_from_community: Vec<ModBanFromCommunityView>,
+ banned: Vec<ModBanView>,
+ added_to_community: Vec<ModAddCommunityView>,
+ added: Vec<ModAddView>,
+ }
}
```
+##### HTTP
+
+`GET /modlog`
+
#### Create Site
##### Request
```rust
##### Response
```rust
{
- op: String,
- site: SiteView,
+ op: "CreateSite",
+ data: {
+ site: SiteView,
+ }
}
```
+##### HTTP
+
+`POST /site`
+
#### Edit Site
##### Request
```rust
##### Response
```rust
{
- op: String,
- site: SiteView,
+ op: "EditSite",
+ data: {
+ site: SiteView,
+ }
}
```
+##### HTTP
+
+`PUT /site`
#### Get Site
##### Request
##### Response
```rust
{
- op: String,
- site: Option<SiteView>,
- admins: Vec<UserView>,
- banned: Vec<UserView>,
+ op: "GetSite",
+ data: {
+ site: Option<SiteView>,
+ admins: Vec<UserView>,
+ banned: Vec<UserView>,
+ }
}
```
+##### HTTP
+
+`GET /site`
#### Transfer Site
##### Request
##### Response
```rust
{
- op: String,
- site: Option<SiteView>,
- admins: Vec<UserView>,
- banned: Vec<UserView>,
+ op: "TransferSite",
+ data: {
+ site: Option<SiteView>,
+ admins: Vec<UserView>,
+ banned: Vec<UserView>,
+ }
}
```
+##### HTTP
+
+`POST /site/transfer`
### Community
#### Get Community
##### Response
```rust
{
- op: String,
- community: CommunityView,
- moderators: Vec<CommunityModeratorView>,
- admins: Vec<UserView>,
+ op: "GetCommunity",
+ data: {
+ community: CommunityView,
+ moderators: Vec<CommunityModeratorView>,
+ admins: Vec<UserView>,
+ }
}
```
+##### HTTP
+
+`GET /community`
#### Create Community
##### Request
##### Response
```rust
{
- op: String,
- community: CommunityView
+ op: "CreateCommunity",
+ data: {
+ community: CommunityView
+ }
}
```
+##### HTTP
+
+`POST /community`
#### List Communities
##### Request
##### Response
```rust
{
- op: String,
- communities: Vec<CommunityView>
+ op: "ListCommunities",
+ data: {
+ communities: Vec<CommunityView>
+ }
}
```
+##### HTTP
+
+`GET /community/list`
#### Ban from Community
##### Request
##### 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
##### Response
```rust
{
- op: String,
- moderators: Vec<CommunityModeratorView>,
+ op: "AddModToCommunity",
+ data: {
+ moderators: Vec<CommunityModeratorView>,
+ }
}
```
+##### HTTP
+
+`POST /community/mod`
#### Edit Community
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
##### Response
```rust
{
- op: String,
- community: CommunityView
+ op: "FollowCommunity",
+ data: {
+ community: CommunityView
+ }
}
```
+##### HTTP
+
+`POST /community/follow`
#### Get Followed Communities
##### Request
##### Response
```rust
{
- op: String,
- communities: Vec<CommunityFollowerView>
+ op: "GetFollowedCommunities",
+ data: {
+ communities: Vec<CommunityFollowerView>
+ }
}
```
+##### HTTP
+
+`GET /user/followed_communities`
#### Transfer Community
##### Request
##### Response
```rust
{
- op: String,
- community: CommunityView,
- moderators: Vec<CommunityModeratorView>,
- admins: Vec<UserView>,
+ op: "TransferCommunity",
+ data: {
+ community: CommunityView,
+ moderators: Vec<CommunityModeratorView>,
+ admins: Vec<UserView>,
+ }
}
```
+##### HTTP
+
+`POST /community/transfer`
### Post
#### Create Post
##### Response
```rust
{
- op: String,
- post: PostView
+ op: "CreatePost",
+ data: {
+ post: PostView
+ }
}
```
+##### HTTP
+
+`POST /post`
#### Get Post
##### Request
##### Response
```rust
{
- op: String,
- post: PostView,
- comments: Vec<CommentView>,
- community: CommunityView,
- moderators: Vec<CommunityModeratorView>,
- admins: Vec<UserView>,
+ op: "GetPost",
+ data: {
+ post: PostView,
+ comments: Vec<CommentView>,
+ community: CommunityView,
+ moderators: Vec<CommunityModeratorView>,
+ admins: Vec<UserView>,
+ }
}
```
+##### HTTP
+
+`GET /post`
#### Get Posts
+
Post listing types are `All, Subscribed, Community`
##### Request
##### Response
```rust
{
- op: String,
- posts: Vec<PostView>,
+ op: "GetPosts",
+ data: {
+ posts: Vec<PostView>,
+ }
}
```
+##### HTTP
+
+`GET /post/list`
#### Create Post Like
+
`score` can be 0, -1, or 1
##### Request
##### 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
##### Response
```rust
{
- op: String,
- post: PostView
+ op: "EditPost",
+ data: {
+ post: PostView
+ }
}
```
+##### HTTP
+
+`PUT /post`
+
#### Save Post
##### Request
```rust
##### Response
```rust
{
- op: String,
- post: PostView
+ op: "SavePost",
+ data: {
+ post: PostView
+ }
}
```
+##### HTTP
+
+`POST /post/save`
### Comment
#### Create Comment
##### 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
##### Response
```rust
{
- op: String,
- comment: CommentView
+ op: "EditComment",
+ data: {
+ comment: CommentView
+ }
}
```
+##### HTTP
+
+`PUT /comment`
#### Save Comment
##### Request
##### 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
##### Response
```rust
{
- op: String,
- comment: CommentView
+ op: "CreateCommentLike",
+ data: {
+ comment: CommentView
+ }
}
```
+##### HTTP
+
+`POST /comment/like`
### RSS / Atom feeds
tab_spaces = 2
-edition="2018"
+edition="2018"
\ No newline at end of file
--- /dev/null
+-- 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;
--- /dev/null
+-- 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)
+-- )
pub struct CreateComment {
content: String,
parent_id: Option<i32>,
- edit_id: Option<i32>,
+ edit_id: Option<i32>, // TODO this isn't used
pub post_id: i32,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct EditComment {
content: String,
- parent_id: Option<i32>,
+ parent_id: Option<i32>, // 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,
#[derive(Serialize, Deserialize, Clone)]
pub struct CommentResponse {
- op: String,
pub comment: CommentView,
}
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 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());
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
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,
})
}
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;
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());
}
}
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
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
Ok(CommentResponse {
- op: self.op.to_string(),
comment: comment_view,
})
}
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;
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,
})
}
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;
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 {
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()),
};
}
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
Ok(CommentResponse {
- op: self.op.to_string(),
comment: liked_comment,
})
}
#[derive(Serialize, Deserialize)]
pub struct GetCommunityResponse {
- op: String,
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
#[derive(Serialize, Deserialize, Clone)]
pub struct CommunityResponse {
- op: String,
pub community: CommunityView,
}
#[derive(Serialize, Deserialize)]
pub struct ListCommunitiesResponse {
- op: String,
communities: Vec<CommunityView>,
}
#[derive(Serialize, Deserialize)]
pub struct BanFromCommunityResponse {
- op: String,
user: UserView,
banned: bool,
}
#[derive(Serialize, Deserialize)]
pub struct AddModToCommunityResponse {
- op: String,
moderators: Vec<CommunityModeratorView>,
}
#[derive(Serialize, Deserialize)]
pub struct GetFollowedCommunitiesResponse {
- op: String,
communities: Vec<CommunityFollowerView>,
}
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;
// Return the jwt
Ok(GetCommunityResponse {
- op: self.op.to_string(),
community: community_view,
moderators,
admins,
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
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 {
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 {
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,
})
}
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
);
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 {
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
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
Ok(CommunityResponse {
- op: self.op.to_string(),
community: community_view,
})
}
.list()?;
// Return the jwt
- Ok(ListCommunitiesResponse {
- op: self.op.to_string(),
- communities,
- })
+ Ok(ListCommunitiesResponse { communities })
}
}
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;
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,
})
}
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;
let communities: Vec<CommunityFollowerView> =
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 })
}
}
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;
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()),
};
}
let user_view = UserView::read(&conn, data.user_id)?;
Ok(BanFromCommunityResponse {
- op: self.op.to_string(),
user: user_view,
banned: data.ban,
})
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;
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()),
};
}
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
- Ok(AddModToCommunityResponse {
- op: self.op.to_string(),
- moderators,
- })
+ Ok(AddModToCommunityResponse { moderators })
}
}
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 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 {
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.
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_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,
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::*;
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<T> {
- op: UserOperation,
data: T,
}
impl<T> Oper<T> {
- pub fn new(op: UserOperation, data: T) -> Oper<T> {
- Oper { op, data }
+ pub fn new(data: T) -> Oper<T> {
+ Oper { data }
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct PostResponse {
- op: String,
pub post: PostView,
}
#[derive(Serialize, Deserialize)]
pub struct GetPostResponse {
- op: String,
post: PostView,
comments: Vec<CommentView>,
community: CommunityView,
#[derive(Serialize, Deserialize)]
pub struct GetPostsResponse {
- op: String,
posts: Vec<PostView>,
}
#[derive(Serialize, Deserialize)]
pub struct CreatePostLikeResponse {
- op: String,
post: PostView,
}
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 {
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
// 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 })
}
}
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)
// Return the jwt
Ok(GetPostResponse {
- op: self.op.to_string(),
post: post_view,
comments,
community,
.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 })
}
}
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;
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 {
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 })
}
}
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
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;
);
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 {
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
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 })
}
}
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;
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 })
}
}
#[derive(Serialize, Deserialize)]
pub struct ListCategoriesResponse {
- op: String,
categories: Vec<Category>,
}
#[derive(Serialize, Deserialize)]
pub struct SearchResponse {
- op: String,
type_: String,
comments: Vec<CommentView>,
posts: Vec<PostView>,
#[derive(Serialize, Deserialize)]
pub struct GetModlogResponse {
- op: String,
removed_posts: Vec<ModRemovePostView>,
locked_posts: Vec<ModLockPostView>,
stickied_posts: Vec<ModStickyPostView>,
#[derive(Serialize, Deserialize)]
pub struct SiteResponse {
- op: String,
site: SiteView,
}
#[derive(Serialize, Deserialize)]
pub struct GetSiteResponse {
- op: String,
site: Option<SiteView>,
admins: Vec<UserView>,
banned: Vec<UserView>,
let categories: Vec<Category> = Category::list_all(&conn)?;
// Return the jwt
- Ok(ListCategoriesResponse {
- op: self.op.to_string(),
- categories,
- })
+ Ok(ListCategoriesResponse { categories })
}
}
// Return the jwt
Ok(GetModlogResponse {
- op: self.op.to_string(),
removed_posts,
locked_posts,
stickied_posts,
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 {
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 })
}
}
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)?;
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 })
}
}
let banned = UserView::banned(&conn)?;
Ok(GetSiteResponse {
- op: self.op.to_string(),
site: site_view,
admins,
banned,
// Return the jwt
Ok(SearchResponse {
- op: self.op.to_string(),
type_: data.type_.to_owned(),
comments,
posts,
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 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 {
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
let banned = UserView::banned(&conn)?;
Ok(GetSiteResponse {
- op: self.op.to_string(),
site: Some(site_view),
admins,
banned,
lang: String,
avatar: Option<String>,
email: Option<String>,
+ matrix_user_id: Option<String>,
new_password: Option<String>,
new_password_verify: Option<String>,
old_password: Option<String>,
#[derive(Serialize, Deserialize)]
pub struct LoginResponse {
- op: String,
jwt: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetUserDetailsResponse {
- op: String,
user: UserView,
follows: Vec<CommunityFollowerView>,
moderates: Vec<CommunityModeratorView>,
#[derive(Serialize, Deserialize)]
pub struct GetRepliesResponse {
- op: String,
replies: Vec<ReplyView>,
}
#[derive(Serialize, Deserialize)]
pub struct GetUserMentionsResponse {
- op: String,
mentions: Vec<UserMentionView>,
}
#[derive(Serialize, Deserialize)]
pub struct AddAdminResponse {
- op: String,
admins: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
pub struct BanUserResponse {
- op: String,
user: UserView,
banned: bool,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct UserMentionResponse {
- op: String,
mention: UserMentionView,
}
}
#[derive(Serialize, Deserialize, Clone)]
-pub struct PasswordResetResponse {
- op: String,
-}
+pub struct PasswordResetResponse {}
#[derive(Serialize, Deserialize)]
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<String>,
+ deleted: Option<bool>,
+ read: Option<bool>,
+ auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetPrivateMessages {
+ unread_only: bool,
+ page: Option<i64>,
+ limit: Option<i64>,
+ auth: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct PrivateMessagesResponse {
+ messages: Vec<PrivateMessageView>,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct PrivateMessageResponse {
+ message: PrivateMessageView,
+}
+
impl Perform<LoginResponse> for Oper<Login> {
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
let data: &Login = &self.data;
// 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() })
}
}
// 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
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,
"user_already_exists"
};
- return Err(APIError::err(&self.op, err_type).into());
+ return Err(APIError::err(err_type).into());
}
};
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
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(),
})
}
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;
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
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,
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,
"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(),
})
}
.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()),
}
}
};
// Return the jwt
Ok(GetUserDetailsResponse {
- op: self.op.to_string(),
user: user_view,
follows,
moderates,
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,
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
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
- Ok(AddAdminResponse {
- op: self.op.to_string(),
- admins,
- })
+ Ok(AddAdminResponse { admins })
}
}
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,
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
let user_view = UserView::read(&conn, data.user_id)?;
Ok(BanUserResponse {
- op: self.op.to_string(),
user: user_view,
banned: data.ban,
})
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;
.limit(data.limit)
.list()?;
- Ok(GetRepliesResponse {
- op: self.op.to_string(),
- replies,
- })
+ Ok(GetRepliesResponse { replies })
}
}
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;
.limit(data.limit)
.list()?;
- Ok(GetUserMentionsResponse {
- op: self.op.to_string(),
- mentions,
- })
+ Ok(GetUserMentionsResponse { mentions })
}
}
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;
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,
})
}
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;
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()),
};
}
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![] })
}
}
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;
// 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
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()),
};
}
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(),
})
}
// 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
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", 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 {})
}
}
// 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<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
+ fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
+ 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!(
+ "<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+ 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<PrivateMessageResponse> for Oper<EditPrivateMessage> {
+ fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
+ 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<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
+ fn perform(&self, conn: &PgConnection) -> Result<PrivateMessagesResponse, Error> {
+ 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 })
+ }
+}
preferred_username: None,
password_encrypted: "here".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
published: naive_now(),
admin: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
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;
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
updated: None,
admin: false,
--- /dev/null
+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<chrono::NaiveDateTime>,
+}
+
+#[derive(Insertable, AsChangeset, Clone)]
+#[table_name = "private_message"]
+pub struct PrivateMessageForm {
+ pub creator_id: i32,
+ pub recipient_id: i32,
+ pub content: Option<String>,
+ pub deleted: Option<bool>,
+ pub read: Option<bool>,
+ pub updated: Option<chrono::NaiveDateTime>,
+}
+
+impl Crud<PrivateMessageForm> for PrivateMessage {
+ fn read(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
+ use crate::schema::private_message::dsl::*;
+ private_message.find(private_message_id).first::<Self>(conn)
+ }
+
+ fn delete(conn: &PgConnection, private_message_id: i32) -> Result<usize, Error> {
+ 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<Self, Error> {
+ use crate::schema::private_message::dsl::*;
+ insert_into(private_message)
+ .values(private_message_form)
+ .get_result::<Self>(conn)
+ }
+
+ fn update(
+ conn: &PgConnection,
+ private_message_id: i32,
+ private_message_form: &PrivateMessageForm,
+ ) -> Result<Self, Error> {
+ use crate::schema::private_message::dsl::*;
+ diesel::update(private_message.find(private_message_id))
+ .set(private_message_form)
+ .get_result::<Self>(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);
+ }
+}
--- /dev/null
+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<Timestamp>,
+ creator_name -> Varchar,
+ creator_avatar -> Nullable<Text>,
+ recipient_name -> Varchar,
+ recipient_avatar -> Nullable<Text>,
+ }
+}
+
+table! {
+ private_message_mview (id) {
+ id -> Int4,
+ creator_id -> Int4,
+ recipient_id -> Int4,
+ content -> Text,
+ deleted -> Bool,
+ read -> Bool,
+ published -> Timestamp,
+ updated -> Nullable<Timestamp>,
+ creator_name -> Varchar,
+ creator_avatar -> Nullable<Text>,
+ recipient_name -> Varchar,
+ recipient_avatar -> Nullable<Text>,
+ }
+}
+
+#[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<chrono::NaiveDateTime>,
+ pub creator_name: String,
+ pub creator_avatar: Option<String>,
+ pub recipient_name: String,
+ pub recipient_avatar: Option<String>,
+}
+
+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<i64>,
+ limit: Option<i64>,
+}
+
+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<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
+ self.page = page.get_optional();
+ self
+ }
+
+ pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
+ self.limit = limit.get_optional();
+ self
+ }
+
+ pub fn list(self) -> Result<Vec<PrivateMessageView>, 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::<PrivateMessageView>(self.conn)
+ }
+}
+
+impl PrivateMessageView {
+ pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result<Self, Error> {
+ 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::<Self>(conn)
+ }
+}
pub lang: String,
pub show_avatars: bool,
pub send_notifications_to_email: bool,
+ pub matrix_user_id: Option<String>,
}
#[derive(Insertable, AsChangeset, Clone)]
pub lang: String,
pub show_avatars: bool,
pub send_notifications_to_email: bool,
+ pub matrix_user_id: Option<String>,
}
impl Crud<UserForm> for User_ {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
+ matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
+ matrix_user_id -> Nullable<Text>,
fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
+ matrix_user_id -> Nullable<Text>,
fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
pub name: String,
pub avatar: Option<String>,
pub email: Option<String>,
+ pub matrix_user_id: Option<String>,
pub fedi_name: String,
pub admin: bool,
pub banned: bool,
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(),
let result = mailer.send(email.into());
+ mailer.close();
+
match result {
Ok(_) => Ok(()),
Err(_) => Err("no_email_setup".to_string()),
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;
.data(pool.clone())
.data(server.clone())
// The routes
+ .configure(api::config)
.configure(federation::config)
.configure(feeds::config)
.configure(index::config)
--- /dev/null
+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<Pool<ConnectionManager<PgConnection>>>;
+
+#[rustfmt::skip]
+pub fn config(cfg: &mut web::ServiceConfig) {
+ cfg
+ // Site
+ .route("/api/v1/site", web::get().to(route_get::<GetSite, GetSiteResponse>))
+ .route("/api/v1/categories", web::get().to(route_get::<ListCategories, ListCategoriesResponse>))
+ .route("/api/v1/modlog", web::get().to(route_get::<GetModlog, GetModlogResponse>))
+ .route("/api/v1/search", web::get().to(route_get::<Search, SearchResponse>))
+ // Community
+ .route("/api/v1/community", web::post().to(route_post::<CreateCommunity, CommunityResponse>))
+ .route("/api/v1/community", web::get().to(route_get::<GetCommunity, GetCommunityResponse>))
+ .route("/api/v1/community", web::put().to(route_post::<EditCommunity, CommunityResponse>))
+ .route("/api/v1/community/list", web::get().to(route_get::<ListCommunities, ListCommunitiesResponse>))
+ .route("/api/v1/community/follow", web::post().to(route_post::<FollowCommunity, CommunityResponse>))
+ // Post
+ .route("/api/v1/post", web::post().to(route_post::<CreatePost, PostResponse>))
+ .route("/api/v1/post", web::put().to(route_post::<EditPost, PostResponse>))
+ .route("/api/v1/post", web::get().to(route_get::<GetPost, GetPostResponse>))
+ .route("/api/v1/post/list", web::get().to(route_get::<GetPosts, GetPostsResponse>))
+ .route("/api/v1/post/like", web::post().to(route_post::<CreatePostLike, CreatePostLikeResponse>))
+ .route("/api/v1/post/save", web::put().to(route_post::<SavePost, PostResponse>))
+ // Comment
+ .route("/api/v1/comment", web::post().to(route_post::<CreateComment, CommentResponse>))
+ .route("/api/v1/comment", web::put().to(route_post::<EditComment, CommentResponse>))
+ .route("/api/v1/comment/like", web::post().to(route_post::<CreateCommentLike, CommentResponse>))
+ .route("/api/v1/comment/save", web::put().to(route_post::<SaveComment, CommentResponse>))
+ // User
+ .route("/api/v1/user", web::get().to(route_get::<GetUserDetails, GetUserDetailsResponse>))
+ .route("/api/v1/user/mention", web::get().to(route_get::<GetUserMentions, GetUserMentionsResponse>))
+ .route("/api/v1/user/mention", web::put().to(route_post::<EditUserMention, UserMentionResponse>))
+ .route("/api/v1/user/replies", web::get().to(route_get::<GetReplies, GetRepliesResponse>))
+ .route("/api/v1/user/followed_communities", web::get().to(route_get::<GetFollowedCommunities, GetFollowedCommunitiesResponse>))
+ // Mod actions
+ .route("/api/v1/community/transfer", web::post().to(route_post::<TransferCommunity, GetCommunityResponse>))
+ .route("/api/v1/community/ban_user", web::post().to(route_post::<BanFromCommunity, BanFromCommunityResponse>))
+ .route("/api/v1/community/mod", web::post().to(route_post::<AddModToCommunity, AddModToCommunityResponse>))
+ // Admin actions
+ .route("/api/v1/site", web::post().to(route_post::<CreateSite, SiteResponse>))
+ .route("/api/v1/site", web::put().to(route_post::<EditSite, SiteResponse>))
+ .route("/api/v1/site/transfer", web::post().to(route_post::<TransferSite, GetSiteResponse>))
+ .route("/api/v1/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
+ .route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
+ // User account actions
+ .route("/api/v1/user/login", web::post().to(route_post::<Login, LoginResponse>))
+ .route("/api/v1/user/register", web::post().to(route_post::<Register, LoginResponse>))
+ .route("/api/v1/user/delete_account", web::post().to(route_post::<DeleteAccount, LoginResponse>))
+ .route("/api/v1/user/password_reset", web::post().to(route_post::<PasswordReset, PasswordResetResponse>))
+ .route("/api/v1/user/password_change", web::post().to(route_post::<PasswordChange, LoginResponse>))
+ .route("/api/v1/user/mark_all_as_read", web::post().to(route_post::<MarkAllAsRead, GetRepliesResponse>))
+ .route("/api/v1/user/save_user_settings", web::put().to(route_post::<SaveUserSettings, LoginResponse>));
+}
+
+fn perform<Request, Response>(data: Request, db: DbParam) -> Result<HttpResponse, Error>
+where
+ Response: Serialize,
+ Oper<Request>: Perform<Response>,
+{
+ let conn = match db.get() {
+ Ok(c) => c,
+ Err(e) => return Err(format_err!("{}", e)),
+ };
+ let oper: Oper<Request> = Oper::new(data);
+ let response = oper.perform(&conn);
+ Ok(HttpResponse::Ok().json(response?))
+}
+
+async fn route_get<Data, Response>(
+ data: web::Query<Data>,
+ db: DbParam,
+) -> Result<HttpResponse, Error>
+where
+ Data: Serialize,
+ Response: Serialize,
+ Oper<Data>: Perform<Response>,
+{
+ perform::<Data, Response>(data.0, db)
+}
+
+async fn route_post<Data, Response>(
+ data: web::Json<Data>,
+ db: DbParam,
+) -> Result<HttpResponse, Error>
+where
+ Data: Serialize,
+ Response: Serialize,
+ Oper<Data>: Perform<Response>,
+{
+ perform::<Data, Response>(data.0, db)
+}
.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))
+pub mod api;
pub mod federation;
pub mod feeds;
pub mod index;
}
}
+table! {
+ private_message (id) {
+ id -> Int4,
+ creator_id -> Int4,
+ recipient_id -> Int4,
+ content -> Text,
+ deleted -> Bool,
+ read -> Bool,
+ published -> Timestamp,
+ updated -> Nullable<Timestamp>,
+ }
+}
+
table! {
site (id) {
id -> Int4,
lang -> Varchar,
show_avatars -> Bool,
send_notifications_to_email -> Bool,
+ matrix_user_id -> Nullable<Text>,
}
}
post_like,
post_read,
post_saved,
+ private_message,
site,
user_,
user_ban,
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,
+}
//! 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};
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
);
Err(
APIError {
- op: "Rate Limit".to_string(),
message: format!("Too many requests. {} per {} seconds", rate, per),
}
.into(),
}
}
+#[derive(Serialize)]
+struct WebsocketResponse<T> {
+ op: String,
+ data: T,
+}
+
+fn to_json_string<T>(op: &UserOperation, data: T) -> Result<String, Error>
+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<ConnectionManager<PgConnection>>,
+) -> Result<String, Error>
+where
+ for<'de> Data: Deserialize<'de> + 'a,
+ Response: Serialize,
+ Oper<Data>: Perform<Response>,
+{
+ 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<String, Error> {
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(),
})?;
let user_operation: UserOperation = UserOperation::from_str(&op)?;
+ // TODO: none of the chat messages are going to work if stuff is submitted via http api,
+ // need to move that handling elsewhere
match user_operation {
- UserOperation::Login => {
- 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::<Login, LoginResponse>(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::<Register, LoginResponse>(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::<GetUserDetails, GetUserDetailsResponse>(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::<SaveUserSettings, LoginResponse>(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::<AddAdmin, AddAdminResponse>(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::<BanUser, BanUserResponse>(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::<GetReplies, GetRepliesResponse>(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::<GetUserMentions, GetUserMentionsResponse>(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::<EditUserMention, UserMentionResponse>(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::<MarkAllAsRead, GetRepliesResponse>(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::<GetCommunity, GetCommunityResponse>(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::<ListCommunities, ListCommunitiesResponse>(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::<CreateCommunity, CommunityResponse>(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::<FollowCommunity, CommunityResponse>(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::<ListCategories, ListCategoriesResponse>(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::<CreatePost, PostResponse>(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::<GetPosts, GetPostsResponse>(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::<CreatePostLike, CreatePostLikeResponse>(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::<SavePost, PostResponse>(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::<SaveComment, CommentResponse>(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::<GetModlog, GetModlogResponse>(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::<CreateSite, SiteResponse>(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::<EditSite, SiteResponse>(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::<Search, SearchResponse>(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::<TransferCommunity, GetCommunityResponse>(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::<TransferSite, GetSiteResponse>(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::<DeleteAccount, LoginResponse>(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::<PasswordReset, PasswordResetResponse>(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::<PasswordChange, LoginResponse>(user_operation, data, &conn)
+ }
+ UserOperation::CreatePrivateMessage => {
+ chat.check_rate_limit_message(msg.id)?;
+ do_user_operation::<CreatePrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
+ }
+ UserOperation::EditPrivateMessage => {
+ do_user_operation::<EditPrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
+ }
+ UserOperation::GetPrivateMessages => {
+ do_user_operation::<GetPrivateMessages, PrivateMessagesResponse>(user_operation, data, &conn)
}
}
}
--- /dev/null
+/*!
+ * 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;
+ }
+}
"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"
} 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';
.catch(error => {
i.state.imageLoading = false;
i.setState(i.state);
- alert(error);
+ toast(error, 'danger');
});
}
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);
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);
</li>
</>
)}
+ {!this.myComment && (
+ <li className="list-inline-item">
+ <Link
+ class="text-muted"
+ to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
+ >
+ {i18n.t('message').toLowerCase()}
+ </Link>
+ </li>
+ )}
<li className="list-inline-item">•</li>
<li className="list-inline-item">
<span
FollowCommunityForm,
ListCommunitiesForm,
SortType,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
-import { msgOp } from '../utils';
+import { wsJsonToRes, toast } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
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
);
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);
}
}
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';
}
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),
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);
}
}
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';
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);
}
}
--- /dev/null
+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<any, any> {
+ 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 (
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t('create_private_message')}</h5>
+ <PrivateMessageForm
+ onCreate={this.handlePrivateMessageCreate}
+ params={this.params}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ 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(`/`);
+ }
+}
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';
}
enum UnreadType {
- Both,
+ All,
Replies,
Mentions,
+ Messages,
}
interface InboxState {
unreadType: UnreadType;
replies: Array<Comment>;
mentions: Array<Comment>;
+ messages: Array<PrivateMessageI>;
sort: SortType;
page: number;
}
private subscription: Subscription;
private emptyState: InboxState = {
unreadOrAll: UnreadOrAll.Unread,
- unreadType: UnreadType.Both,
+ unreadType: UnreadType.All,
replies: [],
mentions: [],
+ messages: [],
sort: SortType.New,
page: 1,
};
</a>
</small>
</h5>
- {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 && (
<ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item">
</ul>
)}
{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()}
</div>
</div>
<option disabled>
<T i18nKey="type">#</T>
</option>
- <option value={UnreadType.Both}>
- <T i18nKey="both">#</T>
+ <option value={UnreadType.All}>
+ <T i18nKey="all">#</T>
</option>
<option value={UnreadType.Replies}>
<T i18nKey="replies">#</T>
<option value={UnreadType.Mentions}>
<T i18nKey="mentions">#</T>
</option>
+ <option value={UnreadType.Messages}>
+ <T i18nKey="messages">#</T>
+ </option>
</select>
<SortSelect
sort={this.state.sort}
);
}
- both() {
- let combined: Array<{
- type_: string;
- data: Comment;
- }> = [];
- let replies = this.state.replies.map(e => {
- return { type_: 'replies', data: e };
- });
- let mentions = this.state.mentions.map(e => {
- return { type_: 'mentions', data: e };
- });
+ all() {
+ let combined: Array<Comment | PrivateMessageI> = [];
- combined.push(...replies);
- combined.push(...mentions);
+ combined.push(...this.state.replies);
+ combined.push(...this.state.mentions);
+ combined.push(...this.state.messages);
// Sort it
- if (this.state.sort == SortType.New) {
- combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
- } else {
- combined.sort((a, b) => b.data.score - a.data.score);
- }
+ combined.sort((a, b) => b.published.localeCompare(a.published));
return (
<div>
- {combined.map(i => (
- <CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
- ))}
+ {combined.map(i =>
+ isCommentType(i) ? (
+ <CommentNodes nodes={[{ comment: i }]} noIndent markable />
+ ) : (
+ <PrivateMessage privateMessage={i} />
+ )
+ )}
</div>
);
}
);
}
+ messages() {
+ return (
+ <div>
+ {this.state.messages.map(message => (
+ <PrivateMessage privateMessage={message} />
+ ))}
+ </div>
+ );
+ }
+
paginator() {
return (
<div class="mt-2">
limit: fetchLimit,
};
WebSocketService.Instance.getUserMentions(userMentionsForm);
+
+ let privateMessagesForm: GetPrivateMessagesForm = {
+ unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
+ page: this.state.page,
+ limit: fetchLimit,
+ };
+ WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
}
handleSortChange(val: SortType) {
WebSocketService.Instance.markAllAsRead();
}
- 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.GetReplies) {
- let res: GetRepliesResponse = msg;
- this.state.replies = res.replies;
+ } else if (res.op == UserOperation.GetReplies) {
+ let data = res.data as GetRepliesResponse;
+ this.state.replies = data.replies;
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetUserMentions) {
+ let data = res.data as GetUserMentionsResponse;
+ this.state.mentions = data.mentions;
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetPrivateMessages) {
+ let data = res.data as PrivateMessagesResponse;
+ this.state.messages = data.messages;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
- } else if (op == UserOperation.GetUserMentions) {
- let res: GetUserMentionsResponse = msg;
- this.state.mentions = res.mentions;
+ } else if (res.op == UserOperation.EditPrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+ let found: PrivateMessageI = this.state.messages.find(
+ m => m.id === data.message.id
+ );
+ found.content = data.message.content;
+ found.updated = data.message.updated;
+ found.deleted = data.message.deleted;
+ // If youre in the unread view, just remove it from the list
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
+ this.state.messages = this.state.messages.filter(
+ r => r.id !== data.message.id
+ );
+ } else {
+ let found = this.state.messages.find(c => c.id == data.message.id);
+ found.read = data.message.read;
+ }
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
- } else if (op == UserOperation.MarkAllAsRead) {
+ } else if (res.op == UserOperation.MarkAllAsRead) {
this.state.replies = [];
this.state.mentions = [];
+ this.state.messages = [];
+ this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
- } else if (op == UserOperation.EditComment) {
- let res: CommentResponse = msg;
-
- let found = this.state.replies.find(c => c.id == res.comment.id);
- found.content = res.comment.content;
- found.updated = res.comment.updated;
- found.removed = res.comment.removed;
- found.deleted = res.comment.deleted;
- found.upvotes = res.comment.upvotes;
- found.downvotes = res.comment.downvotes;
- found.score = res.comment.score;
+ } else if (res.op == UserOperation.EditComment) {
+ let data = res.data as CommentResponse;
+
+ let found = this.state.replies.find(c => c.id == data.comment.id);
+ found.content = data.comment.content;
+ found.updated = data.comment.updated;
+ found.removed = data.comment.removed;
+ found.deleted = data.comment.deleted;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ found.score = data.comment.score;
// If youre in the unread view, just remove it from the list
- if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
this.state.replies = this.state.replies.filter(
- r => r.id !== res.comment.id
+ r => r.id !== data.comment.id
);
} else {
- let found = this.state.replies.find(c => c.id == res.comment.id);
- found.read = res.comment.read;
+ let found = this.state.replies.find(c => c.id == data.comment.id);
+ found.read = data.comment.read;
}
this.sendUnreadCount();
this.setState(this.state);
- } else if (op == UserOperation.EditUserMention) {
- let res: UserMentionResponse = msg;
-
- let found = this.state.mentions.find(c => c.id == res.mention.id);
- found.content = res.mention.content;
- found.updated = res.mention.updated;
- found.removed = res.mention.removed;
- found.deleted = res.mention.deleted;
- found.upvotes = res.mention.upvotes;
- found.downvotes = res.mention.downvotes;
- found.score = res.mention.score;
+ } else if (res.op == UserOperation.EditUserMention) {
+ let data = res.data as UserMentionResponse;
+
+ let found = this.state.mentions.find(c => c.id == data.mention.id);
+ found.content = data.mention.content;
+ found.updated = data.mention.updated;
+ found.removed = data.mention.removed;
+ found.deleted = data.mention.deleted;
+ found.upvotes = data.mention.upvotes;
+ found.downvotes = data.mention.downvotes;
+ found.score = data.mention.score;
// If youre in the unread view, just remove it from the list
- if (this.state.unreadOrAll == UnreadOrAll.Unread && res.mention.read) {
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && data.mention.read) {
this.state.mentions = this.state.mentions.filter(
- r => r.id !== res.mention.id
+ r => r.id !== data.mention.id
);
} else {
- let found = this.state.mentions.find(c => c.id == res.mention.id);
- found.read = res.mention.read;
+ let found = this.state.mentions.find(c => c.id == data.mention.id);
+ found.read = data.mention.read;
}
this.sendUnreadCount();
this.setState(this.state);
- } else if (op == UserOperation.CreateComment) {
+ } else if (res.op == UserOperation.CreateComment) {
// let res: CommentResponse = msg;
- alert(i18n.t('reply_sent'));
+ toast(i18n.t('reply_sent'));
// this.state.replies.unshift(res.comment); // TODO do this right
// this.setState(this.state);
- } else if (op == UserOperation.SaveComment) {
- let res: CommentResponse = msg;
- let found = this.state.replies.find(c => c.id == res.comment.id);
- found.saved = res.comment.saved;
+ } else if (res.op == UserOperation.SaveComment) {
+ let data = res.data as CommentResponse;
+ let found = this.state.replies.find(c => c.id == data.comment.id);
+ found.saved = data.comment.saved;
this.setState(this.state);
- } else if (op == UserOperation.CreateCommentLike) {
- let res: CommentResponse = msg;
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
let found: Comment = this.state.replies.find(
- c => c.id === res.comment.id
+ c => c.id === data.comment.id
);
- found.score = res.comment.score;
- found.upvotes = res.comment.upvotes;
- found.downvotes = res.comment.downvotes;
- if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
+ found.score = data.comment.score;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
this.setState(this.state);
}
}
sendUnreadCount() {
let count =
this.state.replies.filter(r => !r.read).length +
- this.state.mentions.filter(r => !r.read).length;
+ this.state.mentions.filter(r => !r.read).length +
+ this.state.messages.filter(
+ r => !r.read && r.creator_id !== UserService.Instance.user.id
+ ).length;
UserService.Instance.sub.next({
user: UserService.Instance.user,
unreadCount: count,
UserOperation,
PasswordResetForm,
GetSiteResponse,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { msgOp, validEmail } from '../utils';
+import { wsJsonToRes, validEmail, toast } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
this.state = this.emptyState;
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),
WebSocketService.Instance.passwordReset(resetForm);
}
- parseMessage(msg: any) {
- let op: UserOperation = msgOp(msg);
- if (msg.error) {
- alert(i18n.t(msg.error));
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (res.error) {
+ toast(i18n.t(msg.error), 'danger');
this.state = this.emptyState;
this.setState(this.state);
return;
} else {
- if (op == UserOperation.Login) {
+ if (res.op == UserOperation.Login) {
+ let data = res.data as LoginResponse;
this.state = this.emptyState;
this.setState(this.state);
- let res: LoginResponse = msg;
- UserService.Instance.login(res);
+ UserService.Instance.login(data);
+ toast(i18n.t('logged_in'));
this.props.history.push('/');
- } else if (op == UserOperation.Register) {
+ } else if (res.op == UserOperation.Register) {
+ let data = res.data as LoginResponse;
this.state = this.emptyState;
this.setState(this.state);
- let res: LoginResponse = msg;
- UserService.Instance.login(res);
+ UserService.Instance.login(data);
this.props.history.push('/communities');
- } else if (op == UserOperation.PasswordReset) {
- alert(i18n.t('reset_password_mail_sent'));
- } else if (op == UserOperation.GetSite) {
- let res: GetSiteResponse = msg;
- this.state.enable_nsfw = res.site.enable_nsfw;
+ } else if (res.op == UserOperation.PasswordReset) {
+ toast(i18n.t('reset_password_mail_sent'));
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
document.title = `${i18n.t('login')} - ${
WebSocketService.Instance.site.name
CreatePostLikeResponse,
Post,
GetPostsForm,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings';
import { ListingTypeSelect } from './listing-type-select';
import { SiteForm } from './site-form';
import {
- msgOp,
+ wsJsonToRes,
repoUrl,
mdToHtml,
fetchLimit,
postRefetchSeconds,
pictshareAvatarThumbnail,
showAvatars,
+ toast,
} from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
subscribedCommunities: [],
trendingCommunities: [],
site: {
- op: null,
site: {
id: null,
name: null,
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');
return;
- } else if (op == UserOperation.GetFollowedCommunities) {
- let res: GetFollowedCommunitiesResponse = msg;
- this.state.subscribedCommunities = res.communities;
+ } else if (res.op == UserOperation.GetFollowedCommunities) {
+ let data = res.data as GetFollowedCommunitiesResponse;
+ this.state.subscribedCommunities = data.communities;
this.setState(this.state);
- } else if (op == UserOperation.ListCommunities) {
- let res: ListCommunitiesResponse = msg;
- this.state.trendingCommunities = res.communities;
+ } else if (res.op == UserOperation.ListCommunities) {
+ let data = res.data as ListCommunitiesResponse;
+ this.state.trendingCommunities = data.communities;
this.setState(this.state);
- } else if (op == UserOperation.GetSite) {
- let res: GetSiteResponse = msg;
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
// This means it hasn't been set up yet
- if (!res.site) {
+ if (!data.site) {
this.context.router.history.push('/setup');
}
- this.state.site.admins = res.admins;
- this.state.site.site = res.site;
- this.state.site.banned = res.banned;
- this.state.site.online = res.online;
+ this.state.site.admins = data.admins;
+ this.state.site.site = data.site;
+ this.state.site.banned = data.banned;
+ this.state.site.online = data.online;
this.setState(this.state);
document.title = `${WebSocketService.Instance.site.name}`;
- } else if (op == UserOperation.EditSite) {
- let res: SiteResponse = msg;
- this.state.site.site = res.site;
+ } else if (res.op == UserOperation.EditSite) {
+ let data = res.data as SiteResponse;
+ this.state.site.site = data.site;
this.state.showEditSite = false;
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);
}
}
ModAdd,
} from '../interfaces';
import { WebSocketService } from '../services';
-import { msgOp, addTypeInfo, fetchLimit } from '../utils';
+import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
import { MomentTime } from './moment-time';
-import * as moment from 'moment';
+import moment from 'moment';
import { i18n } from '../i18next';
interface ModlogState {
? Number(this.props.match.params.community_id)
: undefined;
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),
WebSocketService.Instance.getModlog(modlogForm);
}
- 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.GetModlog) {
- let res: GetModlogResponse = msg;
+ } else if (res.op == UserOperation.GetModlog) {
+ let data = res.data as GetModlogResponse;
this.state.loading = false;
window.scrollTo(0, 0);
- this.setCombined(res);
+ this.setCombined(data);
}
}
}
import { Component } from 'inferno';
-import * as moment from 'moment';
+import moment from 'moment';
import { getMomentLanguage } from '../utils';
import { i18n } from '../i18next';
GetRepliesResponse,
GetUserMentionsForm,
GetUserMentionsResponse,
+ GetPrivateMessagesForm,
+ PrivateMessagesResponse,
SortType,
GetSiteResponse,
Comment,
+ PrivateMessage,
+ WebSocketJsonResponse,
} from '../interfaces';
import {
- msgOp,
+ wsJsonToRes,
pictshareAvatarThumbnail,
showAvatars,
fetchLimit,
+ isCommentType,
+ toast,
} from '../utils';
import { version } from '../version';
import { i18n } from '../i18next';
expanded: boolean;
replies: Array<Comment>;
mentions: Array<Comment>;
+ messages: Array<PrivateMessage>;
fetchCount: number;
unreadCount: number;
siteName: string;
fetchCount: 0,
replies: [],
mentions: [],
+ messages: [],
expanded: false,
siteName: undefined,
};
i.setState(i.state);
}
- parseMessage(msg: any) {
- let op: UserOperation = msgOp(msg);
- if (msg.error) {
- if (msg.error == 'not_logged_in') {
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (res.error) {
+ if (res.error == 'not_logged_in') {
UserService.Instance.logout();
location.reload();
}
return;
- } else if (op == UserOperation.GetReplies) {
- let res: GetRepliesResponse = msg;
- let unreadReplies = res.replies.filter(r => !r.read);
+ } else if (res.op == UserOperation.GetReplies) {
+ let data = res.data as GetRepliesResponse;
+ let unreadReplies = data.replies.filter(r => !r.read);
if (
unreadReplies.length > 0 &&
this.state.fetchCount > 1 &&
this.state.replies = unreadReplies;
this.setState(this.state);
this.sendUnreadCount();
- } else if (op == UserOperation.GetUserMentions) {
- let res: GetUserMentionsResponse = msg;
- let unreadMentions = res.mentions.filter(r => !r.read);
+ } else if (res.op == UserOperation.GetUserMentions) {
+ let data = res.data as GetUserMentionsResponse;
+ let unreadMentions = data.mentions.filter(r => !r.read);
if (
unreadMentions.length > 0 &&
this.state.fetchCount > 1 &&
this.state.mentions = unreadMentions;
this.setState(this.state);
this.sendUnreadCount();
- } else if (op == UserOperation.GetSite) {
- let res: GetSiteResponse = msg;
+ } else if (res.op == UserOperation.GetPrivateMessages) {
+ let data = res.data as PrivateMessagesResponse;
+ let unreadMessages = data.messages.filter(r => !r.read);
+ if (
+ unreadMessages.length > 0 &&
+ this.state.fetchCount > 1 &&
+ JSON.stringify(this.state.messages) !== JSON.stringify(unreadMessages)
+ ) {
+ this.notify(unreadMessages);
+ }
+
+ this.state.messages = unreadMessages;
+ this.setState(this.state);
+ this.sendUnreadCount();
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
- if (res.site) {
- this.state.siteName = res.site.name;
- WebSocketService.Instance.site = res.site;
+ if (data.site) {
+ this.state.siteName = data.site.name;
+ WebSocketService.Instance.site = data.site;
this.setState(this.state);
}
}
page: 1,
limit: fetchLimit,
};
+
+ let privateMessagesForm: GetPrivateMessagesForm = {
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ };
+
if (this.currentLocation !== '/inbox') {
WebSocketService.Instance.getReplies(repliesForm);
WebSocketService.Instance.getUserMentions(userMentionsForm);
+ WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
this.state.fetchCount++;
}
}
get unreadCount() {
return (
this.state.replies.filter(r => !r.read).length +
- this.state.mentions.filter(r => !r.read).length
+ this.state.mentions.filter(r => !r.read).length +
+ this.state.messages.filter(r => !r.read).length
);
}
if (UserService.Instance.user) {
document.addEventListener('DOMContentLoaded', function() {
if (!Notification) {
- alert(i18n.t('notifications_error'));
+ toast(i18n.t('notifications_error'), 'danger');
return;
}
}
}
- notify(replies: Array<Comment>) {
+ notify(replies: Array<Comment | PrivateMessage>) {
let recentReply = replies[0];
if (Notification.permission !== 'granted') Notification.requestPermission();
else {
var notification = new Notification(
`${replies.length} ${i18n.t('unread_messages')}`,
{
- icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
+ icon: recentReply.creator_avatar
+ ? recentReply.creator_avatar
+ : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
body: `${recentReply.creator_name}: ${recentReply.content}`,
}
);
notification.onclick = () => {
this.context.router.history.push(
- `/post/${recentReply.post_id}/comment/${recentReply.id}`
+ isCommentType(recentReply)
+ ? `/post/${recentReply.post_id}/comment/${recentReply.id}`
+ : `/inbox`
);
};
}
UserOperation,
LoginResponse,
PasswordChangeForm,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { msgOp, capitalizeFirstLetter } from '../utils';
+import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
this.state = this.emptyState;
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),
WebSocketService.Instance.passwordChange(i.state.passwordChangeForm);
}
- parseMessage(msg: any) {
- let op: UserOperation = msgOp(msg);
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
if (msg.error) {
- alert(i18n.t(msg.error));
+ toast(i18n.t(msg.error), 'danger');
this.state.loading = false;
this.setState(this.state);
return;
} else {
- if (op == UserOperation.PasswordChange) {
+ if (res.op == UserOperation.PasswordChange) {
+ let data = res.data as LoginResponse;
this.state = this.emptyState;
this.setState(this.state);
- let res: LoginResponse = msg;
- UserService.Instance.login(res);
+ UserService.Instance.login(data);
this.props.history.push('/');
}
}
SearchType,
SearchResponse,
GetSiteResponse,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
- msgOp,
+ wsJsonToRes,
getPageTitle,
validURL,
capitalizeFirstLetter,
mdToHtml,
debounce,
isImage,
+ toast,
} from '../utils';
import autosize from 'autosize';
import { i18n } from '../i18next';
.catch(error => {
i.state.imageLoading = false;
i.setState(i.state);
- alert(error);
+ toast(error, 'danger');
});
}
- parseMessage(msg: any) {
- let op: UserOperation = msgOp(msg);
- if (msg.error) {
- alert(i18n.t(msg.error));
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (res.error) {
+ toast(i18n.t(msg.error), 'danger');
this.state.loading = false;
this.setState(this.state);
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;
if (this.props.post) {
this.state.postForm.community_id = this.props.post.community_id;
} else if (this.props.params && this.props.params.community) {
- let foundCommunityId = res.communities.find(
+ let foundCommunityId = data.communities.find(
r => r.name == this.props.params.community
).id;
this.state.postForm.community_id = foundCommunityId;
} else {
- this.state.postForm.community_id = res.communities[0].id;
+ this.state.postForm.community_id = data.communities[0].id;
}
this.setState(this.state);
- } else if (op == UserOperation.CreatePost) {
+ } else if (res.op == UserOperation.CreatePost) {
+ let data = res.data as PostResponse;
this.state.loading = false;
- let res: PostResponse = msg;
- this.props.onCreate(res.post.id);
- } else if (op == UserOperation.EditPost) {
+ this.props.onCreate(data.post.id);
+ } else if (res.op == UserOperation.EditPost) {
+ let data = res.data as PostResponse;
this.state.loading = false;
- let res: PostResponse = msg;
- this.props.onEdit(res.post);
- } else if (op == UserOperation.Search) {
- let res: SearchResponse = msg;
-
- if (res.type_ == SearchType[SearchType.Posts]) {
- this.state.suggestedPosts = res.posts;
- } else if (res.type_ == SearchType[SearchType.Url]) {
- this.state.crossPosts = res.posts;
+ this.props.onEdit(data.post);
+ } else if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+
+ if (data.type_ == SearchType[SearchType.Posts]) {
+ this.state.suggestedPosts = data.posts;
+ } else if (data.type_ == SearchType[SearchType.Url]) {
+ this.state.crossPosts = data.posts;
}
this.setState(this.state);
- } else if (op == UserOperation.GetSite) {
- let res: GetSiteResponse = msg;
- this.state.enable_nsfw = res.site.enable_nsfw;
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
}
}
SearchResponse,
GetSiteResponse,
GetCommunityResponse,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { msgOp, hotRank } from '../utils';
+import { wsJsonToRes, hotRank, toast } from '../utils';
import { PostListing } from './post-listing';
import { PostListings } from './post-listings';
import { Sidebar } from './sidebar';
);
}
- 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.GetPost) {
- let res: GetPostResponse = msg;
- this.state.post = res.post;
- this.state.comments = res.comments;
- this.state.community = res.community;
- this.state.moderators = res.moderators;
- this.state.admins = res.admins;
+ } else if (res.op == UserOperation.GetPost) {
+ let data = res.data as GetPostResponse;
+ this.state.post = data.post;
+ this.state.comments = data.comments;
+ this.state.community = data.community;
+ this.state.moderators = data.moderators;
+ this.state.admins = data.admins;
this.state.loading = false;
document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
}
this.setState(this.state);
- } else if (op == UserOperation.CreateComment) {
- let res: CommentResponse = msg;
- this.state.comments.unshift(res.comment);
+ } else if (res.op == UserOperation.CreateComment) {
+ let data = res.data as CommentResponse;
+ this.state.comments.unshift(data.comment);
this.setState(this.state);
- } else if (op == UserOperation.EditComment) {
- let res: CommentResponse = msg;
- let found = this.state.comments.find(c => c.id == res.comment.id);
- found.content = res.comment.content;
- found.updated = res.comment.updated;
- found.removed = res.comment.removed;
- found.deleted = res.comment.deleted;
- found.upvotes = res.comment.upvotes;
- found.downvotes = res.comment.downvotes;
- found.score = res.comment.score;
- found.read = res.comment.read;
+ } else if (res.op == UserOperation.EditComment) {
+ let data = res.data as CommentResponse;
+ let found = this.state.comments.find(c => c.id == data.comment.id);
+ found.content = data.comment.content;
+ found.updated = data.comment.updated;
+ found.removed = data.comment.removed;
+ found.deleted = data.comment.deleted;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ found.score = data.comment.score;
+ found.read = data.comment.read;
this.setState(this.state);
- } else if (op == UserOperation.SaveComment) {
- let res: CommentResponse = msg;
- let found = this.state.comments.find(c => c.id == res.comment.id);
- found.saved = res.comment.saved;
+ } else if (res.op == UserOperation.SaveComment) {
+ let data = res.data as CommentResponse;
+ let found = this.state.comments.find(c => c.id == data.comment.id);
+ found.saved = data.comment.saved;
this.setState(this.state);
- } else if (op == UserOperation.CreateCommentLike) {
- let res: CommentResponse = msg;
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
let found: Comment = this.state.comments.find(
- c => c.id === res.comment.id
+ c => c.id === data.comment.id
);
- found.score = res.comment.score;
- found.upvotes = res.comment.upvotes;
- found.downvotes = res.comment.downvotes;
- if (res.comment.my_vote !== null) {
- found.my_vote = res.comment.my_vote;
+ found.score = data.comment.score;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ if (data.comment.my_vote !== null) {
+ found.my_vote = data.comment.my_vote;
found.upvoteLoading = false;
found.downvoteLoading = false;
}
this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let res: CreatePostLikeResponse = msg;
- this.state.post.my_vote = res.post.my_vote;
- this.state.post.score = res.post.score;
- this.state.post.upvotes = res.post.upvotes;
- this.state.post.downvotes = res.post.downvotes;
- this.state.post.upvoteLoading = false;
- this.state.post.downvoteLoading = false;
+ } else if (res.op == UserOperation.CreatePostLike) {
+ let data = res.data as CreatePostLikeResponse;
+ this.state.post.my_vote = data.post.my_vote;
+ this.state.post.score = data.post.score;
+ this.state.post.upvotes = data.post.upvotes;
+ this.state.post.downvotes = data.post.downvotes;
this.setState(this.state);
- } else if (op == UserOperation.EditPost) {
- let res: PostResponse = msg;
- this.state.post = res.post;
+ } else if (res.op == UserOperation.EditPost) {
+ let data = res.data as PostResponse;
+ this.state.post = data.post;
this.setState(this.state);
- } else if (op == UserOperation.SavePost) {
- let res: PostResponse = msg;
- this.state.post = res.post;
+ } else if (res.op == UserOperation.SavePost) {
+ let data = res.data as PostResponse;
+ this.state.post = data.post;
this.setState(this.state);
- } else if (op == UserOperation.EditCommunity) {
- let res: CommunityResponse = msg;
- this.state.community = res.community;
- this.state.post.community_id = res.community.id;
- this.state.post.community_name = res.community.name;
+ } else if (res.op == UserOperation.EditCommunity) {
+ let data = res.data as CommunityResponse;
+ this.state.community = data.community;
+ this.state.post.community_id = data.community.id;
+ this.state.post.community_name = data.community.name;
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.BanFromCommunity) {
- let res: BanFromCommunityResponse = msg;
+ } else if (res.op == UserOperation.BanFromCommunity) {
+ let data = res.data as BanFromCommunityResponse;
this.state.comments
- .filter(c => c.creator_id == res.user.id)
- .forEach(c => (c.banned_from_community = res.banned));
- if (this.state.post.creator_id == res.user.id) {
- this.state.post.banned_from_community = res.banned;
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned_from_community = data.banned));
+ if (this.state.post.creator_id == data.user.id) {
+ this.state.post.banned_from_community = data.banned;
}
this.setState(this.state);
- } else if (op == UserOperation.AddModToCommunity) {
- let res: AddModToCommunityResponse = msg;
- this.state.moderators = res.moderators;
+ } else if (res.op == UserOperation.AddModToCommunity) {
+ let data = res.data as AddModToCommunityResponse;
+ this.state.moderators = data.moderators;
this.setState(this.state);
- } else if (op == UserOperation.BanUser) {
- let res: BanUserResponse = msg;
+ } else if (res.op == UserOperation.BanUser) {
+ let data = res.data as BanUserResponse;
this.state.comments
- .filter(c => c.creator_id == res.user.id)
- .forEach(c => (c.banned = res.banned));
- if (this.state.post.creator_id == res.user.id) {
- this.state.post.banned = res.banned;
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
+ if (this.state.post.creator_id == data.user.id) {
+ this.state.post.banned = data.banned;
}
this.setState(this.state);
- } else if (op == UserOperation.AddAdmin) {
- let res: AddAdminResponse = msg;
- this.state.admins = res.admins;
+ } else if (res.op == UserOperation.AddAdmin) {
+ let data = res.data as AddAdminResponse;
+ this.state.admins = data.admins;
this.setState(this.state);
- } else if (op == UserOperation.Search) {
- let res: SearchResponse = msg;
- this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
+ } else if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+ this.state.crossPosts = data.posts.filter(
+ p => p.id != this.state.post.id
+ );
this.setState(this.state);
- } else if (op == UserOperation.TransferSite) {
- let res: GetSiteResponse = msg;
+ } else if (res.op == UserOperation.TransferSite) {
+ let data = res.data as GetSiteResponse;
- this.state.admins = res.admins;
+ this.state.admins = data.admins;
this.setState(this.state);
- } else if (op == UserOperation.TransferCommunity) {
- let res: GetCommunityResponse = msg;
- this.state.community = res.community;
- this.state.moderators = res.moderators;
- this.state.admins = res.admins;
+ } else if (res.op == UserOperation.TransferCommunity) {
+ let data = res.data as GetCommunityResponse;
+ this.state.community = data.community;
+ this.state.moderators = data.moderators;
+ this.state.admins = data.admins;
this.setState(this.state);
}
}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ PrivateMessageForm as PrivateMessageFormI,
+ EditPrivateMessageForm,
+ PrivateMessageFormParams,
+ PrivateMessage,
+ PrivateMessageResponse,
+ UserView,
+ UserOperation,
+ UserDetailsResponse,
+ GetUserDetailsForm,
+ SortType,
+ WebSocketJsonResponse,
+} from '../interfaces';
+import { WebSocketService } from '../services';
+import {
+ capitalizeFirstLetter,
+ markdownHelpUrl,
+ mdToHtml,
+ showAvatars,
+ pictshareAvatarThumbnail,
+ wsJsonToRes,
+ toast,
+} from '../utils';
+import autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PrivateMessageFormProps {
+ privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
+ params?: PrivateMessageFormParams;
+ onCancel?(): any;
+ onCreate?(message: PrivateMessage): any;
+ onEdit?(message: PrivateMessage): any;
+}
+
+interface PrivateMessageFormState {
+ privateMessageForm: PrivateMessageFormI;
+ recipient: UserView;
+ loading: boolean;
+ previewMode: boolean;
+ showDisclaimer: boolean;
+}
+
+export class PrivateMessageForm extends Component<
+ PrivateMessageFormProps,
+ PrivateMessageFormState
+> {
+ private subscription: Subscription;
+ private emptyState: PrivateMessageFormState = {
+ privateMessageForm: {
+ content: null,
+ recipient_id: null,
+ },
+ recipient: null,
+ loading: false,
+ previewMode: false,
+ showDisclaimer: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ if (this.props.privateMessage) {
+ this.state.privateMessageForm = {
+ content: this.props.privateMessage.content,
+ recipient_id: this.props.privateMessage.recipient_id,
+ };
+ }
+
+ if (this.props.params) {
+ this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
+ let form: GetUserDetailsForm = {
+ user_id: this.state.privateMessageForm.recipient_id,
+ sort: SortType[SortType.New],
+ saved_only: false,
+ };
+ WebSocketService.Instance.getUserDetails(form);
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentDidMount() {
+ autosize(document.querySelectorAll('textarea'));
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ render() {
+ return (
+ <div>
+ <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
+ {!this.props.privateMessage && (
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ {capitalizeFirstLetter(i18n.t('to'))}
+ </label>
+
+ {this.state.recipient && (
+ <div class="col-sm-10 form-control-plaintext">
+ <Link
+ className="text-info"
+ to={`/u/${this.state.recipient.name}`}
+ >
+ {this.state.recipient.avatar && showAvatars() && (
+ <img
+ height="32"
+ width="32"
+ src={pictshareAvatarThumbnail(
+ this.state.recipient.avatar
+ )}
+ class="rounded-circle mr-1"
+ />
+ )}
+ <span>{this.state.recipient.name}</span>
+ </Link>
+ </div>
+ )}
+ </div>
+ )}
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
+ <div class="col-sm-10">
+ <textarea
+ value={this.state.privateMessageForm.content}
+ onInput={linkEvent(this, this.handleContentChange)}
+ className={`form-control ${this.state.previewMode && 'd-none'}`}
+ rows={4}
+ maxLength={10000}
+ />
+ {this.state.previewMode && (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(
+ this.state.privateMessageForm.content
+ )}
+ />
+ )}
+
+ {this.state.privateMessageForm.content && (
+ <button
+ className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
+ .previewMode && 'active'}`}
+ onClick={linkEvent(this, this.handlePreviewToggle)}
+ >
+ {i18n.t('preview')}
+ </button>
+ )}
+ <ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
+ <li class="list-inline-item">
+ <span
+ onClick={linkEvent(this, this.handleShowDisclaimer)}
+ class="pointer"
+ >
+ {i18n.t('disclaimer')}
+ </span>
+ </li>
+ <li class="list-inline-item">
+ <a href={markdownHelpUrl} target="_blank" class="text-muted">
+ {i18n.t('formatting_help')}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ {this.state.showDisclaimer && (
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <div class="alert alert-danger" role="alert">
+ <T i18nKey="private_message_disclaimer">
+ #
+ <a
+ class="alert-link"
+ target="_blank"
+ href="https://about.riot.im/"
+ >
+ #
+ </a>
+ </T>
+ </div>
+ </div>
+ </div>
+ )}
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary mr-2">
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : this.props.privateMessage ? (
+ capitalizeFirstLetter(i18n.t('save'))
+ ) : (
+ capitalizeFirstLetter(i18n.t('send_message'))
+ )}
+ </button>
+ {this.props.privateMessage && (
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleCancel)}
+ >
+ {i18n.t('cancel')}
+ </button>
+ )}
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+
+ handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
+ event.preventDefault();
+ if (i.props.privateMessage) {
+ let editForm: EditPrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ content: i.state.privateMessageForm.content,
+ };
+ WebSocketService.Instance.editPrivateMessage(editForm);
+ } else {
+ WebSocketService.Instance.createPrivateMessage(
+ i.state.privateMessageForm
+ );
+ }
+ i.state.loading = true;
+ i.setState(i.state);
+ }
+
+ handleRecipientChange(i: PrivateMessageForm, event: any) {
+ i.state.recipient = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleContentChange(i: PrivateMessageForm, event: any) {
+ i.state.privateMessageForm.content = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleCancel(i: PrivateMessageForm) {
+ i.props.onCancel();
+ }
+
+ handlePreviewToggle(i: PrivateMessageForm, event: any) {
+ event.preventDefault();
+ i.state.previewMode = !i.state.previewMode;
+ i.setState(i.state);
+ }
+
+ handleShowDisclaimer(i: PrivateMessageForm) {
+ i.state.showDisclaimer = !i.state.showDisclaimer;
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (res.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (res.op == UserOperation.EditPrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+ this.state.loading = false;
+ this.props.onEdit(data.message);
+ } else if (res.op == UserOperation.GetUserDetails) {
+ let data = res.data as UserDetailsResponse;
+ this.state.recipient = data.user;
+ this.state.privateMessageForm.recipient_id = data.user.id;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreatePrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+ this.state.loading = false;
+ this.props.onCreate(data.message);
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import {
+ PrivateMessage as PrivateMessageI,
+ EditPrivateMessageForm,
+} from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import {
+ mdToHtml,
+ pictshareAvatarThumbnail,
+ showAvatars,
+ toast,
+} from '../utils';
+import { MomentTime } from './moment-time';
+import { PrivateMessageForm } from './private-message-form';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PrivateMessageState {
+ showReply: boolean;
+ showEdit: boolean;
+ collapsed: boolean;
+ viewSource: boolean;
+}
+
+interface PrivateMessageProps {
+ privateMessage: PrivateMessageI;
+}
+
+export class PrivateMessage extends Component<
+ PrivateMessageProps,
+ PrivateMessageState
+> {
+ private emptyState: PrivateMessageState = {
+ showReply: false,
+ showEdit: false,
+ collapsed: false,
+ viewSource: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleReplyCancel = this.handleReplyCancel.bind(this);
+ this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
+ this
+ );
+ this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
+ }
+
+ get mine(): boolean {
+ return UserService.Instance.user.id == this.props.privateMessage.creator_id;
+ }
+
+ render() {
+ let message = this.props.privateMessage;
+ return (
+ <div class="mb-2">
+ <div>
+ <ul class="list-inline mb-0 text-muted small">
+ <li className="list-inline-item">
+ {this.mine ? i18n.t('to') : i18n.t('from')}
+ </li>
+ <li className="list-inline-item">
+ <Link
+ className="text-info"
+ to={
+ this.mine
+ ? `/u/${message.recipient_name}`
+ : `/u/${message.creator_name}`
+ }
+ >
+ {(this.mine
+ ? message.recipient_avatar
+ : message.creator_avatar) &&
+ showAvatars() && (
+ <img
+ height="32"
+ width="32"
+ src={pictshareAvatarThumbnail(
+ this.mine
+ ? message.recipient_avatar
+ : message.creator_avatar
+ )}
+ class="rounded-circle mr-1"
+ />
+ )}
+ <span>
+ {this.mine ? message.recipient_name : message.creator_name}
+ </span>
+ </Link>
+ </li>
+ <li className="list-inline-item">
+ <span>
+ <MomentTime data={message} />
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <div
+ className="pointer text-monospace"
+ onClick={linkEvent(this, this.handleMessageCollapse)}
+ >
+ {this.state.collapsed ? '[+]' : '[-]'}
+ </div>
+ </li>
+ </ul>
+ {this.state.showEdit && (
+ <PrivateMessageForm
+ privateMessage={message}
+ onEdit={this.handlePrivateMessageEdit}
+ onCancel={this.handleReplyCancel}
+ />
+ )}
+ {!this.state.showEdit && !this.state.collapsed && (
+ <div>
+ {this.state.viewSource ? (
+ <pre>{this.messageUnlessRemoved}</pre>
+ ) : (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
+ />
+ )}
+ <ul class="list-inline mb-1 text-muted small font-weight-bold">
+ {!this.mine && (
+ <>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleMarkRead)}
+ >
+ {message.read
+ ? i18n.t('mark_as_unread')
+ : i18n.t('mark_as_read')}
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleReplyClick)}
+ >
+ <T i18nKey="reply">#</T>
+ </span>
+ </li>
+ </>
+ )}
+ {this.mine && (
+ <>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleEditClick)}
+ >
+ <T i18nKey="edit">#</T>
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleDeleteClick)}
+ >
+ {!message.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')}
+ </span>
+ </li>
+ </>
+ )}
+ <li className="list-inline-item">•</li>
+ <li className="list-inline-item">
+ <span
+ className="pointer"
+ onClick={linkEvent(this, this.handleViewSource)}
+ >
+ <T i18nKey="view_source">#</T>
+ </span>
+ </li>
+ </ul>
+ </div>
+ )}
+ </div>
+ {this.state.showReply && (
+ <PrivateMessageForm
+ params={{
+ recipient_id: this.props.privateMessage.creator_id,
+ }}
+ onCreate={this.handlePrivateMessageCreate}
+ />
+ )}
+ {/* A collapsed clearfix */}
+ {this.state.collapsed && <div class="row col-12"></div>}
+ </div>
+ );
+ }
+
+ get messageUnlessRemoved(): string {
+ let message = this.props.privateMessage;
+ return message.deleted ? `*${i18n.t('deleted')}*` : message.content;
+ }
+
+ handleReplyClick(i: PrivateMessage) {
+ i.state.showReply = true;
+ i.setState(i.state);
+ }
+
+ handleEditClick(i: PrivateMessage) {
+ i.state.showEdit = true;
+ i.setState(i.state);
+ }
+
+ handleDeleteClick(i: PrivateMessage) {
+ let form: EditPrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ deleted: !i.props.privateMessage.deleted,
+ };
+ WebSocketService.Instance.editPrivateMessage(form);
+ }
+
+ handleReplyCancel() {
+ this.state.showReply = false;
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handleMarkRead(i: PrivateMessage) {
+ let form: EditPrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ read: !i.props.privateMessage.read,
+ };
+ WebSocketService.Instance.editPrivateMessage(form);
+ }
+
+ handleMessageCollapse(i: PrivateMessage) {
+ i.state.collapsed = !i.state.collapsed;
+ i.setState(i.state);
+ }
+
+ handleViewSource(i: PrivateMessage) {
+ i.state.viewSource = !i.state.viewSource;
+ i.setState(i.state);
+ }
+
+ handlePrivateMessageEdit() {
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handlePrivateMessageCreate() {
+ this.state.showReply = false;
+ this.setState(this.state);
+ toast(i18n.t('message_sent'));
+ }
+}
SearchType,
CreatePostLikeResponse,
CommentResponse,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
import {
- msgOp,
+ wsJsonToRes,
fetchLimit,
routeSearchTypeToEnum,
routeSortTypeToEnum,
pictshareAvatarThumbnail,
showAvatars,
+ toast,
} from '../utils';
import { PostListing } from './post-listing';
import { SortSelect } from './sort-select';
sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props),
searchResponse: {
- op: null,
type_: null,
posts: [],
comments: [],
return (
<div>
{res &&
- res.op &&
res.posts.length == 0 &&
res.comments.length == 0 &&
res.communities.length == 0 &&
);
}
- 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.Search) {
- let res: SearchResponse = msg;
- this.state.searchResponse = res;
+ } else if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+ this.state.searchResponse = data;
this.state.loading = false;
document.title = `${i18n.t('search')} - ${this.state.q} - ${
WebSocketService.Instance.site.name
}`;
window.scrollTo(0, 0);
this.setState(this.state);
- } else if (op == UserOperation.CreateCommentLike) {
- let res: CommentResponse = msg;
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
let found: Comment = this.state.searchResponse.comments.find(
- c => c.id === res.comment.id
+ c => c.id === data.comment.id
);
- found.score = res.comment.score;
- found.upvotes = res.comment.upvotes;
- found.downvotes = res.comment.downvotes;
- if (res.comment.my_vote !== null) {
- found.my_vote = res.comment.my_vote;
+ found.score = data.comment.score;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ if (data.comment.my_vote !== null) {
+ found.my_vote = data.comment.my_vote;
found.upvoteLoading = false;
found.downvoteLoading = false;
}
this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let res: CreatePostLikeResponse = msg;
+ } else if (res.op == UserOperation.CreatePostLike) {
+ let data = res.data as CreatePostLikeResponse;
let found = this.state.searchResponse.posts.find(
- c => c.id == res.post.id
+ c => c.id == data.post.id
);
- found.my_vote = res.post.my_vote;
- found.score = res.post.score;
- found.upvotes = res.post.upvotes;
- found.downvotes = res.post.downvotes;
+ 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);
}
}
import { Component, linkEvent } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
-import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
+import {
+ RegisterForm,
+ LoginResponse,
+ UserOperation,
+ WebSocketJsonResponse,
+} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { msgOp } from '../utils';
+import { wsJsonToRes, toast } from '../utils';
import { SiteForm } from './site-form';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
this.state = this.emptyState;
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),
i.setState(i.state);
}
- parseMessage(msg: any) {
- let op: UserOperation = msgOp(msg);
- if (msg.error) {
- alert(i18n.t(msg.error));
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (res.error) {
+ toast(i18n.t(msg.error), 'danger');
this.state.userLoading = false;
this.setState(this.state);
return;
- } else if (op == UserOperation.Register) {
+ } else if (res.op == UserOperation.Register) {
+ let data = res.data as LoginResponse;
this.state.userLoading = false;
this.state.doneRegisteringUser = true;
- let res: LoginResponse = msg;
- UserService.Instance.login(res);
- console.log(res);
+ UserService.Instance.login(data);
this.setState(this.state);
- } else if (op == UserOperation.CreateSite) {
+ } else if (res.op == UserOperation.CreateSite) {
this.props.history.push('/');
}
}
AddAdminResponse,
DeleteAccountForm,
CreatePostLikeResponse,
+ WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
- msgOp,
+ wsJsonToRes,
fetchLimit,
routeSortTypeToEnum,
capitalizeFirstLetter,
setTheme,
languages,
showAvatars,
+ toast,
} from '../utils';
import { PostListing } from './post-listing';
import { SortSelect } from './sort-select';
</tr>
</table>
</div>
- {this.isCurrentUser && (
+ {this.isCurrentUser ? (
<button
class="btn btn-block btn-secondary mt-3"
onClick={linkEvent(this, this.handleLogoutClick)}
>
<T i18nKey="logout">#</T>
</button>
+ ) : (
+ <>
+ <a
+ className={`btn btn-block btn-secondary mt-3 ${!this.state
+ .user.matrix_user_id && 'disabled'}`}
+ target="_blank"
+ href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+ >
+ {i18n.t('send_secure_message')}
+ </a>
+ <Link
+ class="btn btn-block btn-secondary mt-3"
+ to={`/create_private_message?recipient_id=${this.state.user.id}`}
+ >
+ {i18n.t('send_message')}
+ </Link>
+ </>
)}
</div>
</div>
/>
</div>
</div>
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label">
+ <a href="https://about.riot.im/" target="_blank">
+ {i18n.t('matrix_user_id')}
+ </a>
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="text"
+ class="form-control"
+ placeholder="@user:example.com"
+ value={this.state.userSettingsForm.matrix_user_id}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsMatrixUserIdChange
+ )}
+ minLength={3}
+ />
+ </div>
+ </div>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<T i18nKey="new_password">#</T>
i.setState(i.state);
}
+ handleUserSettingsMatrixUserIdChange(i: User, event: any) {
+ i.state.userSettingsForm.matrix_user_id = event.target.value;
+ if (
+ i.state.userSettingsForm.matrix_user_id == '' &&
+ !i.state.user.matrix_user_id
+ ) {
+ i.state.userSettingsForm.matrix_user_id = undefined;
+ }
+ i.setState(i.state);
+ }
+
handleUserSettingsNewPasswordChange(i: User, event: any) {
i.state.userSettingsForm.new_password = event.target.value;
if (i.state.userSettingsForm.new_password == '') {
.catch(error => {
i.state.avatarLoading = false;
i.setState(i.state);
- alert(error);
+ toast(error, 'danger');
});
}
WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
}
- 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.state.deleteAccountLoading = false;
this.state.avatarLoading = false;
this.state.userSettingsLoading = false;
- if (msg.error == 'couldnt_find_that_username_or_email') {
+ if (res.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
this.setState(this.state);
return;
- } else if (op == UserOperation.GetUserDetails) {
- let res: UserDetailsResponse = msg;
- this.state.user = res.user;
- this.state.comments = res.comments;
- this.state.follows = res.follows;
- this.state.moderates = res.moderates;
- this.state.posts = res.posts;
- this.state.admins = res.admins;
+ } else if (res.op == UserOperation.GetUserDetails) {
+ let data = res.data as UserDetailsResponse;
+ this.state.user = data.user;
+ this.state.comments = data.comments;
+ this.state.follows = data.follows;
+ this.state.moderates = data.moderates;
+ this.state.posts = data.posts;
+ this.state.admins = data.admins;
this.state.loading = false;
if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw =
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
this.state.userSettingsForm.show_avatars =
UserService.Instance.user.show_avatars;
+ this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
}
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0, 0);
this.setState(this.state);
- } else if (op == UserOperation.EditComment) {
- let res: CommentResponse = msg;
+ } else if (res.op == UserOperation.EditComment) {
+ let data = res.data as CommentResponse;
- let found = this.state.comments.find(c => c.id == res.comment.id);
- found.content = res.comment.content;
- found.updated = res.comment.updated;
- found.removed = res.comment.removed;
- found.deleted = res.comment.deleted;
- found.upvotes = res.comment.upvotes;
- found.downvotes = res.comment.downvotes;
- found.score = res.comment.score;
+ let found = this.state.comments.find(c => c.id == data.comment.id);
+ found.content = data.comment.content;
+ found.updated = data.comment.updated;
+ found.removed = data.comment.removed;
+ found.deleted = data.comment.deleted;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ found.score = data.comment.score;
this.setState(this.state);
- } else if (op == UserOperation.CreateComment) {
+ } else if (res.op == UserOperation.CreateComment) {
// let res: CommentResponse = msg;
- alert(i18n.t('reply_sent'));
+ toast(i18n.t('reply_sent'));
// this.state.comments.unshift(res.comment); // TODO do this right
// this.setState(this.state);
- } else if (op == UserOperation.SaveComment) {
- let res: CommentResponse = msg;
- let found = this.state.comments.find(c => c.id == res.comment.id);
- found.saved = res.comment.saved;
+ } else if (res.op == UserOperation.SaveComment) {
+ let data = res.data as CommentResponse;
+ let found = this.state.comments.find(c => c.id == data.comment.id);
+ found.saved = data.comment.saved;
this.setState(this.state);
- } else if (op == UserOperation.CreateCommentLike) {
- let res: CommentResponse = msg;
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
let found: Comment = this.state.comments.find(
- c => c.id === res.comment.id
+ c => c.id === data.comment.id
);
- found.score = res.comment.score;
- found.upvotes = res.comment.upvotes;
- found.downvotes = res.comment.downvotes;
- if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
+ found.score = data.comment.score;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
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);
- } else if (op == UserOperation.BanUser) {
- let res: BanUserResponse = msg;
+ } else if (res.op == UserOperation.BanUser) {
+ let data = res.data as BanUserResponse;
this.state.comments
- .filter(c => c.creator_id == res.user.id)
- .forEach(c => (c.banned = res.banned));
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
this.state.posts
- .filter(c => c.creator_id == res.user.id)
- .forEach(c => (c.banned = res.banned));
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
this.setState(this.state);
- } else if (op == UserOperation.AddAdmin) {
- let res: AddAdminResponse = msg;
- this.state.admins = res.admins;
+ } else if (res.op == UserOperation.AddAdmin) {
+ let data = res.data as AddAdminResponse;
+ this.state.admins = data.admins;
this.setState(this.state);
- } else if (op == UserOperation.SaveUserSettings) {
+ } else if (res.op == UserOperation.SaveUserSettings) {
+ let data = res.data as LoginResponse;
this.state = this.emptyState;
this.state.userSettingsLoading = false;
this.setState(this.state);
- let res: LoginResponse = msg;
- UserService.Instance.login(res);
- } else if (op == UserOperation.DeleteAccount) {
+ UserService.Instance.login(data);
+ } else if (res.op == UserOperation.DeleteAccount) {
this.state.deleteAccountLoading = false;
this.state.deleteAccountShowConfirm = false;
this.setState(this.state);
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
+ <link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />
import { Login } from './components/login';
import { CreatePost } from './components/create-post';
import { CreateCommunity } from './components/create-community';
+import { CreatePrivateMessage } from './components/create-private-message';
import { PasswordChange } from './components/password_change';
import { Post } from './components/post';
import { Community } from './components/community';
<Route path={`/login`} component={Login} />
<Route path={`/create_post`} component={CreatePost} />
<Route path={`/create_community`} component={CreateCommunity} />
+ <Route
+ path={`/create_private_message`}
+ component={CreatePrivateMessage}
+ />
<Route path={`/communities/page/:page`} component={Communities} />
<Route path={`/communities`} component={Communities} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
DeleteAccount,
PasswordReset,
PasswordChange,
+ CreatePrivateMessage,
+ EditPrivateMessage,
+ GetPrivateMessages,
}
export enum CommentSortType {
name: string;
avatar?: string;
email?: string;
+ matrix_user_id?: string;
fedi_name: string;
published: string;
number_of_posts: number;
enable_nsfw: boolean;
}
+export interface PrivateMessage {
+ id: number;
+ creator_id: number;
+ recipient_id: number;
+ content: string;
+ deleted: boolean;
+ read: boolean;
+ published: string;
+ updated?: string;
+ creator_name: string;
+ creator_avatar?: string;
+ recipient_name: string;
+ recipient_avatar?: string;
+}
+
export enum BanType {
Community,
Site,
}
export interface GetFollowedCommunitiesResponse {
- op: string;
communities: Array<CommunityUser>;
}
}
export interface UserDetailsResponse {
- op: string;
user: UserView;
follows: Array<CommunityUser>;
moderates: Array<CommunityUser>;
}
export interface GetRepliesResponse {
- op: string;
replies: Array<Comment>;
}
}
export interface GetUserMentionsResponse {
- op: string;
mentions: Array<Comment>;
}
}
export interface UserMentionResponse {
- op: string;
mention: Comment;
}
}
export interface BanFromCommunityResponse {
- op: string;
user: UserView;
banned: boolean;
}
}
export interface AddModToCommunityResponse {
- op: string;
moderators: Array<CommunityUser>;
}
}
export interface GetModlogResponse {
- op: string;
removed_posts: Array<ModRemovePost>;
locked_posts: Array<ModLockPost>;
stickied_posts: Array<ModStickyPost>;
}
export interface LoginResponse {
- op: string;
jwt: string;
}
lang: string;
avatar?: string;
email?: string;
+ matrix_user_id?: string;
new_password?: string;
new_password_verify?: string;
old_password?: string;
}
export interface GetCommunityResponse {
- op: string;
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
}
export interface CommunityResponse {
- op: string;
community: Community;
}
}
export interface ListCommunitiesResponse {
- op: string;
communities: Array<Community>;
}
export interface ListCategoriesResponse {
- op: string;
categories: Array<Category>;
}
}
export interface GetPostResponse {
- op: string;
post: Post;
comments: Array<Comment>;
community: Community;
}
export interface PostResponse {
- op: string;
post: Post;
}
}
export interface CommentResponse {
- op: string;
comment: Comment;
}
}
export interface GetPostsResponse {
- op: string;
posts: Array<Post>;
}
}
export interface CreatePostLikeResponse {
- op: string;
post: Post;
}
}
export interface GetSiteResponse {
- op: string;
site: Site;
admins: Array<UserView>;
banned: Array<UserView>;
}
export interface SiteResponse {
- op: string;
site: Site;
}
}
export interface BanUserResponse {
- op: string;
user: UserView;
banned: boolean;
}
}
export interface AddAdminResponse {
- op: string;
admins: Array<UserView>;
}
}
export interface SearchResponse {
- op: string;
type_: string;
posts?: Array<Post>;
comments?: Array<Comment>;
email: string;
}
-export interface PasswordResetResponse {
- op: string;
-}
+// export interface PasswordResetResponse {
+// }
export interface PasswordChangeForm {
token: string;
password: string;
password_verify: string;
}
+
+export interface PrivateMessageForm {
+ content: string;
+ recipient_id: number;
+ auth?: string;
+}
+
+export interface PrivateMessageFormParams {
+ recipient_id: number;
+}
+
+export interface EditPrivateMessageForm {
+ edit_id: number;
+ content?: string;
+ deleted?: boolean;
+ read?: boolean;
+ auth?: string;
+}
+
+export interface GetPrivateMessagesForm {
+ unread_only: boolean;
+ page?: number;
+ limit?: number;
+ auth?: string;
+}
+
+export interface PrivateMessagesResponse {
+ messages: Array<PrivateMessage>;
+}
+
+export interface PrivateMessageResponse {
+ message: PrivateMessage;
+}
+
+type ResponseType =
+ | SiteResponse
+ | GetFollowedCommunitiesResponse
+ | ListCommunitiesResponse
+ | GetPostsResponse
+ | CreatePostLikeResponse
+ | GetRepliesResponse
+ | GetUserMentionsResponse
+ | ListCategoriesResponse
+ | CommunityResponse
+ | CommentResponse
+ | UserMentionResponse
+ | LoginResponse
+ | GetModlogResponse
+ | SearchResponse
+ | BanFromCommunityResponse
+ | AddModToCommunityResponse
+ | BanUserResponse
+ | AddAdminResponse
+ | PrivateMessageResponse
+ | PrivateMessagesResponse;
+
+export interface WebSocketResponse {
+ op: UserOperation;
+ data: ResponseType;
+ error?: string;
+}
+
+export interface WebSocketJsonResponse {
+ op: string;
+ data: ResponseType;
+ error?: string;
+}
DeleteAccountForm,
PasswordResetForm,
PasswordChangeForm,
+ PrivateMessageForm,
+ EditPrivateMessageForm,
+ GetPrivateMessagesForm,
} from '../interfaces';
import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs';
-import { retryWhen, delay, take } from 'rxjs/operators';
+import { retryWhen, delay } from 'rxjs/operators';
import { UserService } from './';
import { i18n } from '../i18next';
+import { toast } from '../utils';
export class WebSocketService {
private static _instance: WebSocketService;
this.subject.next(this.wsSendWrapper(UserOperation.PasswordChange, form));
}
+ public createPrivateMessage(form: PrivateMessageForm) {
+ this.setAuth(form);
+ this.subject.next(
+ this.wsSendWrapper(UserOperation.CreatePrivateMessage, form)
+ );
+ }
+
+ public editPrivateMessage(form: EditPrivateMessageForm) {
+ this.setAuth(form);
+ this.subject.next(
+ this.wsSendWrapper(UserOperation.EditPrivateMessage, form)
+ );
+ }
+
+ public getPrivateMessages(form: GetPrivateMessagesForm) {
+ this.setAuth(form);
+ this.subject.next(
+ this.wsSendWrapper(UserOperation.GetPrivateMessages, form)
+ );
+ }
+
private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data };
console.log(send);
private setAuth(obj: any, throwErr: boolean = true) {
obj.auth = UserService.Instance.auth;
if (obj.auth == null && throwErr) {
- alert(i18n.t('not_logged_in'));
+ toast(i18n.t('not_logged_in'), 'danger');
throw 'Not logged in';
}
}
list_of_communities: 'List of communities',
number_of_communities: '{{count}} Communities',
community_reqs: 'lowercase, underscores, and no spaces.',
+ create_private_message: 'Create Private Message',
+ send_secure_message: 'Send Secure Message',
+ send_message: 'Send Message',
+ message: 'Message',
edit: 'edit',
reply: 'reply',
cancel: 'Cancel',
replies: 'Replies',
mentions: 'Mentions',
reply_sent: 'Reply sent',
+ message_sent: 'Message sent',
search: 'Search',
overview: 'Overview',
view: 'View',
notifications_error:
'Desktop notifications not available in your browser. Try Firefox or Chrome.',
unread_messages: 'Unread Messages',
+ messages: 'Messages',
password: 'Password',
verify_password: 'Verify Password',
old_password: 'Old Password',
new_password: 'New Password',
no_email_setup: "This server hasn't correctly set up email.",
email: 'Email',
+ matrix_user_id: 'Matrix User',
+ private_message_disclaimer:
+ 'Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.',
send_notifications_to_email: 'Send notifications to Email',
optional: 'Optional',
expires: 'Expires',
joined: 'Joined',
by: 'by',
to: 'to',
+ from: 'from',
transfer_community: 'transfer community',
transfer_site: 'transfer site',
are_you_sure: 'are you sure?',
landing_0:
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
not_logged_in: 'Not logged in.',
+ logged_in: 'Logged in.',
community_ban: 'You have been banned from this community.',
site_ban: 'You have been banned from the site',
couldnt_create_comment: "Couldn't create comment.",
email_already_exists: 'Email already exists.',
couldnt_update_user: "Couldn't update user.",
system_err_login: 'System error. Try logging out and back in.',
+ couldnt_create_private_message: "Couldn't create private message.",
+ no_private_message_edit_allowed: 'Not allowed to edit private message.',
+ couldnt_update_private_message: "Couldn't update private message.",
},
};
import {
UserOperation,
Comment,
+ PrivateMessage,
User,
SortType,
ListingType,
SearchType,
+ WebSocketResponse,
+ WebSocketJsonResponse,
} from './interfaces';
import { UserService } from './services/UserService';
import markdown_it from 'markdown-it';
import markdown_it_container from 'markdown-it-container';
import * as twemoji from 'twemoji';
import * as emojiShortName from 'emoji-short-name';
+import Toastify from 'toastify-js';
export const repoUrl = 'https://github.com/dessalines/lemmy';
export const markdownHelpUrl = 'https://commonmark.org/help/';
.substr(2, 10);
}
-export function msgOp(msg: any): UserOperation {
+export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
let opStr: string = msg.op;
- return UserOperation[opStr];
+ return {
+ op: UserOperation[opStr],
+ data: msg.data,
+ };
}
export const md = new markdown_it({
return url;
}
}
+
+export function isCommentType(item: Comment | PrivateMessage): item is Comment {
+ return (item as Comment).community_id !== undefined;
+}
+
+export function toast(text: string, background: string = 'success') {
+ let backgroundColor = `var(--${background})`;
+ Toastify({
+ text: text,
+ backgroundColor: backgroundColor,
+ }).showToast();
+}
regex-not "^1.0.2"
safe-regex "^1.1.0"
+toastify-js@^1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.6.2.tgz#38af35625797d3d3f51fa09851f0bda449271423"
+ integrity sha512-ECQzgjTjxaElfwp/8e8qoIYx7U5rU2G54e5aiPMv+UtmGOYEitrtNp/Kr8uMgntnQNrDZEQJNGjBtoNnEgR5EA==
+
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"