FIX linefeed
[euphorik.git] / modules / erl / euphorik_protocole.erl
1 % coding: utf-8
2 % Copyright 2008 Grégory Burri
3 %
4 % This file is part of Euphorik.
5 %
6 % Euphorik is free software: you can redistribute it and/or modify
7 % it under the terms of the GNU General Public License as published by
8 % the Free Software Foundation, either version 3 of the License, or
9 % (at your option) any later version.
10 %
11 % Euphorik is distributed in the hope that it will be useful,
12 % but WITHOUT ANY WARRANTY; without even the implied warranty of
13 % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 % GNU General Public License for more details.
15 %
16 % You should have received a copy of the GNU General Public License
17 % along with Euphorik. If not, see <http://www.gnu.org/licenses/>.
18 %
19 % Ce module gére les différents messages envoyés par le client (javascript) via AJAX.
20 % Les messages donnés ainsi que les réponses sont au format JSON.
21 % @author G.Burri
22
23
24 -module(euphorik_protocole).
25 -export([
26 register/2,
27 login/2,
28 logout/1,
29 profile/1,
30 wait_event/1,
31 put_message/1,
32 ban/1,
33 slap/1,
34 put_troll/1,
35 mod_troll/1,
36 del_troll/1,
37 unban_ip/1,
38 list_banned_ips/1,
39 erreur/1
40 ]).
41 -include("../include/euphorik_bd.hrl").
42 -include("../include/euphorik_defines.hrl").
43
44
45 % Une utilisateur s'enregistre avec un tuple {Login, Password}.
46 register([{login, Login}, {password, Password}], IP) ->
47 Can_register = euphorik_bd:can_register(IP),
48 if Can_register ->
49 case euphorik_bd:user_by_login(Login) of
50 {ok, _} ->
51 erreur("Login déjà existant");
52 _ ->
53 User = euphorik_bd:nouveau_user(Login, Password, generer_cookie()),
54 euphorik_bd:update_ip(User#user.id, IP),
55 json_reponse_login_ok(User)
56 end;
57 true ->
58 erreur_register_flood()
59 end;
60 % Enregistrement sans {Login, Password}
61 register([], IP) ->
62 Can_register = euphorik_bd:can_register(IP),
63 if Can_register ->
64 User = euphorik_bd:nouveau_user("<nick>", generer_cookie()),
65 euphorik_bd:update_ip(User#user.id, IP),
66 json_reponse_login_ok(User);
67 true ->
68 erreur_register_flood()
69 end.
70
71 erreur_register_flood() ->
72 erreur("Trop de register (flood)").
73
74
75 % Un utilisateur se logge (avec un couple {login, mot de passe})
76 login([{login, Login}, {password, Password}], IP) ->
77 loginUser(euphorik_bd:user_by_login_password(Login, Password), IP);
78 % Un utilisateur se logge (avec un cookie)
79 login([{cookie, Cookie}], IP) ->
80 loginUser(euphorik_bd:user_by_cookie(Cookie), IP).
81
82 loginUser({ok, User}, IP) ->
83 euphorik_bd:update_ip(User#user.id, IP),
84 euphorik_bd:update_date_derniere_connexion(User#user.id),
85 json_reponse_login_ok(User);
86 loginUser(_, _) ->
87 % ajoute un délais d'attente
88 timer:sleep(?TEMPS_ATTENTE_ERREUR_LOGIN),
89 erreur("Couple login/pass introuvable").
90
91
92 % Renvoie un string() représentant un cookie en base 36. Il y a 10^32 possibillités.
93 generer_cookie() ->
94 {A1, A2, A3} = now(),
95 random:seed(A1, A2, A3),
96 erlang:integer_to_list(random:uniform(math:pow(10, 32)), 36).
97
98
99 % Un utilisateur se délogge.
100 logout(_) ->
101 do_nothing.
102
103
104 % Modification du profile.
105 profile(
106 [
107 {cookie, Cookie},
108 {login, Login},
109 {password, Password},
110 {nick, Pseudo},
111 {email, Email},
112 {css, Css},
113 {nick_format, Nick_format_str},
114 {view_times, View_times},
115 {view_tooltips, View_tooltips},
116 {main_page, Main_page},
117 {conversations, {array, Conversations_json}}
118 ]
119 ) ->
120 % est-ce que les messages auquel on répond existent ?
121 Conversations = lists:foldr(
122 fun({struct, [{root, Root}, {page, Page}]}, Acc) ->
123 Message_existe = euphorik_bd:message_existe(Root),
124 if Message_existe ->
125 [{Root, Page} | Acc];
126 true ->
127 Acc
128 end
129 end,
130 [],
131 Conversations_json
132 ),
133 % TODO : pas très beau, mieux vaut construire un #user
134 case euphorik_bd:set_profile(
135 Cookie,
136 Login,
137 Password,
138 Pseudo,
139 Email,
140 Css,
141 list_to_atom(Nick_format_str),
142 View_times,
143 View_tooltips,
144 Main_page,
145 Conversations) of
146 ok ->
147 json_reponse_ok();
148 login_deja_pris ->
149 erreur("Login déjà pris");
150 _ ->
151 erreur("Impossible de mettre à jour le profile")
152 end.
153
154
155 % Renvoie les messages appropriés.
156 % last_message id et cookie sont facultatifs
157 wait_event([{page, "chat"} | Data]) ->
158 % traitement des inputs
159 Cookie = case lists:keysearch(cookie, 1, Data) of {value, {_, C}} -> C; _ -> inconnu end,
160 Last_message_id = case lists:keysearch(last_message_id, 1, Data) of {value, {_, Id}} -> Id; _ -> 0 end,
161 {value, {_, Message_count}} = lists:keysearch(message_count, 1, Data),
162 Main_page = case lists:keysearch(main_page, 1, Data) of {value, {_, P}} -> P; _ -> 1 end,
163 Troll_id = case lists:keysearch(troll_id, 1, Data) of {value, {_, T}} -> T; _ -> 0 end,
164 {value, {_, {array, Conversations_json}}} = lists:keysearch(conversations, 1, Data),
165 Racines_conversations = lists:map(
166 fun({struct, [{root, Racine}, {page, Page} | Reste]}) ->
167 Last_mess_conv = case Reste of [{last_message_id, L}] -> L; _ -> 0 end,
168 {Racine, Page, Last_mess_conv}
169 end,
170 Conversations_json
171 ),
172 User = case euphorik_bd:user_by_cookie(Cookie) of
173 {ok, U} -> U;
174 _ -> inconnu
175 end,
176 case {mnesia:subscribe({table, minichat, detailed}), mnesia:subscribe({table, troll, detailed})} of
177 {{error, E}, _} -> E;
178 {_, {error, E}} -> E;
179 _ ->
180 % attente d'événements
181 R = wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id),
182 mnesia:unsubscribe({table, minichat, detailed}),
183 mnesia:unsubscribe({table, troll, detailed}),
184 R
185 end;
186 wait_event([{page, "admin"}, {last_troll, Last_troll}]) ->
187 case wait_event_page_admin(Last_troll) of
188 banned_ips_refresh ->
189 {struct,
190 [
191 {reply, "banned_ips_refresh"}
192 ]
193 };
194 {mod, Troll} ->
195 {struct,
196 [
197 {reply, "troll_modified"},
198 {troll_id, Troll#troll.id},
199 {content, Troll#troll.content}
200 ]
201 };
202 {add, Trolls} ->
203 {struct,
204 [
205 {reply, "troll_added"},
206 {trolls, {array,
207 lists:map(
208 fun(T) ->
209 {ok, User} = euphorik_bd:user_by_id(T#troll.id_user),
210 {struct,
211 [
212 {troll_id, T#troll.id},
213 {content, T#troll.content},
214 {author, User#user.pseudo},
215 {author_id, User#user.id}
216 ]
217 }
218 end,
219 Trolls
220 )
221 }}
222 ]
223 };
224 {del, Troll_id} ->
225 {struct,
226 [
227 {reply, "troll_deleted"},
228 {troll_id, Troll_id}
229 ]
230 };
231 _ ->
232 erreur("timeout")
233 end;
234 wait_event(_) ->
235 erreur("Page inconnue").
236
237
238 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id) ->
239 % est-ce que le troll est à jour ?
240 case euphorik_bd:current_troll() of
241 Current when is_record(Current, troll), Current#troll.id =/= Troll_id ->
242 {struct, [
243 {reply, "new_troll"},
244 {troll_id, Current#troll.id},
245 {message_id, euphorik_bd:message_id_associe(Current#troll.id)},
246 {content, Current#troll.content}
247 ]};
248 _ ->
249 % est-ce qu'il y a des nouveaux messages ?
250 case euphorik_minichat_conversation:conversations(Racines_conversations, Message_count, Last_message_id, Main_page) of
251 vide ->
252 wait_event_bd_page_chat(),
253 % TODO : l'appel est-il bien tail-recursive ?
254 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id);
255 Conversations ->
256 % accrochez-vous ca va siouxer ;)
257 {struct, [
258 {reply, "new_messages"},
259 {conversations, {array,
260 lists:map(
261 fun({Conv, Plus}) ->
262 {struct, [
263 {last_page, not Plus},
264 {messages, {array,
265 lists:map(
266 fun({Mess, Repond_a}) ->
267 Est_proprietaire = User =/= inconnu andalso User#user.id =:= Mess#minichat.auteur_id,
268 A_repondu_a_message = User =/= inconnu andalso euphorik_bd:a_repondu_a_message(User#user.id, Mess#minichat.id),
269 Est_une_reponse_a_user = User =/= inconnu andalso euphorik_bd:est_une_reponse_a_user(User#user.id, Mess#minichat.id),
270 {ok, User_mess } = euphorik_bd:user_by_id(Mess#minichat.auteur_id),
271 {struct, [
272 {id, Mess#minichat.id},
273 {user_id, User_mess#user.id},
274 {date, format_date(Mess#minichat.date)},
275 {system, Mess#minichat.auteur_id =:= 0},
276 {owner, Est_proprietaire},
277 {answered, A_repondu_a_message},
278 {is_a_reply, Est_une_reponse_a_user},
279 {nick, Mess#minichat.pseudo},
280 {login, User_mess#user.login},
281 {content, Mess#minichat.contenu},
282 {answer_to, {array, lists:map(
283 fun(Id_mess) ->
284 {ok, M} = euphorik_bd:message_by_id(Id_mess),
285 {ok, User_reponse} = euphorik_bd:user_by_mess(M#minichat.id),
286 {struct, [{id, M#minichat.id}, {nick, M#minichat.pseudo}, {login, User_reponse#user.login}]}
287 end,
288 Repond_a
289 )}},
290 {ek_master, User_mess#user.ek_master}
291 ]}
292 end,
293 Conv
294 )
295 }}
296 ]}
297 end,
298 Conversations
299 )
300 }}
301 ]}
302 end
303 end.
304
305
306 % Attend un événement lié à la page 'chat'.
307 wait_event_bd_page_chat() ->
308 receive % attente d'un post
309 {mnesia_table_event, {write, minichat, _Message, [], _}} ->
310 ok;
311 {mnesia_table_event, {write, troll, Troll, [Old_troll | _], _}} when Troll#troll.date_post =/= undefined, Old_troll#troll.date_post == undefined ->
312 ok;
313 {tcp_closed, _} ->
314 exit(normal);
315 _ ->
316 wait_event_bd_page_chat()
317 % 60 minutes de timeout (on ne sais jamais)
318 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
319 after 1000 * 60 * 60 ->
320 timeout
321 end.
322
323
324 % Attent un événement concernant la page admin
325 % Renvoie les trolls manquants posté après Last_id ou banned_ips_refresh.
326 % Si pas de trolls alors attend un événement tel qu'un ajout, une modification ou une suppression.
327 % renvoie :
328 % {mod, Troll}
329 % ou {add, [Trolls]}
330 % ou {del, Troll_id}
331 % ou banned_ips_refresh
332 % ou timeout
333 wait_event_page_admin(Last_id) ->
334 case {mnesia:subscribe({table, troll, detailed}), mnesia:subscribe({table, ip_table, detailed})} of
335 {{error, E}, _ } -> E;
336 {_, {error, E}} -> E;
337 _ ->
338 R = case euphorik_bd:trolls(Last_id) of
339 [] -> % pas de trolls
340 wait_event_page_admin();
341 Trolls ->
342 {add, Trolls}
343 end,
344 mnesia:unsubscribe({table, troll, detailed}),
345 mnesia:unsubscribe({table, ip_table, detailed}),
346 R
347 end.
348
349 wait_event_page_admin() ->
350 % s'il n'y a pas de trolls que l'utilisateur n'a pas connaissance alors on attend un événement
351 receive
352 % cas où un troll est choisit comme courant
353 {mnesia_table_event, {write, troll, Troll, [Old_troll | _], _}}
354 when Old_troll#troll.date_post =:= undefined, Troll#troll.date_post =/= undefined ->
355 {del, Troll#troll.id};
356 {mnesia_table_event, {write, troll, Troll, [_Old_troll | _], _}} ->
357 {mod, Troll};
358 {mnesia_table_event, {write, troll, Troll, [], _}} ->
359 {add, [Troll]};
360 {mnesia_table_event, {delete, troll, {troll, Id}, _, _}} ->
361 {del, Id};
362 {mnesia_table_event, {write, ip_table, IP, [Old_IP | _], _}}
363 when Old_IP#ip_table.ban =/= IP#ip_table.ban; Old_IP#ip_table.ban_duration =/= IP#ip_table.ban_duration ->
364 banned_ips_refresh;
365 {tcp_closed, _} ->
366 exit(normal);
367 _ ->
368 wait_event_page_admin()
369 % 60 minutes de timeout (on ne sais jamais)
370 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
371 after 1000 * 60 * 60 ->
372 timeout
373 end.
374
375
376 % Un utilisateur envoie un message
377 put_message(
378 [
379 {cookie, Cookie},
380 {nick, Nick},
381 {content, Content},
382 {answer_to, {array, Answer_to}}
383 ]
384 ) ->
385 case euphorik_bd:user_by_cookie(Cookie) of
386 {ok, User} ->
387 case euphorik_bd:est_banni(User#user.id) of
388 {true, Temps_restant} ->
389 erreur("Vous êtes banni pour encore " ++ format_minutes(Temps_restant));
390 _ ->
391 Strip_content = string:strip(Content),
392 if Strip_content =:= [] ->
393 erreur("Message vide");
394 true ->
395 % TODO : non-atomique (update_pseudo+nouveau_message)
396 euphorik_bd:update_pseudo_user(User#user.id, Nick),
397 case euphorik_bd:nouveau_message(Strip_content, User#user.id, Answer_to) of
398 {erreur, R} -> erreur("Impossible d'ajouter un nouveau message. Raison : " ++ R);
399 _ ->
400 json_reponse_ok()
401 end
402 end
403 end;
404 _ ->
405 erreur("Utilisateur inconnu")
406 end.
407
408
409 % bannissement d'un utilisateur (son ip est bannie)
410 ban(
411 [
412 {cookie, Cookie},
413 {duration, Duration},
414 {user_id, User_id},
415 {reason, Reason}
416 ]) ->
417 % controle que l'utilisateur est un admin
418 case euphorik_bd:user_by_cookie(Cookie) of
419 {ok, User1 = #user{ek_master = true}} ->
420 case euphorik_bd:user_by_id(User_id) of
421 {ok, User1} ->
422 erreur("Il n'est pas possible de s'auto bannir");
423 {ok, User2 = #user{ek_master = false}} ->
424 euphorik_bd:ban(User2#user.last_ip, Duration),
425 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("''~s~s'' est ~s pour ~s.~s",
426 [
427 User2#user.pseudo,
428 if User2#user.login =:= [] -> ""; true -> " (" ++ User2#user.login ++ ")" end,
429 if Duration =< 15 -> "kické"; true -> "banni" end,
430 format_minutes(Duration),
431 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
432 ]
433 ))),
434 json_reponse_ok();
435 {ok, _} ->
436 erreur("L'utilisateur est lui même un ekMaster");
437 _ ->
438 erreur("Utilisateur à bannir inconnu")
439 end;
440 _ ->
441 erreur("Utilisateur inconnu ou non ek master")
442 end.
443
444
445 % slapage d'un user (avertissement)
446 slap(
447 [
448 {cookie, Cookie},
449 {user_id, User_id},
450 {reason, Reason}
451 ]) ->
452 % controle que l'utilisateur est un admin
453 case euphorik_bd:user_by_cookie(Cookie) of
454 {ok, User1 = #user{ek_master = true}} ->
455 case euphorik_bd:user_by_id(User_id) of
456 {ok, User1} ->
457 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s s'auto slap~s.",
458 [
459 User1#user.pseudo,
460 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end
461 ]
462 ))),
463 json_reponse_ok();
464 {ok, User2 = #user{ek_master = false}} ->
465 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s se fait slaper par ~s.~s",
466 [
467 User2#user.pseudo,
468 User1#user.pseudo,
469 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
470 ]
471 ))),
472 json_reponse_ok();
473 {ok, _} ->
474 erreur("L'utilisateur est lui même un ekMaster");
475 _ ->
476 erreur("Utilisateur à slaper inconnu")
477 end;
478 _ ->
479 erreur("Utilisateur inconnu ou non ek master")
480 end.
481
482
483 put_troll(
484 [
485 {cookie, Cookie},
486 {content, Content}
487 ]
488 ) ->
489 % controle que l'utilisateur est un admin
490 case euphorik_bd:user_by_cookie(Cookie) of
491 {ok, User = #user{ek_master = true}} ->
492 case euphorik_bd:put_troll(User#user.id, Content) of
493 max_troll_reached_per_user ->
494 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum par utilisateur est atteint : ~w ", [?NB_MAX_TROLL_WAITING_BY_USER])));
495 max_troll_reached ->
496 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum en attente est atteint : ~w ", [?NB_MAX_TROLL_WAITING])));
497 _Id ->
498 json_reponse_ok()
499 end;
500 _ ->
501 erreur("Seul les ekMaster peuvent proposer des trolls")
502 end.
503
504
505 mod_troll(
506 [
507 {cookie, Cookie},
508 {troll_id, Troll_id},
509 {content, Content}
510 ]
511 ) ->
512 % controle que l'utilisateur est un admin
513 case euphorik_bd:user_by_cookie(Cookie) of
514 {ok, User = #user{ek_master = true}} ->
515 User_id = User#user.id,
516 case euphorik_bd:troll_by_id(Troll_id) of
517 {ok, #troll{id_user = User_id}} ->
518 euphorik_bd:mod_troll(Troll_id, Content),
519 json_reponse_ok();
520 _ ->
521 erreur("Vous ne posséder pas ce troll")
522 end;
523 _ ->
524 erreur("Seul les ekMaster peuvent proposer des trolls")
525 end.
526
527
528 del_troll(
529 [
530 {cookie, Cookie},
531 {troll_id, Troll_id}
532 ]
533 ) ->
534 % controle que l'utilisateur est un admin
535 case euphorik_bd:user_by_cookie(Cookie) of
536 {ok, User = #user{ek_master = true}} ->
537 User_id = User#user.id,
538 case euphorik_bd:troll_by_id(Troll_id) of
539 {ok, #troll{id_user = User_id}} ->
540 euphorik_bd:del_troll(Troll_id),
541 json_reponse_ok();
542 _ ->
543 erreur("Vous ne posséder pas ce troll")
544 end;
545 _ ->
546 erreur("Seul les ekMaster peuvent proposer des trolls")
547 end.
548
549
550 unban_ip(
551 [
552 {cookie, Cookie},
553 {ip, IP}
554 ]
555 ) ->
556 case euphorik_bd:user_by_cookie(Cookie) of
557 {ok, #user{ek_master = true}} ->
558 euphorik_bd:deban(euphorik_common:unserialize_ip(IP)),
559 json_reponse_ok();
560 _ ->
561 erreur("Seul les ekMaster peuvent connaitre la liste des ips bannies")
562 end.
563
564
565 list_banned_ips(
566 [
567 {cookie, Cookie}
568 ]
569 ) ->
570 case euphorik_bd:user_by_cookie(Cookie) of
571 {ok, #user{ek_master = true}} ->
572 {
573 struct,
574 [
575 {reply, "list_banned_ips"},
576 {list, {array, lists:map(
577 fun({IP, T, Users}) ->
578 {struct,
579 [
580 {ip, euphorik_common:serialize_ip(IP)},
581 {remaining_time, format_minutes(T)},
582 {users, {array, lists:map(
583 fun({Pseudo, Login}) ->
584 {struct,
585 [
586 {nick, Pseudo},
587 {login, Login}
588 ]
589 }
590 end,
591 Users
592 )}}
593 ]
594 }
595 end,
596 euphorik_bd:list_ban()
597 )}}
598 ]
599 };
600 _ ->
601 erreur("Seul les ekMaster peuvent connaitre la liste des ips bannies")
602 end.
603
604
605 % Construit une erreur
606 erreur(Message) ->
607 {
608 struct, [
609 {reply, "error"},
610 {error_message, Message}
611 ]
612 }.
613
614
615 % Formatage de minutes.
616 % par exemple : "1min", "45min", "1h23min", "1jour 2h34min"
617 format_minutes(Min) ->
618 Jours = Min div (60 * 24),
619 Heures = Min rem (60 * 24) div 60,
620 Minutes = Min rem (60),
621 if Jours =/= 0 -> integer_to_list(Jours) ++ " Jour" ++ if Jours > 1 -> "s"; true -> "" end ++ " "; true -> "" end ++
622 if Heures =/= 0 -> integer_to_list(Heures) ++ " heure" ++ if Heures > 1 -> "s"; true -> "" end; true -> "" end ++
623 if Minutes == 0 ->
624 "";
625 true ->
626 " " ++ integer_to_list(Minutes) ++ " minute" ++ if Minutes > 1 -> "s"; true -> "" end
627 end.
628
629
630 % Formatage d'une heure
631 % local_time() -> string
632 format_date(Date) ->
633 DateLocal = calendar:now_to_local_time(Date),
634 DateNowLocal = calendar:local_time(),
635 {{Annee, Mois, Jour}, {Heure, Minute, Seconde}} = DateLocal,
636 {{AnneeNow, _, _}, {_, _, _}} = DateNowLocal,
637 Hier = calendar:date_to_gregorian_days(element(1, DateLocal)) =:= calendar:date_to_gregorian_days(element(1, DateNowLocal)) - 1,
638 lists:flatten(
639 if element(1, DateLocal) =:= element(1, DateNowLocal) ->
640 "";
641 Hier ->
642 "Hier ";
643 Annee =:= AnneeNow ->
644 io_lib:format("~2.10.0B/~2.10.0B ", [Jour, Mois]);
645 true ->
646 io_lib:format("~2.10.0B/~2.10.0B/~B ", [Jour, Mois, Annee])
647 end ++
648 io_lib:format("~2.10.0B:~2.10.0B:~2.10.0B", [Heure, Minute, Seconde])
649 ).
650
651
652 json_reponse_ok() ->
653 {struct, [{reply, "ok"}]}.
654
655
656 json_reponse_login_ok(User) ->
657 {
658 struct, [
659 {reply, "login"},
660 {status, if (User#user.password =/= []) and (User#user.login =/= []) -> "auth_registered"; true -> "auth_not_registered" end},
661 {cookie, User#user.cookie},
662 {id, User#user.id},
663 {nick, User#user.pseudo},
664 {login, User#user.login},
665 {email, User#user.email},
666 {css, User#user.css},
667 {nick_format, atom_to_list(User#user.nick_format)},
668 {view_times, User#user.view_times},
669 {view_tooltips, User#user.view_tooltips},
670 {main_page, User#user.page_principale},
671 {conversations,
672 {array,
673 lists:map(
674 fun(C) ->
675 {struct,
676 [
677 {root, element(1, C)},
678 {page, element(2, C)}
679 ]
680 }
681 end,
682 User#user.conversations
683 )
684 }
685 },
686 {ek_master, User#user.ek_master}
687 ]
688 }.