08624ae45e710d97e0224ccb9d51218eb0a8d7cb
[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 -include_lib("xmerl/include/xmerl.hrl").
22 -include("../include/euphorik_bd.hrl").
23 -include("../include/euphorik_defines.hrl").
24
25
26 % Une utilisateur s'enregistre avec un tuple {Login, Password}.
27 register([{login, Login}, {password, Password}], IP) ->
28 Can_register = euphorik_bd:can_register(IP),
29 if Can_register ->
30 case euphorik_bd:user_by_login(Login) of
31 {ok, _} ->
32 erreur("Login déjà existant");
33 _ ->
34 User = euphorik_bd:nouveau_user(Login, Password, generer_cookie()),
35 euphorik_bd:update_ip(User#user.id, IP),
36 json_reponse_login_ok(User)
37 end;
38 true ->
39 erreur_register_flood()
40 end;
41 % Enregistrement sans {Login, Password}
42 register([], IP) ->
43 Can_register = euphorik_bd:can_register(IP),
44 if Can_register ->
45 User = euphorik_bd:nouveau_user("<nick>", generer_cookie()),
46 euphorik_bd:update_ip(User#user.id, IP),
47 json_reponse_login_ok(User);
48 true ->
49 erreur_register_flood()
50 end.
51
52 erreur_register_flood() ->
53 erreur("Trop de register (flood)").
54
55
56 % Un utilisateur se logge (avec un couple {login, mot de passe})
57 login([{login, Login}, {password, Password}], IP) ->
58 loginUser(euphorik_bd:user_by_login_password(Login, Password), IP);
59 % Un utilisateur se logge (avec un cookie)
60 login([{cookie, Cookie}], IP) ->
61 loginUser(euphorik_bd:user_by_cookie(Cookie), IP).
62
63 loginUser({ok, User}, IP) ->
64 euphorik_bd:update_ip(User#user.id, IP),
65 euphorik_bd:update_date_derniere_connexion(User#user.id),
66 json_reponse_login_ok(User);
67 loginUser(_, _) ->
68 % ajoute un délais d'attente
69 timer:sleep(1000),
70 erreur("Erreur login").
71
72
73 % Renvoie un string() représentant un cookie en base 36. Il y a 10^32 possibillités.
74 generer_cookie() ->
75 {A1,A2,A3} = now(),
76 random:seed(A1, A2, A3),
77 erlang:integer_to_list(random:uniform(math:pow(10, 32)), 36).
78
79
80 % Un utilisateur se délogge.
81 logout(_) ->
82 do_nothing.
83
84
85 % Modification du profile.
86 profile(
87 [
88 {cookie, Cookie},
89 {login, Login},
90 {password, Password},
91 {nick, Pseudo},
92 {email, Email},
93 {css, Css},
94 {nick_format, Nick_format_str},
95 {main_page, Main_page},
96 {conversations, {array, Conversations_json}}
97 ]
98 ) ->
99 % est-ce que les messages auquel on répond existent ?
100 Conversations = lists:foldr(
101 fun({struct, [{root, Root}, {page, Page}]}, Acc) ->
102 Message_existe = euphorik_bd:message_existe(Root),
103 if Message_existe ->
104 [{Root, Page} | Acc];
105 true ->
106 Acc
107 end
108 end,
109 [],
110 Conversations_json
111 ),
112 case euphorik_bd:set_profile(Cookie, Login, Password, Pseudo, Email, Css, list_to_atom(Nick_format_str), Main_page, Conversations) of
113 ok ->
114 json_reponse_ok();
115 login_deja_pris ->
116 erreur("Login déjà pris");
117 _ ->
118 erreur("Impossible de mettre à jour le profile")
119 end.
120
121
122 % Renvoie les messages appropriés.
123 % last_message id et cookie sont facultatifs
124 wait_event([{page, "chat"} | Data]) ->
125 % traitement des inputs
126 Cookie = case lists:keysearch(cookie, 1, Data) of {value, {_, C}} -> C; _ -> inconnu end,
127 Last_message_id = case lists:keysearch(last_message_id, 1, Data) of {value, {_, Id}} -> Id; _ -> 0 end,
128 {value, {_, Message_count}} = lists:keysearch(message_count, 1, Data),
129 Main_page = case lists:keysearch(main_page, 1, Data) of {value, {_, P}} -> P; _ -> 1 end,
130 Troll_id = case lists:keysearch(troll_id, 1, Data) of {value, {_, T}} -> T; _ -> 0 end,
131 {value, {_, {array, Conversations_json}}} = lists:keysearch(conversations, 1, Data),
132 Racines_conversations = lists:map(
133 fun({struct, [{root, Racine}, {page, Page} | Reste]}) ->
134 Last_mess_conv = case Reste of [{last_message_id, L}] -> L; _ -> 0 end,
135 {Racine, Page, Last_mess_conv}
136 end,
137 Conversations_json
138 ),
139 User = case euphorik_bd:user_by_cookie(Cookie) of
140 {ok, U} -> U;
141 _ -> inconnu
142 end,
143 case {mnesia:subscribe({table, minichat, detailed}), mnesia:subscribe({table, troll, detailed})} of
144 {{error, E}, _} -> E;
145 {_, {error, E}} -> E;
146 _ ->
147 % attente d'événements
148 R = wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id),
149 mnesia:unsubscribe({table, minichat, detailed}),
150 mnesia:unsubscribe({table, troll, detailed}),
151 R
152 end;
153 wait_event([{page, "admin"}, {last_troll, Last_troll}]) ->
154 case euphorik_bd:trolls_attente(Last_troll) of
155 {mod, Troll} ->
156 {struct,
157 [
158 {reply, "troll_modified"},
159 {troll_id, Troll#troll.id},
160 {content, Troll#troll.content}
161 ]
162 };
163 {add, Trolls} ->
164 {struct,
165 [
166 {reply, "troll_added"},
167 {trolls, {array,
168 lists:map(
169 fun(T) ->
170 {ok, User} = euphorik_bd:user_by_id(T#troll.id_user),
171 {struct,
172 [
173 {troll_id, T#troll.id},
174 {content, T#troll.content},
175 {author, User#user.pseudo},
176 {author_id, User#user.id}
177 ]
178 }
179 end,
180 Trolls
181 )
182 }}
183 ]
184 };
185 {del, Troll_id} ->
186 {struct,
187 [
188 {reply, "troll_deleted"},
189 {troll_id, Troll_id}
190 ]
191 };
192 _ ->
193 erreur("timeout")
194 end;
195 wait_event(_) ->
196 erreur("Page inconnue").
197
198
199 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id) ->
200 % est-ce que le troll est à jour ?
201 case euphorik_bd:current_troll() of
202 Current when is_record(Current, troll), Current#troll.id =/= Troll_id ->
203 {struct, [
204 {reply, "new_troll"},
205 {troll_id, Current#troll.id},
206 {content, Current#troll.content}
207 ]};
208 _ ->
209 % est-ce qu'il y a des nouveaux messages ?
210 case euphorik_minichat_conversation:conversations(Racines_conversations, Message_count, Last_message_id, Main_page) of
211 vide ->
212 wait_event_bd_page_chat(),
213 % TODO : l'appel est-il bien tail-recursive ?
214 wait_event_page_chat(User, Racines_conversations, Message_count, Last_message_id, Main_page, Troll_id);
215 Conversations ->
216 % accrochez-vous ca va siouxer ;)
217 {struct, [
218 {reply, "new_messages"},
219 {conversations, {array,
220 lists:map(
221 fun({Conv, Plus}) ->
222 {struct, [
223 {last_page, not Plus},
224 {messages, {array,
225 lists:map(
226 fun({Mess, Repond_a}) ->
227 Est_proprietaire = User =/= inconnu andalso User#user.id =:= Mess#minichat.auteur_id,
228 A_repondu_a_message = User =/= inconnu andalso euphorik_bd:a_repondu_a_message(User#user.id, Mess#minichat.id),
229 Est_une_reponse_a_user = User =/= inconnu andalso euphorik_bd:est_une_reponse_a_user(User#user.id, Mess#minichat.id),
230 {ok, User_mess } = euphorik_bd:user_by_id(Mess#minichat.auteur_id),
231 {struct, [
232 {id, Mess#minichat.id},
233 {user_id, User_mess#user.id},
234 {date, format_date(Mess#minichat.date)},
235 {system, Mess#minichat.auteur_id =:= 0},
236 {owner, Est_proprietaire},
237 {answered, A_repondu_a_message},
238 {is_a_reply, Est_une_reponse_a_user},
239 {nick, Mess#minichat.pseudo},
240 {login, User_mess#user.login},
241 {content, Mess#minichat.contenu},
242 {answer_to, {array, lists:map(
243 fun(Id_mess) ->
244 {ok, M} = euphorik_bd:message_by_id(Id_mess),
245 {ok, User_reponse} = euphorik_bd:user_by_mess(M#minichat.id),
246 {struct, [{id, M#minichat.id}, {nick, M#minichat.pseudo}, {login, User_reponse#user.login}]}
247 end,
248 Repond_a
249 )}},
250 {ek_master, User_mess#user.ek_master}
251 ]}
252 end,
253 Conv
254 )
255 }}
256 ]}
257 end,
258 Conversations
259 )
260 }}
261 ]}
262 end
263 end.
264
265
266 % Attend un événement lié à la page 'chat'.
267 wait_event_bd_page_chat() ->
268 receive % attente d'un post
269 {mnesia_table_event, {write, minichat, _Message, [], _}} ->
270 ok;
271 {mnesia_table_event, {write, troll, Troll, [Old_troll | _], _}} when Troll#troll.date_post =/= undefined, Old_troll#troll.date_post == undefined ->
272 ok;
273 {tcp_closed, _} ->
274 exit(normal);
275 _ ->
276 wait_event_bd_page_chat()
277 % 60 minutes de timeout (on ne sais jamais)
278 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
279 after 1000 * 60 * 60 ->
280 timeout
281 end.
282
283
284 % Un utilisateur envoie un message
285 put_message(
286 [
287 {cookie, Cookie},
288 {nick, Nick},
289 {content, Content},
290 {answer_to, {array, Answer_to}}
291 ]
292 ) ->
293 case euphorik_bd:user_by_cookie(Cookie) of
294 {ok, User} ->
295 case euphorik_bd:est_banni(User#user.id) of
296 {true, Temps_restant} ->
297 erreur("Vous êtes banni pour encore " ++ format_minutes(Temps_restant));
298 _ ->
299 Strip_content = string:strip(Content),
300 if Strip_content =:= [] ->
301 erreur("Message vide");
302 true ->
303 % TODO : non-atomique (update_pseudo+nouveau_message)
304 euphorik_bd:update_pseudo_user(User#user.id, Nick),
305 case euphorik_bd:nouveau_message(Strip_content, User#user.id, Answer_to) of
306 {erreur, R} -> erreur("Impossible d'ajouter un nouveau message. Raison : " ++ R);
307 _ ->
308 json_reponse_ok()
309 end
310 end
311 end;
312 _ ->
313 erreur("Utilisateur inconnu")
314 end.
315
316
317 % Formatage de minutes.
318 % par exemple : "1min", "45min", "1h23min", "1jour 2h34min"
319 format_minutes(Min) ->
320 Jours = Min div (60 * 24),
321 Heures = Min rem (60 * 24) div 60,
322 Minutes = Min rem (60),
323 if Jours =/= 0 -> integer_to_list(Jours) ++ "Jour" ++ if Jours > 1 -> "s"; true -> "" end ++ " "; true -> "" end ++
324 if Heures =/= 0 -> integer_to_list(Heures) ++ "h"; true -> "" end ++
325 if Minutes == 0 ->
326 "";
327 true ->
328 lists:flatten(io_lib:format(if Jours =:= 0, Heures =:= 0 -> "~w"; true -> "~2.2.0w" end, [Minutes])) ++ "min"
329 end.
330
331
332 % bannissement d'un utilisateur (son ip est bannie)
333 ban(
334 [
335 {cookie, Cookie},
336 {duration, Duration},
337 {user_id, User_id},
338 {reason, Reason}
339 ]) ->
340 % controle que l'utilisateur est un admin
341 case euphorik_bd:user_by_cookie(Cookie) of
342 {ok, User1 = #user{ek_master = true}} ->
343 case euphorik_bd:user_by_id(User_id) of
344 {ok, User1} ->
345 erreur("Il n'est pas possible de s'auto bannir");
346 {ok, User2 = #user{ek_master = false}} ->
347 euphorik_bd:ban(User2#user.last_ip, Duration),
348 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s ~s est ~s pour ~s.~s",
349 [
350 User2#user.pseudo,
351 if User2#user.login =:= [] -> ""; true -> "(" ++ User2#user.login ++ ")" end,
352 if Duration =< 15 -> "kické"; true -> "banni" end,
353 format_minutes(Duration),
354 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
355 ]
356 ))),
357 json_reponse_ok();
358 {ok, _} ->
359 erreur("L'utilisateur est lui même un ekMaster");
360 _ ->
361 erreur("Utilisateur à bannir inconnu")
362 end;
363 _ ->
364 erreur("Utilisateur inconnu ou non ek master")
365 end.
366
367
368 % slapage d'un user (avertissement)
369 slap(
370 [
371 {cookie, Cookie},
372 {user_id, User_id},
373 {reason, Reason}
374 ]) ->
375 % controle que l'utilisateur est un admin
376 case euphorik_bd:user_by_cookie(Cookie) of
377 {ok, User1 = #user{ek_master = true}} ->
378 case euphorik_bd:user_by_id(User_id) of
379 {ok, User1} ->
380 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s s'auto slap~s.",
381 [
382 User1#user.pseudo,
383 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end
384 ]
385 ))),
386 json_reponse_ok();
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 }.