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