MOD insignifiant
[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(1000),
89 erreur("Erreur login").
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 {main_page, Main_page},
115 {conversations, {array, Conversations_json}}
116 ]
117 ) ->
118 % est-ce que les messages auquel on répond existent ?
119 Conversations = lists:foldr(
120 fun({struct, [{root, Root}, {page, Page}]}, Acc) ->
121 Message_existe = euphorik_bd:message_existe(Root),
122 if Message_existe ->
123 [{Root, Page} | Acc];
124 true ->
125 Acc
126 end
127 end,
128 [],
129 Conversations_json
130 ),
131 case euphorik_bd:set_profile(Cookie, Login, Password, Pseudo, Email, Css, list_to_atom(Nick_format_str), Main_page, Conversations) of
132 ok ->
133 json_reponse_ok();
134 login_deja_pris ->
135 erreur("Login déjà pris");
136 _ ->
137 erreur("Impossible de mettre à jour le profile")
138 end.
139
140
141 % Renvoie les messages appropriés.
142 % last_message id et cookie sont facultatifs
143 wait_event([{page, "chat"} | Data]) ->
144 % traitement des inputs
145 Cookie = case lists:keysearch(cookie, 1, Data) of {value, {_, C}} -> C; _ -> inconnu end,
146 Last_message_id = case lists:keysearch(last_message_id, 1, Data) of {value, {_, Id}} -> Id; _ -> 0 end,
147 {value, {_, Message_count}} = lists:keysearch(message_count, 1, Data),
148 Main_page = case lists:keysearch(main_page, 1, Data) of {value, {_, P}} -> P; _ -> 1 end,
149 Troll_id = case lists:keysearch(troll_id, 1, Data) of {value, {_, T}} -> T; _ -> 0 end,
150 {value, {_, {array, Conversations_json}}} = lists:keysearch(conversations, 1, Data),
151 Racines_conversations = lists:map(
152 fun({struct, [{root, Racine}, {page, Page} | Reste]}) ->
153 Last_mess_conv = case Reste of [{last_message_id, L}] -> L; _ -> 0 end,
154 {Racine, Page, Last_mess_conv}
155 end,
156 Conversations_json
157 ),
158 User = case euphorik_bd:user_by_cookie(Cookie) of
159 {ok, U} -> U;
160 _ -> inconnu
161 end,
162 case {mnesia:subscribe({table, minichat, detailed}), mnesia:subscribe({table, troll, detailed})} of
163 {{error, E}, _} -> E;
164 {_, {error, E}} -> E;
165 _ ->
166 % attente d'événements
167 R = wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id),
168 mnesia:unsubscribe({table, minichat, detailed}),
169 mnesia:unsubscribe({table, troll, detailed}),
170 R
171 end;
172 wait_event([{page, "admin"}, {last_troll, Last_troll}]) ->
173 case wait_event_page_admin(Last_troll) of
174 banned_ips_refresh ->
175 {struct,
176 [
177 {reply, "banned_ips_refresh"}
178 ]
179 };
180 {mod, Troll} ->
181 {struct,
182 [
183 {reply, "troll_modified"},
184 {troll_id, Troll#troll.id},
185 {content, Troll#troll.content}
186 ]
187 };
188 {add, Trolls} ->
189 {struct,
190 [
191 {reply, "troll_added"},
192 {trolls, {array,
193 lists:map(
194 fun(T) ->
195 {ok, User} = euphorik_bd:user_by_id(T#troll.id_user),
196 {struct,
197 [
198 {troll_id, T#troll.id},
199 {content, T#troll.content},
200 {author, User#user.pseudo},
201 {author_id, User#user.id}
202 ]
203 }
204 end,
205 Trolls
206 )
207 }}
208 ]
209 };
210 {del, Troll_id} ->
211 {struct,
212 [
213 {reply, "troll_deleted"},
214 {troll_id, Troll_id}
215 ]
216 };
217 _ ->
218 erreur("timeout")
219 end;
220 wait_event(_) ->
221 erreur("Page inconnue").
222
223
224 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id) ->
225 % est-ce que le troll est à jour ?
226 case euphorik_bd:current_troll() of
227 Current when is_record(Current, troll), Current#troll.id =/= Troll_id ->
228 {struct, [
229 {reply, "new_troll"},
230 {troll_id, Current#troll.id},
231 {message_id, euphorik_bd:message_id_associe(Current#troll.id)},
232 {content, Current#troll.content}
233 ]};
234 _ ->
235 % est-ce qu'il y a des nouveaux messages ?
236 case euphorik_minichat_conversation:conversations(Racines_conversations, Message_count, Last_message_id, Main_page) of
237 vide ->
238 wait_event_bd_page_chat(),
239 % TODO : l'appel est-il bien tail-recursive ?
240 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id);
241 Conversations ->
242 % accrochez-vous ca va siouxer ;)
243 {struct, [
244 {reply, "new_messages"},
245 {conversations, {array,
246 lists:map(
247 fun({Conv, Plus}) ->
248 {struct, [
249 {last_page, not Plus},
250 {messages, {array,
251 lists:map(
252 fun({Mess, Repond_a}) ->
253 Est_proprietaire = User =/= inconnu andalso User#user.id =:= Mess#minichat.auteur_id,
254 A_repondu_a_message = User =/= inconnu andalso euphorik_bd:a_repondu_a_message(User#user.id, Mess#minichat.id),
255 Est_une_reponse_a_user = User =/= inconnu andalso euphorik_bd:est_une_reponse_a_user(User#user.id, Mess#minichat.id),
256 {ok, User_mess } = euphorik_bd:user_by_id(Mess#minichat.auteur_id),
257 {struct, [
258 {id, Mess#minichat.id},
259 {user_id, User_mess#user.id},
260 {date, format_date(Mess#minichat.date)},
261 {system, Mess#minichat.auteur_id =:= 0},
262 {owner, Est_proprietaire},
263 {answered, A_repondu_a_message},
264 {is_a_reply, Est_une_reponse_a_user},
265 {nick, Mess#minichat.pseudo},
266 {login, User_mess#user.login},
267 {content, Mess#minichat.contenu},
268 {answer_to, {array, lists:map(
269 fun(Id_mess) ->
270 {ok, M} = euphorik_bd:message_by_id(Id_mess),
271 {ok, User_reponse} = euphorik_bd:user_by_mess(M#minichat.id),
272 {struct, [{id, M#minichat.id}, {nick, M#minichat.pseudo}, {login, User_reponse#user.login}]}
273 end,
274 Repond_a
275 )}},
276 {ek_master, User_mess#user.ek_master}
277 ]}
278 end,
279 Conv
280 )
281 }}
282 ]}
283 end,
284 Conversations
285 )
286 }}
287 ]}
288 end
289 end.
290
291
292 % Attend un événement lié à la page 'chat'.
293 wait_event_bd_page_chat() ->
294 receive % attente d'un post
295 {mnesia_table_event, {write, minichat, _Message, [], _}} ->
296 ok;
297 {mnesia_table_event, {write, troll, Troll, [Old_troll | _], _}} when Troll#troll.date_post =/= undefined, Old_troll#troll.date_post == undefined ->
298 ok;
299 {tcp_closed, _} ->
300 exit(normal);
301 _ ->
302 wait_event_bd_page_chat()
303 % 60 minutes de timeout (on ne sais jamais)
304 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
305 after 1000 * 60 * 60 ->
306 timeout
307 end.
308
309
310 % Attent un événement concernant la page admin
311 % Renvoie les trolls manquants posté après Last_id ou banned_ips_refresh.
312 % Si pas de trolls alors attend un événement tel qu'un ajout, une modification ou une suppression.
313 % renvoie :
314 % {mod, Troll}
315 % ou {add, [Trolls]}
316 % ou {del, Troll_id}
317 % ou banned_ips_refresh
318 % ou timeout
319 wait_event_page_admin(Last_id) ->
320 case {mnesia:subscribe({table, troll, detailed}), mnesia:subscribe({table, ip_table, detailed})} of
321 {{error, E}, _ } -> E;
322 {_, {error, E}} -> E;
323 _ ->
324 R = case euphorik_bd:trolls(Last_id) of
325 [] -> % pas de trolls
326 wait_event_page_admin();
327 Trolls ->
328 {add, Trolls}
329 end,
330 mnesia:unsubscribe({table, troll, detailed}),
331 mnesia:unsubscribe({table, ip_table, detailed}),
332 R
333 end.
334
335 wait_event_page_admin() ->
336 % s'il n'y a pas de trolls que l'utilisateur n'a pas connaissance alors on attend un événement
337 receive
338 % cas où un troll est choisit comme courant
339 {mnesia_table_event, {write, troll, Troll, [Old_troll | _], _}}
340 when Old_troll#troll.date_post =:= undefined, Troll#troll.date_post =/= undefined ->
341 {del, Troll#troll.id};
342 {mnesia_table_event, {write, troll, Troll, [_Old_troll | _], _}} ->
343 {mod, Troll};
344 {mnesia_table_event, {write, troll, Troll, [], _}} ->
345 {add, [Troll]};
346 {mnesia_table_event, {delete, troll, {troll, Id}, _, _}} ->
347 {del, Id};
348 {mnesia_table_event, {write, ip_table, IP, [Old_IP | _], _}}
349 when Old_IP#ip_table.ban =/= IP#ip_table.ban; Old_IP#ip_table.ban_duration =/= IP#ip_table.ban_duration ->
350 banned_ips_refresh;
351 {tcp_closed, _} ->
352 exit(normal);
353 _ ->
354 wait_event_page_admin()
355 % 60 minutes de timeout (on ne sais jamais)
356 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
357 after 1000 * 60 * 60 ->
358 timeout
359 end.
360
361
362 % Un utilisateur envoie un message
363 put_message(
364 [
365 {cookie, Cookie},
366 {nick, Nick},
367 {content, Content},
368 {answer_to, {array, Answer_to}}
369 ]
370 ) ->
371 case euphorik_bd:user_by_cookie(Cookie) of
372 {ok, User} ->
373 case euphorik_bd:est_banni(User#user.id) of
374 {true, Temps_restant} ->
375 erreur("Vous êtes banni pour encore " ++ format_minutes(Temps_restant));
376 _ ->
377 Strip_content = string:strip(Content),
378 if Strip_content =:= [] ->
379 erreur("Message vide");
380 true ->
381 % TODO : non-atomique (update_pseudo+nouveau_message)
382 euphorik_bd:update_pseudo_user(User#user.id, Nick),
383 case euphorik_bd:nouveau_message(Strip_content, User#user.id, Answer_to) of
384 {erreur, R} -> erreur("Impossible d'ajouter un nouveau message. Raison : " ++ R);
385 _ ->
386 json_reponse_ok()
387 end
388 end
389 end;
390 _ ->
391 erreur("Utilisateur inconnu")
392 end.
393
394
395 % bannissement d'un utilisateur (son ip est bannie)
396 ban(
397 [
398 {cookie, Cookie},
399 {duration, Duration},
400 {user_id, User_id},
401 {reason, Reason}
402 ]) ->
403 % controle que l'utilisateur est un admin
404 case euphorik_bd:user_by_cookie(Cookie) of
405 {ok, User1 = #user{ek_master = true}} ->
406 case euphorik_bd:user_by_id(User_id) of
407 {ok, User1} ->
408 erreur("Il n'est pas possible de s'auto bannir");
409 {ok, User2 = #user{ek_master = false}} ->
410 euphorik_bd:ban(User2#user.last_ip, Duration),
411 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("''~s~s'' est ~s pour ~s.~s",
412 [
413 User2#user.pseudo,
414 if User2#user.login =:= [] -> ""; true -> " (" ++ User2#user.login ++ ")" end,
415 if Duration =< 15 -> "kické"; true -> "banni" end,
416 format_minutes(Duration),
417 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
418 ]
419 ))),
420 json_reponse_ok();
421 {ok, _} ->
422 erreur("L'utilisateur est lui même un ekMaster");
423 _ ->
424 erreur("Utilisateur à bannir inconnu")
425 end;
426 _ ->
427 erreur("Utilisateur inconnu ou non ek master")
428 end.
429
430
431 % slapage d'un user (avertissement)
432 slap(
433 [
434 {cookie, Cookie},
435 {user_id, User_id},
436 {reason, Reason}
437 ]) ->
438 % controle que l'utilisateur est un admin
439 case euphorik_bd:user_by_cookie(Cookie) of
440 {ok, User1 = #user{ek_master = true}} ->
441 case euphorik_bd:user_by_id(User_id) of
442 {ok, User1} ->
443 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s s'auto slap~s.",
444 [
445 User1#user.pseudo,
446 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end
447 ]
448 ))),
449 json_reponse_ok();
450 {ok, User2 = #user{ek_master = false}} ->
451 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s se fait slaper par ~s.~s",
452 [
453 User2#user.pseudo,
454 User1#user.pseudo,
455 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
456 ]
457 ))),
458 json_reponse_ok();
459 {ok, _} ->
460 erreur("L'utilisateur est lui même un ekMaster");
461 _ ->
462 erreur("Utilisateur à slaper inconnu")
463 end;
464 _ ->
465 erreur("Utilisateur inconnu ou non ek master")
466 end.
467
468
469 put_troll(
470 [
471 {cookie, Cookie},
472 {content, Content}
473 ]
474 ) ->
475 % controle que l'utilisateur est un admin
476 case euphorik_bd:user_by_cookie(Cookie) of
477 {ok, User = #user{ek_master = true}} ->
478 case euphorik_bd:put_troll(User#user.id, Content) of
479 max_troll_reached_per_user ->
480 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum par utilisateur est atteint : ~w ", [?NB_MAX_TROLL_WAITING_BY_USER])));
481 max_troll_reached ->
482 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum en attente est atteint : ~w ", [?NB_MAX_TROLL_WAITING])));
483 _Id ->
484 json_reponse_ok()
485 end;
486 _ ->
487 erreur("Seul les ekMaster peuvent proposer des trolls")
488 end.
489
490
491 mod_troll(
492 [
493 {cookie, Cookie},
494 {troll_id, Troll_id},
495 {content, Content}
496 ]
497 ) ->
498 % controle que l'utilisateur est un admin
499 case euphorik_bd:user_by_cookie(Cookie) of
500 {ok, User = #user{ek_master = true}} ->
501 User_id = User#user.id,
502 case euphorik_bd:troll_by_id(Troll_id) of
503 {ok, #troll{id_user = User_id}} ->
504 euphorik_bd:mod_troll(Troll_id, Content),
505 json_reponse_ok();
506 _ ->
507 erreur("Vous ne posséder pas ce troll")
508 end;
509 _ ->
510 erreur("Seul les ekMaster peuvent proposer des trolls")
511 end.
512
513
514 del_troll(
515 [
516 {cookie, Cookie},
517 {troll_id, Troll_id}
518 ]
519 ) ->
520 % controle que l'utilisateur est un admin
521 case euphorik_bd:user_by_cookie(Cookie) of
522 {ok, User = #user{ek_master = true}} ->
523 User_id = User#user.id,
524 case euphorik_bd:troll_by_id(Troll_id) of
525 {ok, #troll{id_user = User_id}} ->
526 euphorik_bd:del_troll(Troll_id),
527 json_reponse_ok();
528 _ ->
529 erreur("Vous ne posséder pas ce troll")
530 end;
531 _ ->
532 erreur("Seul les ekMaster peuvent proposer des trolls")
533 end.
534
535
536 unban_ip(
537 [
538 {cookie, Cookie},
539 {ip, IP}
540 ]
541 ) ->
542 case euphorik_bd:user_by_cookie(Cookie) of
543 {ok, #user{ek_master = true}} ->
544 euphorik_bd:deban(unserialize_ip(IP)),
545 json_reponse_ok();
546 _ ->
547 erreur("Seul les ekMaster peuvent connaitre la liste des ips bannies")
548 end.
549
550
551 list_banned_ips(
552 [
553 {cookie, Cookie}
554 ]
555 ) ->
556 case euphorik_bd:user_by_cookie(Cookie) of
557 {ok, #user{ek_master = true}} ->
558 {
559 struct,
560 [
561 {reply, "list_banned_ips"},
562 {list, {array, lists:map(
563 fun({IP, T, Users}) ->
564 {struct,
565 [
566 {ip, serialize_ip(IP)},
567 {remaining_time, format_minutes(T)},
568 {users, {array, lists:map(
569 fun({Pseudo, Login}) ->
570 {struct,
571 [
572 {nick, Pseudo},
573 {login, Login}
574 ]
575 }
576 end,
577 Users
578 )}}
579 ]
580 }
581 end,
582 euphorik_bd:list_ban()
583 )}}
584 ]
585 };
586 _ ->
587 erreur("Seul les ekMaster peuvent connaitre la liste des ips bannies")
588 end.
589
590
591 % Construit une erreur
592 erreur(Message) ->
593 {
594 struct, [
595 {reply, "error"},
596 {error_message, Message}
597 ]
598 }.
599
600
601 serialize_ip(IP) ->
602 lists:flatten(io_lib:format("~w.~w.~w.~w", tuple_to_list(IP))).
603
604
605 unserialize_ip(IP) ->
606 case io_lib:fread("~d.~d.~d.~d", IP) of
607 {ok, [A, B, C, D], []} -> {A, B, C, D};
608 _ -> erreur
609 end.
610
611
612 % Formatage de minutes.
613 % par exemple : "1min", "45min", "1h23min", "1jour 2h34min"
614 format_minutes(Min) ->
615 Jours = Min div (60 * 24),
616 Heures = Min rem (60 * 24) div 60,
617 Minutes = Min rem (60),
618 if Jours =/= 0 -> integer_to_list(Jours) ++ " Jour" ++ if Jours > 1 -> "s"; true -> "" end ++ " "; true -> "" end ++
619 if Heures =/= 0 -> integer_to_list(Heures) ++ " heure" ++ if Heures > 1 -> "s"; true -> "" end; true -> "" end ++
620 if Minutes == 0 ->
621 "";
622 true ->
623 " " ++ integer_to_list(Minutes) ++ " minute" ++ if Minutes > 1 -> "s"; true -> "" end
624 end.
625
626
627 % Formatage d'une heure
628 % local_time() -> string
629 format_date(Date) ->
630 DateLocal = calendar:now_to_local_time(Date),
631 DateNowLocal = calendar:local_time(),
632 {{Annee, Mois, Jour}, {Heure, Minute, Seconde}} = DateLocal,
633 {{AnneeNow, _, _}, {_, _, _}} = DateNowLocal,
634 Hier = calendar:date_to_gregorian_days(element(1, DateLocal)) =:= calendar:date_to_gregorian_days(element(1, DateNowLocal)) - 1,
635 lists:flatten(
636 if element(1, DateLocal) =:= element(1, DateNowLocal) ->
637 "";
638 Hier ->
639 "Hier ";
640 Annee =:= AnneeNow ->
641 io_lib:format("~2.10.0B/~2.10.0B ", [Jour, Mois]);
642 true ->
643 io_lib:format("~2.10.0B/~2.10.0B/~B ", [Jour, Mois, Annee])
644 end ++
645 io_lib:format("~2.10.0B:~2.10.0B:~2.10.0B", [Heure, Minute, Seconde])
646 ).
647
648
649 json_reponse_ok() ->
650 {struct, [{reply, "ok"}]}.
651
652
653 json_reponse_login_ok(User) ->
654 {
655 struct, [
656 {reply, "login"},
657 {status, if (User#user.password =/= []) and (User#user.login =/= []) -> "auth_registered"; true -> "auth_not_registered" end},
658 {cookie, User#user.cookie},
659 {id, User#user.id},
660 {nick, User#user.pseudo},
661 {login, User#user.login},
662 {email, User#user.email},
663 {css, User#user.css},
664 {nick_format, atom_to_list(User#user.nick_format)},
665 {main_page, User#user.page_principale},
666 {conversations,
667 {array,
668 lists:map(
669 fun(C) ->
670 {struct,
671 [
672 {root, element(1, C)},
673 {page, element(2, C)}
674 ]
675 }
676 end,
677 User#user.conversations
678 )
679 }
680 },
681 {ek_master, User#user.ek_master}
682 ]
683 }.