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