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