]> Untitled Git - lemmy.git/blob - lemmy_api/src/comment.rs
Fixing some clippy warnings.
[lemmy.git] / lemmy_api / src / comment.rs
1 use crate::{
2   check_community_ban,
3   check_downvotes_enabled,
4   collect_moderated_communities,
5   get_post,
6   get_user_from_jwt,
7   get_user_from_jwt_opt,
8   is_mod_or_admin,
9   Perform,
10 };
11 use actix_web::web::Data;
12 use lemmy_apub::{ApubLikeableType, ApubObjectType};
13 use lemmy_db::{
14   source::{
15     comment::{Comment, CommentForm, CommentLike, CommentLikeForm, CommentSaved, CommentSavedForm},
16     comment_report::{CommentReport, CommentReportForm},
17     moderator::{ModRemoveComment, ModRemoveCommentForm},
18   },
19   views::{
20     comment_report_view::{CommentReportQueryBuilder, CommentReportView},
21     comment_view::{CommentQueryBuilder, CommentView},
22   },
23   Crud,
24   Likeable,
25   ListingType,
26   Reportable,
27   Saveable,
28   SortType,
29 };
30 use lemmy_structs::{blocking, comment::*, send_local_notifs};
31 use lemmy_utils::{
32   apub::{make_apub_endpoint, EndpointType},
33   utils::{remove_slurs, scrape_text_for_mentions},
34   APIError,
35   ConnectionId,
36   LemmyError,
37 };
38 use lemmy_websocket::{
39   messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
40   LemmyContext,
41   UserOperation,
42 };
43 use std::str::FromStr;
44
45 #[async_trait::async_trait(?Send)]
46 impl Perform for CreateComment {
47   type Response = CommentResponse;
48
49   async fn perform(
50     &self,
51     context: &Data<LemmyContext>,
52     websocket_id: Option<ConnectionId>,
53   ) -> Result<CommentResponse, LemmyError> {
54     let data: &CreateComment = &self;
55     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
56
57     let content_slurs_removed = remove_slurs(&data.content.to_owned());
58
59     let comment_form = CommentForm {
60       content: content_slurs_removed,
61       parent_id: data.parent_id.to_owned(),
62       post_id: data.post_id,
63       creator_id: user.id,
64       removed: None,
65       deleted: None,
66       read: None,
67       published: None,
68       updated: None,
69       ap_id: None,
70       local: true,
71     };
72
73     // Check for a community ban
74     let post_id = data.post_id;
75     let post = get_post(post_id, context.pool()).await?;
76
77     check_community_ban(user.id, post.community_id, context.pool()).await?;
78
79     // Check if post is locked, no new comments
80     if post.locked {
81       return Err(APIError::err("locked").into());
82     }
83
84     // Create the comment
85     let comment_form2 = comment_form.clone();
86     let inserted_comment = match blocking(context.pool(), move |conn| {
87       Comment::create(&conn, &comment_form2)
88     })
89     .await?
90     {
91       Ok(comment) => comment,
92       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
93     };
94
95     // Necessary to update the ap_id
96     let inserted_comment_id = inserted_comment.id;
97     let updated_comment: Comment = match blocking(context.pool(), move |conn| {
98       let apub_id =
99         make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
100       Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
101     })
102     .await?
103     {
104       Ok(comment) => comment,
105       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
106     };
107
108     updated_comment.send_create(&user, context).await?;
109
110     // Scan the comment for user mentions, add those rows
111     let post_id = post.id;
112     let mentions = scrape_text_for_mentions(&comment_form.content);
113     let recipient_ids = send_local_notifs(
114       mentions,
115       updated_comment.clone(),
116       &user,
117       post,
118       context.pool(),
119       true,
120     )
121     .await?;
122
123     // You like your own comment by default
124     let like_form = CommentLikeForm {
125       comment_id: inserted_comment.id,
126       post_id,
127       user_id: user.id,
128       score: 1,
129     };
130
131     let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
132     if blocking(context.pool(), like).await?.is_err() {
133       return Err(APIError::err("couldnt_like_comment").into());
134     }
135
136     updated_comment.send_like(&user, context).await?;
137
138     let user_id = user.id;
139     let comment_view = blocking(context.pool(), move |conn| {
140       CommentView::read(&conn, inserted_comment.id, Some(user_id))
141     })
142     .await??;
143
144     let mut res = CommentResponse {
145       comment_view,
146       recipient_ids,
147       form_id: data.form_id.to_owned(),
148     };
149
150     context.chat_server().do_send(SendComment {
151       op: UserOperation::CreateComment,
152       comment: res.clone(),
153       websocket_id,
154     });
155
156     // strip out the recipient_ids, so that
157     // users don't get double notifs
158     // TODO Do this in a different way
159     res.recipient_ids = Vec::new();
160
161     Ok(res)
162   }
163 }
164
165 #[async_trait::async_trait(?Send)]
166 impl Perform for EditComment {
167   type Response = CommentResponse;
168
169   async fn perform(
170     &self,
171     context: &Data<LemmyContext>,
172     websocket_id: Option<ConnectionId>,
173   ) -> Result<CommentResponse, LemmyError> {
174     let data: &EditComment = &self;
175     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
176
177     let edit_id = data.edit_id;
178     let orig_comment = blocking(context.pool(), move |conn| {
179       CommentView::read(&conn, edit_id, None)
180     })
181     .await??;
182
183     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
184
185     // Verify that only the creator can edit
186     if user.id != orig_comment.creator.id {
187       return Err(APIError::err("no_comment_edit_allowed").into());
188     }
189
190     // Do the update
191     let content_slurs_removed = remove_slurs(&data.content.to_owned());
192     let edit_id = data.edit_id;
193     let updated_comment = match blocking(context.pool(), move |conn| {
194       Comment::update_content(conn, edit_id, &content_slurs_removed)
195     })
196     .await?
197     {
198       Ok(comment) => comment,
199       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
200     };
201
202     // Send the apub update
203     updated_comment.send_update(&user, context).await?;
204
205     // Do the mentions / recipients
206     let updated_comment_content = updated_comment.content.to_owned();
207     let mentions = scrape_text_for_mentions(&updated_comment_content);
208     let recipient_ids = send_local_notifs(
209       mentions,
210       updated_comment,
211       &user,
212       orig_comment.post,
213       context.pool(),
214       false,
215     )
216     .await?;
217
218     let edit_id = data.edit_id;
219     let user_id = user.id;
220     let comment_view = blocking(context.pool(), move |conn| {
221       CommentView::read(conn, edit_id, Some(user_id))
222     })
223     .await??;
224
225     let mut res = CommentResponse {
226       comment_view,
227       recipient_ids,
228       form_id: data.form_id.to_owned(),
229     };
230
231     context.chat_server().do_send(SendComment {
232       op: UserOperation::EditComment,
233       comment: res.clone(),
234       websocket_id,
235     });
236
237     // strip out the recipient_ids, so that
238     // users don't get double notifs
239     // TODO again
240     res.recipient_ids = Vec::new();
241
242     Ok(res)
243   }
244 }
245
246 #[async_trait::async_trait(?Send)]
247 impl Perform for DeleteComment {
248   type Response = CommentResponse;
249
250   async fn perform(
251     &self,
252     context: &Data<LemmyContext>,
253     websocket_id: Option<ConnectionId>,
254   ) -> Result<CommentResponse, LemmyError> {
255     let data: &DeleteComment = &self;
256     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
257
258     let edit_id = data.edit_id;
259     let orig_comment = blocking(context.pool(), move |conn| {
260       CommentView::read(&conn, edit_id, None)
261     })
262     .await??;
263
264     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
265
266     // Verify that only the creator can delete
267     if user.id != orig_comment.creator.id {
268       return Err(APIError::err("no_comment_edit_allowed").into());
269     }
270
271     // Do the delete
272     let deleted = data.deleted;
273     let updated_comment = match blocking(context.pool(), move |conn| {
274       Comment::update_deleted(conn, edit_id, deleted)
275     })
276     .await?
277     {
278       Ok(comment) => comment,
279       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
280     };
281
282     // Send the apub message
283     if deleted {
284       updated_comment.send_delete(&user, context).await?;
285     } else {
286       updated_comment.send_undo_delete(&user, context).await?;
287     }
288
289     // Refetch it
290     let edit_id = data.edit_id;
291     let user_id = user.id;
292     let comment_view = blocking(context.pool(), move |conn| {
293       CommentView::read(conn, edit_id, Some(user_id))
294     })
295     .await??;
296
297     // Build the recipients
298     let comment_view_2 = comment_view.clone();
299     let mentions = vec![];
300     let recipient_ids = send_local_notifs(
301       mentions,
302       updated_comment,
303       &user,
304       comment_view_2.post,
305       context.pool(),
306       false,
307     )
308     .await?;
309
310     let mut res = CommentResponse {
311       comment_view,
312       recipient_ids,
313       form_id: None, // TODO a comment delete might clear forms?
314     };
315
316     context.chat_server().do_send(SendComment {
317       op: UserOperation::DeleteComment,
318       comment: res.clone(),
319       websocket_id,
320     });
321
322     // strip out the recipient_ids, so that
323     // users don't get double notifs
324     // TODO again
325     res.recipient_ids = Vec::new();
326
327     Ok(res)
328   }
329 }
330
331 #[async_trait::async_trait(?Send)]
332 impl Perform for RemoveComment {
333   type Response = CommentResponse;
334
335   async fn perform(
336     &self,
337     context: &Data<LemmyContext>,
338     websocket_id: Option<ConnectionId>,
339   ) -> Result<CommentResponse, LemmyError> {
340     let data: &RemoveComment = &self;
341     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
342
343     let edit_id = data.edit_id;
344     let orig_comment = blocking(context.pool(), move |conn| {
345       CommentView::read(&conn, edit_id, None)
346     })
347     .await??;
348
349     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
350
351     // Verify that only a mod or admin can remove
352     is_mod_or_admin(context.pool(), user.id, orig_comment.community.id).await?;
353
354     // Do the remove
355     let removed = data.removed;
356     let updated_comment = match blocking(context.pool(), move |conn| {
357       Comment::update_removed(conn, edit_id, removed)
358     })
359     .await?
360     {
361       Ok(comment) => comment,
362       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
363     };
364
365     // Mod tables
366     let form = ModRemoveCommentForm {
367       mod_user_id: user.id,
368       comment_id: data.edit_id,
369       removed: Some(removed),
370       reason: data.reason.to_owned(),
371     };
372     blocking(context.pool(), move |conn| {
373       ModRemoveComment::create(conn, &form)
374     })
375     .await??;
376
377     // Send the apub message
378     if removed {
379       updated_comment.send_remove(&user, context).await?;
380     } else {
381       updated_comment.send_undo_remove(&user, context).await?;
382     }
383
384     // Refetch it
385     let edit_id = data.edit_id;
386     let user_id = user.id;
387     let comment_view = blocking(context.pool(), move |conn| {
388       CommentView::read(conn, edit_id, Some(user_id))
389     })
390     .await??;
391
392     // Build the recipients
393     let comment_view_2 = comment_view.clone();
394
395     let mentions = vec![];
396     let recipient_ids = send_local_notifs(
397       mentions,
398       updated_comment,
399       &user,
400       comment_view_2.post,
401       context.pool(),
402       false,
403     )
404     .await?;
405
406     let mut res = CommentResponse {
407       comment_view,
408       recipient_ids,
409       form_id: None, // TODO maybe this might clear other forms
410     };
411
412     context.chat_server().do_send(SendComment {
413       op: UserOperation::RemoveComment,
414       comment: res.clone(),
415       websocket_id,
416     });
417
418     // strip out the recipient_ids, so that
419     // users don't get double notifs
420     // TODO again
421     res.recipient_ids = Vec::new();
422
423     Ok(res)
424   }
425 }
426
427 #[async_trait::async_trait(?Send)]
428 impl Perform for MarkCommentAsRead {
429   type Response = CommentResponse;
430
431   async fn perform(
432     &self,
433     context: &Data<LemmyContext>,
434     _websocket_id: Option<ConnectionId>,
435   ) -> Result<CommentResponse, LemmyError> {
436     let data: &MarkCommentAsRead = &self;
437     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
438
439     let comment_id = data.comment_id;
440     let orig_comment = blocking(context.pool(), move |conn| {
441       CommentView::read(&conn, comment_id, None)
442     })
443     .await??;
444
445     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
446
447     // Verify that only the recipient can mark as read
448     if user.id != orig_comment.get_recipient_id() {
449       return Err(APIError::err("no_comment_edit_allowed").into());
450     }
451
452     // Do the mark as read
453     let read = data.read;
454     match blocking(context.pool(), move |conn| {
455       Comment::update_read(conn, comment_id, read)
456     })
457     .await?
458     {
459       Ok(comment) => comment,
460       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
461     };
462
463     // Refetch it
464     let edit_id = data.comment_id;
465     let user_id = user.id;
466     let comment_view = blocking(context.pool(), move |conn| {
467       CommentView::read(conn, edit_id, Some(user_id))
468     })
469     .await??;
470
471     let res = CommentResponse {
472       comment_view,
473       recipient_ids: Vec::new(),
474       form_id: None,
475     };
476
477     Ok(res)
478   }
479 }
480
481 #[async_trait::async_trait(?Send)]
482 impl Perform for SaveComment {
483   type Response = CommentResponse;
484
485   async fn perform(
486     &self,
487     context: &Data<LemmyContext>,
488     _websocket_id: Option<ConnectionId>,
489   ) -> Result<CommentResponse, LemmyError> {
490     let data: &SaveComment = &self;
491     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
492
493     let comment_saved_form = CommentSavedForm {
494       comment_id: data.comment_id,
495       user_id: user.id,
496     };
497
498     if data.save {
499       let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
500       if blocking(context.pool(), save_comment).await?.is_err() {
501         return Err(APIError::err("couldnt_save_comment").into());
502       }
503     } else {
504       let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
505       if blocking(context.pool(), unsave_comment).await?.is_err() {
506         return Err(APIError::err("couldnt_save_comment").into());
507       }
508     }
509
510     let comment_id = data.comment_id;
511     let user_id = user.id;
512     let comment_view = blocking(context.pool(), move |conn| {
513       CommentView::read(conn, comment_id, Some(user_id))
514     })
515     .await??;
516
517     Ok(CommentResponse {
518       comment_view,
519       recipient_ids: Vec::new(),
520       form_id: None,
521     })
522   }
523 }
524
525 #[async_trait::async_trait(?Send)]
526 impl Perform for CreateCommentLike {
527   type Response = CommentResponse;
528
529   async fn perform(
530     &self,
531     context: &Data<LemmyContext>,
532     websocket_id: Option<ConnectionId>,
533   ) -> Result<CommentResponse, LemmyError> {
534     let data: &CreateCommentLike = &self;
535     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
536
537     let mut recipient_ids = Vec::new();
538
539     // Don't do a downvote if site has downvotes disabled
540     check_downvotes_enabled(data.score, context.pool()).await?;
541
542     let comment_id = data.comment_id;
543     let orig_comment = blocking(context.pool(), move |conn| {
544       CommentView::read(&conn, comment_id, None)
545     })
546     .await??;
547
548     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
549
550     // Add parent user to recipients
551     recipient_ids.push(orig_comment.get_recipient_id());
552
553     let like_form = CommentLikeForm {
554       comment_id: data.comment_id,
555       post_id: orig_comment.post.id,
556       user_id: user.id,
557       score: data.score,
558     };
559
560     // Remove any likes first
561     let user_id = user.id;
562     blocking(context.pool(), move |conn| {
563       CommentLike::remove(conn, user_id, comment_id)
564     })
565     .await??;
566
567     // Only add the like if the score isnt 0
568     let comment = orig_comment.comment;
569     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
570     if do_add {
571       let like_form2 = like_form.clone();
572       let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
573       if blocking(context.pool(), like).await?.is_err() {
574         return Err(APIError::err("couldnt_like_comment").into());
575       }
576
577       if like_form.score == 1 {
578         comment.send_like(&user, context).await?;
579       } else if like_form.score == -1 {
580         comment.send_dislike(&user, context).await?;
581       }
582     } else {
583       comment.send_undo_like(&user, context).await?;
584     }
585
586     // Have to refetch the comment to get the current state
587     let comment_id = data.comment_id;
588     let user_id = user.id;
589     let liked_comment = blocking(context.pool(), move |conn| {
590       CommentView::read(conn, comment_id, Some(user_id))
591     })
592     .await??;
593
594     let mut res = CommentResponse {
595       comment_view: liked_comment,
596       recipient_ids,
597       form_id: None,
598     };
599
600     context.chat_server().do_send(SendComment {
601       op: UserOperation::CreateCommentLike,
602       comment: res.clone(),
603       websocket_id,
604     });
605
606     // strip out the recipient_ids, so that
607     // users don't get double notifs
608     res.recipient_ids = Vec::new();
609     // TODO why
610
611     Ok(res)
612   }
613 }
614
615 #[async_trait::async_trait(?Send)]
616 impl Perform for GetComments {
617   type Response = GetCommentsResponse;
618
619   async fn perform(
620     &self,
621     context: &Data<LemmyContext>,
622     _websocket_id: Option<ConnectionId>,
623   ) -> Result<GetCommentsResponse, LemmyError> {
624     let data: &GetComments = &self;
625     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
626     let user_id = user.map(|u| u.id);
627
628     let type_ = ListingType::from_str(&data.type_)?;
629     let sort = SortType::from_str(&data.sort)?;
630
631     let community_id = data.community_id;
632     let community_name = data.community_name.to_owned();
633     let page = data.page;
634     let limit = data.limit;
635     let comments = blocking(context.pool(), move |conn| {
636       CommentQueryBuilder::create(conn)
637         .listing_type(type_)
638         .sort(&sort)
639         .community_id(community_id)
640         .community_name(community_name)
641         .my_user_id(user_id)
642         .page(page)
643         .limit(limit)
644         .list()
645     })
646     .await?;
647     let comments = match comments {
648       Ok(comments) => comments,
649       Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
650     };
651
652     Ok(GetCommentsResponse { comments })
653   }
654 }
655
656 /// Creates a comment report and notifies the moderators of the community
657 #[async_trait::async_trait(?Send)]
658 impl Perform for CreateCommentReport {
659   type Response = CreateCommentReportResponse;
660
661   async fn perform(
662     &self,
663     context: &Data<LemmyContext>,
664     websocket_id: Option<ConnectionId>,
665   ) -> Result<CreateCommentReportResponse, LemmyError> {
666     let data: &CreateCommentReport = &self;
667     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
668
669     // check size of report and check for whitespace
670     let reason = data.reason.trim();
671     if reason.is_empty() {
672       return Err(APIError::err("report_reason_required").into());
673     }
674     if reason.len() > 1000 {
675       return Err(APIError::err("report_too_long").into());
676     }
677
678     let user_id = user.id;
679     let comment_id = data.comment_id;
680     let comment_view = blocking(context.pool(), move |conn| {
681       CommentView::read(&conn, comment_id, None)
682     })
683     .await??;
684
685     check_community_ban(user_id, comment_view.community.id, context.pool()).await?;
686
687     let report_form = CommentReportForm {
688       creator_id: user_id,
689       comment_id,
690       original_comment_text: comment_view.comment.content,
691       reason: data.reason.to_owned(),
692     };
693
694     let report = match blocking(context.pool(), move |conn| {
695       CommentReport::report(conn, &report_form)
696     })
697     .await?
698     {
699       Ok(report) => report,
700       Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
701     };
702
703     let res = CreateCommentReportResponse { success: true };
704
705     context.chat_server().do_send(SendUserRoomMessage {
706       op: UserOperation::CreateCommentReport,
707       response: res.clone(),
708       recipient_id: user.id,
709       websocket_id,
710     });
711
712     context.chat_server().do_send(SendModRoomMessage {
713       op: UserOperation::CreateCommentReport,
714       response: report,
715       community_id: comment_view.community.id,
716       websocket_id,
717     });
718
719     Ok(res)
720   }
721 }
722
723 /// Resolves or unresolves a comment report and notifies the moderators of the community
724 #[async_trait::async_trait(?Send)]
725 impl Perform for ResolveCommentReport {
726   type Response = ResolveCommentReportResponse;
727
728   async fn perform(
729     &self,
730     context: &Data<LemmyContext>,
731     websocket_id: Option<ConnectionId>,
732   ) -> Result<ResolveCommentReportResponse, LemmyError> {
733     let data: &ResolveCommentReport = &self;
734     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
735
736     let report_id = data.report_id;
737     let report = blocking(context.pool(), move |conn| {
738       CommentReportView::read(&conn, report_id)
739     })
740     .await??;
741
742     let user_id = user.id;
743     is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
744
745     let resolved = data.resolved;
746     let resolve_fun = move |conn: &'_ _| {
747       if resolved {
748         CommentReport::resolve(conn, report_id, user_id)
749       } else {
750         CommentReport::unresolve(conn, report_id, user_id)
751       }
752     };
753
754     if blocking(context.pool(), resolve_fun).await?.is_err() {
755       return Err(APIError::err("couldnt_resolve_report").into());
756     };
757
758     let report_id = data.report_id;
759     let res = ResolveCommentReportResponse {
760       report_id,
761       resolved,
762     };
763
764     context.chat_server().do_send(SendModRoomMessage {
765       op: UserOperation::ResolveCommentReport,
766       response: res.clone(),
767       community_id: report.community.id,
768       websocket_id,
769     });
770
771     Ok(res)
772   }
773 }
774
775 /// Lists comment reports for a community if an id is supplied
776 /// or returns all comment reports for communities a user moderates
777 #[async_trait::async_trait(?Send)]
778 impl Perform for ListCommentReports {
779   type Response = ListCommentReportsResponse;
780
781   async fn perform(
782     &self,
783     context: &Data<LemmyContext>,
784     websocket_id: Option<ConnectionId>,
785   ) -> Result<ListCommentReportsResponse, LemmyError> {
786     let data: &ListCommentReports = &self;
787     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
788
789     let user_id = user.id;
790     let community_id = data.community;
791     let community_ids =
792       collect_moderated_communities(user_id, community_id, context.pool()).await?;
793
794     let page = data.page;
795     let limit = data.limit;
796     let comments = blocking(context.pool(), move |conn| {
797       CommentReportQueryBuilder::create(conn)
798         .community_ids(community_ids)
799         .page(page)
800         .limit(limit)
801         .list()
802     })
803     .await??;
804
805     let res = ListCommentReportsResponse { comments };
806
807     context.chat_server().do_send(SendUserRoomMessage {
808       op: UserOperation::ListCommentReports,
809       response: res.clone(),
810       recipient_id: user.id,
811       websocket_id,
812     });
813
814     Ok(res)
815   }
816 }