Update to the new library 'json2'
[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
22
23 -module(euphorik_protocole).
24 -author("Greg Burri <greg.burri@gmail.com>").
25 -export([
26 register/2,
27 login/2,
28 profile/1,
29 wait_event/1,
30 put_message/1,
31 ban/1,
32 slap/1,
33 unban_ip/1,
34 list_banned_ips/1,
35 erreur/1
36 ]).
37 -include("../include/euphorik_bd.hrl").
38 -include("../include/euphorik_defines.hrl").
39
40
41 % Une utilisateur s'enregistre avec un tuple {Login, Password}.
42 register([{"login", Login}, {"password", Password}, {"profile", Profile_json}], IP) ->
43 Can_register = euphorik_bd:can_register(IP),
44 if Can_register ->
45 case euphorik_bd:user_by_login(Login) of
46 {ok, _} ->
47 erreur(10);
48 _ ->
49 User = euphorik_bd:nouveau_user(Login, Password, generer_cookie(), profile_from_json(Profile_json)),
50 euphorik_bd:update_ip(User#user.id, IP),
51 json_reponse_login_ok(User)
52 end;
53 true ->
54 erreur_register_flood()
55 end;
56 % Enregistrement sans {Login, Password}
57 register([{"profile", Profile_json}], IP) ->
58 Can_register = euphorik_bd:can_register(IP),
59 if Can_register ->
60 Profile = profile_from_json(Profile_json),
61 User = euphorik_bd:nouveau_user(generer_cookie(), Profile#profile{pseudo = "<nick>"}),
62 euphorik_bd:update_ip(User#user.id, IP),
63 json_reponse_login_ok(User);
64 true ->
65 erreur_register_flood()
66 end.
67
68 erreur_register_flood() ->
69 erreur(20).
70
71
72 % Un utilisateur se logge (avec un couple {login, mot de passe})
73 login([{"login", Login}, {"password", Password}], IP) ->
74 case euphorik_bd:user_by_login_password(Login, Password) of
75 {ok, User} ->
76 loginUser(User, IP);
77 _ ->
78 timer:sleep(?TEMPS_ATTENTE_ERREUR_LOGIN),
79 erreur(30)
80 end;
81 % Un utilisateur se logge (avec un cookie)
82 login([{"cookie", Cookie}], IP) ->
83 case euphorik_bd:user_by_cookie(Cookie) of
84 {ok, User} ->
85 loginUser(User, IP);
86 _ ->
87 timer:sleep(?TEMPS_ATTENTE_ERREUR_LOGIN),
88 erreur(40)
89 end.
90
91
92 % L'utilisateur donné se logge avec l'ip donnée.
93 loginUser(User, IP) ->
94 euphorik_bd:update_ip(User#user.id, IP),
95 euphorik_bd:update_date_derniere_connexion(User#user.id),
96 json_reponse_login_ok(User).
97
98
99 % Renvoie un string() représentant un cookie en base 36. Il y a 10^32 possibillités.
100 generer_cookie() ->
101 {A1, A2, A3} = erlang:timestamp(),
102 random:seed(A1, A2, A3),
103 erlang:integer_to_list(random:uniform(trunc(math:pow(10, 32))), 36).
104
105
106 % Modification du profile.
107 profile(
108 [
109 {"cookie", Cookie},
110 {"login", Login},
111 {"password", Password},
112 {"profile", Profile_json}
113 ]
114 ) ->
115 case profile_from_json(Profile_json) of
116 {erreur, E} -> E;
117 Profile ->
118 case euphorik_bd:set_profile(Cookie, Login, Password, Profile) of
119 ok ->
120 json_reponse_ok();
121 login_deja_pris ->
122 erreur(10);
123 _ ->
124 erreur(50)
125 end
126 end.
127
128
129 % Construit un #user à partir des données JSON
130 profile_from_json(
131 {struct,
132 [
133 {"nick", Pseudo},
134 {"email", Email},
135 {"css", Css},
136 {"chat_order", Chat_order_str},
137 {"nick_format", Nick_format_str},
138 {"view_times", View_times},
139 {"view_tooltips", View_tooltips},
140 {"conversations", {array, Conversations_json}},
141 {"ostentatious_master", Ostentatious_master_str}
142 ]
143 }
144 ) ->
145 % décomposition de la strucure JSON
146 Conversations = lists:foldr(
147 fun({struct, [{"root", Racine}, {"minimized", Reduit}]}, A) ->
148 % virage des messages qui n'existent pas
149 Message_exite = euphorik_bd:message_existe(Racine),
150 if Message_exite ->
151 [ {Racine, Reduit} | A];
152 true ->
153 A
154 end
155 end,
156 [],
157 Conversations_json
158 ),
159 % vérification des données JSON
160 Chat_order = list_to_atom(Chat_order_str),
161 Chat_order_valide = lists:any(fun(E) -> E =:= Chat_order end, [reverse, chrono]),
162 if not Chat_order_valide ->
163 {erreur, Chat_order_str ++ " n'est pas une valeur acceptée pour 'chat_order'"};
164 true ->
165 Nick_format = list_to_atom(Nick_format_str),
166 Nick_format_valide = lists:any(fun(E) -> E =:= Nick_format end, [nick, login, nick_login]),
167 if not Nick_format_valide ->
168 {erreur, Nick_format_str ++ " n'est pas une valeur acceptée pour 'nick_format'"};
169 true ->
170 Ostentatious_master = list_to_atom(Ostentatious_master_str),
171 Ostentatious_master_valide = lists:any(fun(E) -> E =:= Ostentatious_master end, [invisible, light, heavy]),
172 if not Ostentatious_master_valide ->
173 {erreur, Ostentatious_master_str ++ " n'est pas une valeur acceptée pour 'ostentatious_master'"};
174 true ->
175 #profile{
176 pseudo = Pseudo,
177 email = Email,
178 css = Css,
179 chat_order = Chat_order,
180 nick_format = Nick_format,
181 view_times = View_times,
182 view_tooltips = View_tooltips,
183 conversations = Conversations,
184 ostentatious_master = Ostentatious_master
185 }
186 end
187 end
188 end.
189
190
191 % Attend un événement pour la page "Chat"
192 % last_message id et cookie sont facultatifs
193 wait_event([{"page", "chat"} | Data]) ->
194 % traitement des inputs
195 Cookie = case lists:keysearch("cookie", 1, Data) of {value, {_, C}} -> C; _ -> inconnu end,
196 Last_message_id = case lists:keysearch("last_message_id", 1, Data) of {value, {_, Id}} -> Id; _ -> 0 end,
197 {value, {_, Message_count}} = lists:keysearch("message_count", 1, Data),
198 Main_page = case lists:keysearch("main_page", 1, Data) of {value, {_, P}} -> P; _ -> 1 end,
199 {value, {_, {array, Conversations_json}}} = lists:keysearch("conversations", 1, Data),
200 Racines_conversations = lists:map(
201 fun({struct, [{"root", Racine}, {"page", Page} | Reste]}) ->
202 Last_mess_conv = case Reste of [{"last_message_id", L}] -> L; _ -> 0 end,
203 {Racine, Page, Last_mess_conv}
204 end,
205 Conversations_json
206 ),
207 User = case euphorik_bd:user_by_cookie(Cookie) of
208 {ok, U} -> U;
209 _ -> inconnu
210 end,
211 case mnesia:subscribe({table, minichat, detailed}) of
212 {error, E} -> E;
213 _ ->
214 % attente d'événements
215 R = wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page),
216 mnesia:unsubscribe({table, minichat, detailed}),
217 R
218 end;
219 % Attend un événement pour la page "Admin"
220 wait_event([{"page", "admin"}]) ->
221 case wait_event_page_admin() of
222 banned_ips_refresh ->
223 {struct,
224 [
225 {"reply", "banned_ips_refresh"}
226 ]
227 };
228 _ ->
229 erreur(60)
230 end;
231 wait_event(_) ->
232 erreur(70).
233
234
235 % Attend un événement pour la page "Chat" et renvoie les messages manquants au client.
236 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page) ->
237 % est-ce qu'il y a des nouveaux messages ?
238 case euphorik_minichat_conversation:conversations(Racines_conversations, Message_count, Last_message_id, Main_page) of
239 vide ->
240 wait_event_bd_page_chat(),
241 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page);
242 Conversations ->
243 % Accrochez-vous ca va siouxer ;).
244 {struct, [
245 {"reply", "new_messages"},
246 {"conversations", {array,
247 lists:map(
248 fun({Racine, {Conv, Plus}}) ->
249 {struct, [
250 {"last_page", not Plus},
251 {"first", % le premier message de la conversation
252 if Racine =:= undefined orelse Conv =:= [] ->
253 null;
254 true ->
255 {Racine_id, _, _} = Racine,
256 case euphorik_bd:message_by_id(Racine_id) of
257 {ok, Mess} ->
258 json_message(Mess, euphorik_bd:parents_id(Racine_id), User);
259 _ ->
260 null
261 end
262 end
263 },
264 {messages, {array,
265 lists:map(
266 fun({Mess, Repond_a}) ->
267 json_message(Mess, Repond_a, User)
268 end,
269 Conv
270 )
271 }}
272 ]}
273 end,
274 % on ajoute un 'undefined' correspondant à la premier conversation qui ne possède pas de racine
275 % TODO : peut être à revoir car un peu lourd est compliqué
276 aggregation_racines_conversations([undefined | Racines_conversations], Conversations)
277 )
278 }}
279 ]}
280 end.
281
282
283 aggregation_racines_conversations(L1, L2) ->
284 aggregation_racines_conversations(L1, L2, []).
285 aggregation_racines_conversations([], [], L) -> lists:reverse(L);
286 aggregation_racines_conversations([E1|R1], [E2|R2], L) ->
287 aggregation_racines_conversations(R1, R2, [{E1, E2} | L]).
288
289
290
291 % Attend un événement lié à la page 'chat'.
292 wait_event_bd_page_chat() ->
293 receive % attente d'un post
294 {mnesia_table_event, {write, minichat, _Message, [], _}} ->
295 ok;
296 {tcp_closed, _} ->
297 exit(normal);
298 _ ->
299 wait_event_bd_page_chat()
300 % 60 minutes de timeout (on ne sais jamais)
301 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
302 after 1000 * 60 * 60 ->
303 timeout
304 end.
305
306
307 % Attent un événement concernant la page admin
308 % banned_ips_refresh
309 % ou timeout
310 wait_event_page_admin() ->
311 case mnesia:subscribe({table, ip_table, detailed}) of
312 {error, E} -> E;
313 _ ->
314 R = receive
315 {mnesia_table_event, {write, ip_table, IP, [Old_IP | _], _}}
316 when Old_IP#ip_table.ban =/= IP#ip_table.ban; Old_IP#ip_table.ban_duration =/= IP#ip_table.ban_duration ->
317 banned_ips_refresh;
318 {tcp_closed, _} ->
319 exit(normal);
320 _ ->
321 wait_event_page_admin()
322 % 60 minutes de timeout (on ne sais jamais)
323 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
324 after 1000 * 60 * 60 ->
325 timeout
326 end,
327 mnesia:unsubscribe({table, ip_table, detailed}),
328 R
329 end.
330
331
332 % Un utilisateur envoie un message
333 % Answer_to est une liste d'id (int)
334 put_message(
335 [
336 {"cookie", Cookie},
337 {"nick", Nick},
338 {"content", Content},
339 {"answer_to", {array, Answer_to}}
340 ]
341 ) ->
342 case euphorik_bd:user_by_cookie(Cookie) of
343 {ok, User} ->
344 case euphorik_bd:est_banni(User#user.id) of
345 {true, Temps_restant} ->
346 erreur(80, [format_minutes(Temps_restant)]);
347 _ ->
348 Strip_content = string:strip(Content),
349 if Strip_content =:= [] ->
350 erreur(90);
351 true ->
352 % attention : non-atomique (update_pseudo+nouveau_message)
353 euphorik_bd:update_pseudo_user(User#user.id, Nick),
354 case euphorik_bd:nouveau_message(Strip_content, User#user.id, Answer_to) of
355 {erreur, R} -> erreur(100, [R]);
356 _ ->
357 json_reponse_ok()
358 end
359 end
360 end;
361 _ ->
362 erreur(110)
363 end.
364
365
366 % bannissement d'un utilisateur (son ip est bannie)
367 ban(
368 [
369 {"cookie", Cookie},
370 {"duration", Duration},
371 {"user_id", User_id},
372 {"reason", Reason}
373 ]) ->
374 % controle que l'utilisateur est un admin
375 case euphorik_bd:user_by_cookie(Cookie) of
376 {ok, User1 = #user{ek_master = true}} ->
377 case euphorik_bd:user_by_id(User_id) of
378 {ok, User1} ->
379 erreur(120);
380 {ok, #user{ek_master = false, profile = Profile2} = User2} ->
381 euphorik_bd:ban(User2#user.last_ip, Duration),
382 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("\"~s~s\" est ~s pour ~s.~s",
383 [
384 Profile2#profile.pseudo,
385 if User2#user.login =:= [] -> ""; true -> " (" ++ User2#user.login ++ ")" end,
386 if Duration =< 15 -> "kické"; true -> "banni" end,
387 format_minutes(Duration),
388 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
389 ]
390 ))),
391 json_reponse_ok();
392 {ok, _} ->
393 erreur(130);
394 _ ->
395 erreur(140)
396 end;
397 _ ->
398 erreur(150)
399 end.
400
401
402 % slapage d'un user (avertissement)
403 slap(
404 [
405 {"cookie", Cookie},
406 {"user_id", User_id},
407 {"reason", Reason}
408 ]) ->
409 % controle que l'utilisateur est un admin
410 case euphorik_bd:user_by_cookie(Cookie) of
411 {ok, User1 = #user{ek_master = true, profile = Profile1}} ->
412 case euphorik_bd:user_by_id(User_id) of
413 {ok, User1} ->
414 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s s'auto slap~s.",
415 [
416 Profile1#profile.pseudo,
417 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end
418 ]
419 ))),
420 json_reponse_ok();
421 {ok, #user{ek_master = false, profile = Profile2}} ->
422 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s se fait slaper par ~s.~s",
423 [
424 Profile2#profile.pseudo,
425 Profile1#profile.pseudo,
426 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
427 ]
428 ))),
429 json_reponse_ok();
430 {ok, _} ->
431 erreur(130);
432 _ ->
433 erreur(160)
434 end;
435 _ ->
436 erreur(170)
437 end.
438
439
440 unban_ip(
441 [
442 {"cookie", Cookie},
443 {"ip", IP}
444 ]
445 ) ->
446 case euphorik_bd:user_by_cookie(Cookie) of
447 {ok, #user{ek_master = true}} ->
448 euphorik_bd:deban(euphorik_common:unserialize_ip(IP)),
449 json_reponse_ok();
450 _ ->
451 erreur(230)
452 end.
453
454
455 list_banned_ips(
456 [
457 {"cookie", Cookie}
458 ]
459 ) ->
460 case euphorik_bd:user_by_cookie(Cookie) of
461 {ok, #user{ek_master = true}} ->
462 {
463 struct,
464 [
465 {"reply", "list_banned_ips"},
466 {"list", {array, lists:map(
467 fun({IP, T, Users}) ->
468 {struct,
469 [
470 {"ip", euphorik_common:serialize_ip(IP)},
471 {"remaining_time", format_minutes(T)},
472 {"users", {array, lists:map(
473 fun({Pseudo, Login}) ->
474 {struct,
475 [
476 {"nick", Pseudo},
477 {"login", Login}
478 ]
479 }
480 end,
481 Users
482 )}}
483 ]
484 }
485 end,
486 euphorik_bd:list_ban()
487 )}}
488 ]
489 };
490 _ ->
491 erreur(230)
492 end.
493
494
495 % Construit une erreur
496 erreur(Num, Args) ->
497 erreur_json(Num, lists:flatten(io_lib:format(euphorik_bd:get_texte(Num), Args))).
498
499
500 erreur(Num) ->
501 erreur_json(Num, euphorik_bd:get_texte(Num)).
502
503
504 erreur_json(Num, Mess) ->
505 {
506 struct, [
507 {"reply", "error"},
508 {"no", Num},
509 {"error_message", Mess}
510 ]
511 }.
512
513
514 % Formatage de minutes.
515 % par exemple : "1min", "45min", "1h23min", "1jour 2h34min"
516 format_minutes(Min) ->
517 Jours = Min div (60 * 24),
518 Heures = Min rem (60 * 24) div 60,
519 Minutes = Min rem (60),
520 if Jours =/= 0 -> integer_to_list(Jours) ++ " Jour" ++ if Jours > 1 -> "s"; true -> "" end ++ " "; true -> "" end ++
521 if Heures =/= 0 -> integer_to_list(Heures) ++ " heure" ++ if Heures > 1 -> "s"; true -> "" end; true -> "" end ++
522 if Minutes == 0 ->
523 "";
524 true ->
525 " " ++ integer_to_list(Minutes) ++ " minute" ++ if Minutes > 1 -> "s"; true -> "" end
526 end.
527
528
529 % Formatage d'une heure
530 % local_time() -> string
531 format_date(Date) ->
532 DateLocal = calendar:now_to_local_time(Date),
533 DateNowLocal = calendar:local_time(),
534 {{Annee, Mois, Jour}, {Heure, Minute, Seconde}} = DateLocal,
535 {{AnneeNow, _, _}, {_, _, _}} = DateNowLocal,
536 Hier = calendar:date_to_gregorian_days(element(1, DateLocal)) =:= calendar:date_to_gregorian_days(element(1, DateNowLocal)) - 1,
537 lists:flatten(
538 if element(1, DateLocal) =:= element(1, DateNowLocal) ->
539 "";
540 Hier ->
541 "Hier ";
542 Annee =:= AnneeNow ->
543 io_lib:format("~2.10.0B/~2.10.0B ", [Jour, Mois]);
544 true ->
545 io_lib:format("~2.10.0B/~2.10.0B/~B ", [Jour, Mois, Annee])
546 end ++
547 io_lib:format("~2.10.0B:~2.10.0B:~2.10.0B", [Heure, Minute, Seconde])
548 ).
549
550
551 json_reponse_ok() ->
552 {struct, [{"reply", "ok"}]}.
553
554
555 json_reponse_login_ok(#user{profile = Profile} = User) ->
556 {
557 struct, [
558 {"reply", "login"},
559 {"status", if (User#user.password =/= []) and (User#user.login =/= []) -> "auth_registered"; true -> "auth_not_registered" end},
560 {"cookie", User#user.cookie},
561 {"id", User#user.id},
562 {"login", User#user.login},
563 {"ek_master", User#user.ek_master},
564 {"profile", {struct,
565 [
566 {"nick", Profile#profile.pseudo},
567 {"email", Profile#profile.email},
568 {"css", Profile#profile.css},
569 {"chat_order", atom_to_list(Profile#profile.chat_order)},
570 {"nick_format", atom_to_list(Profile#profile.nick_format)},
571 {"view_times", Profile#profile.view_times},
572 {"view_tooltips", Profile#profile.view_tooltips},
573 {"conversations", {array, lists:map(
574 fun({Racine, Reduit}) ->
575 {struct, [{"root", Racine}, {"minimized", Reduit}]}
576 end,
577 Profile#profile.conversations
578 )}},
579 {"ostentatious_master", atom_to_list(Profile#profile.ostentatious_master)}
580 ]
581 }}
582 ]
583 }.
584
585 % Renvoie le message formaté en JSON.
586 % Mess est de type #minichat
587 % Repond_a est une liste d'id des messages auquel répond Mess
588 % User est l'utilisateur courant de type #user
589 json_message(Mess, Repond_a, User) ->
590 Est_proprietaire = User =/= inconnu andalso User#user.id =:= Mess#minichat.auteur_id,
591 A_repondu_a_message = User =/= inconnu andalso euphorik_bd:a_repondu_a_message(User#user.id, Mess#minichat.id),
592 Est_une_reponse_a_user = User =/= inconnu andalso euphorik_bd:est_une_reponse_a_user(User#user.id, Mess#minichat.id),
593 {ok, #user{profile = Profile_mess} = User_mess } = euphorik_bd:user_by_id(Mess#minichat.auteur_id),
594 {struct, [
595 {"id", Mess#minichat.id},
596 {"user_id", User_mess#user.id},
597 {"date", case Mess#minichat.date of undefined -> "?"; _ -> format_date(Mess#minichat.date) end},
598 {"system", Mess#minichat.auteur_id =:= 0},
599 {"owner", Est_proprietaire},
600 {"answered", A_repondu_a_message},
601 {"is_a_reply", Est_une_reponse_a_user},
602 {"nick", Mess#minichat.pseudo},
603 {"login", User_mess#user.login},
604 {"content", Mess#minichat.contenu},
605 {"root", Mess#minichat.racine_id},
606 {"answer_to", {array, lists:map(
607 fun(Id_mess) ->
608 {ok, M} = euphorik_bd:message_by_id(Id_mess),
609 {ok, User_reponse} = euphorik_bd:user_by_mess(M#minichat.id),
610 {struct, [{"id", M#minichat.id}, {"nick", M#minichat.pseudo}, {"login", User_reponse#user.login}]}
611 end,
612 Repond_a
613 )}},
614 {"ek_master", User_mess#user.ek_master},
615 {"ostentatious_master", atom_to_list(Profile_mess#profile.ostentatious_master)}
616 ]}.