ADD troll sur la page principale
[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 _ ->
275 wait_event_bd_page_chat()
276 % 60 minutes de timeout (le cas ou il n'y a que des consultations et jamais de post)
277 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
278 % TODO : pour être mieux : quand le socket se ferme alors un message devrait être envoyé et débloquer ce receive (demande en cours sur la mailing list de yaws)
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 -> erreur("Impossible d'ajouter un nouveau message");
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 {ok, User2 = #user{ek_master = false}} ->
387 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s se fait slaper par ~s.~s",
388 [
389 User2#user.pseudo,
390 User1#user.pseudo,
391 if Reason =/= [] -> " - Raison: " ++ Reason; true -> "" end ++ "."
392 ]
393 ))),
394 json_reponse_ok();
395 {ok, _} ->
396 erreur("L'utilisateur est lui même un ekMaster");
397 _ ->
398 erreur("Utilisateur à slaper inconnu")
399 end;
400 _ ->
401 erreur("Utilisateur inconnu ou non ek master")
402 end.
403
404
405 put_troll(
406 [
407 {cookie, Cookie},
408 {content, Content}
409 ]
410 ) ->
411 % controle que l'utilisateur est un admin
412 case euphorik_bd:user_by_cookie(Cookie) of
413 {ok, User = #user{ek_master = true}} ->
414 case euphorik_bd:put_troll(User#user.id, Content) of
415 max_troll_reached_per_user ->
416 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum par utilisateur est atteint : ~w ", [?NB_MAX_TROLL_WAITING_BY_USER])));
417 max_troll_reached ->
418 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum en attente est atteint : ~w ", [?NB_MAX_TROLL_WAITING])));
419 _Id ->
420 json_reponse_ok()
421 end;
422 _ ->
423 erreur("Seul les ekMaster peuvent proposer des trolls")
424 end.
425
426
427 mod_troll(
428 [
429 {cookie, Cookie},
430 {troll_id, Troll_id},
431 {content, Content}
432 ]
433 ) ->
434 % controle que l'utilisateur est un admin
435 case euphorik_bd:user_by_cookie(Cookie) of
436 {ok, User = #user{ek_master = true}} ->
437 User_id = User#user.id,
438 case euphorik_bd:troll_by_id(Troll_id) of
439 {ok, #troll{id_user = User_id}} ->
440 euphorik_bd:mod_troll(Troll_id, Content),
441 json_reponse_ok();
442 _ ->
443 erreur("Vous ne posséder pas ce troll")
444 end;
445 _ ->
446 erreur("Seul les ekMaster peuvent proposer des trolls")
447 end.
448
449
450 del_troll(
451 [
452 {cookie, Cookie},
453 {troll_id, Troll_id}
454 ]
455 ) ->
456 % controle que l'utilisateur est un admin
457 case euphorik_bd:user_by_cookie(Cookie) of
458 {ok, User = #user{ek_master = true}} ->
459 User_id = User#user.id,
460 case euphorik_bd:troll_by_id(Troll_id) of
461 {ok, #troll{id_user = User_id}} ->
462 euphorik_bd:del_troll(Troll_id),
463 json_reponse_ok();
464 _ ->
465 erreur("Vous ne posséder pas ce troll")
466 end;
467 _ ->
468 erreur("Seul les ekMaster peuvent proposer des trolls")
469 end.
470
471
472 % Construit une erreur
473 erreur(Message) ->
474 {
475 struct, [
476 {reply, "error"},
477 {error_message, Message}
478 ]
479 }.
480
481
482 % Formatage d'une heure
483 % local_time() -> string
484 format_date(Date) ->
485 DateLocal = calendar:now_to_local_time(Date),
486 DateNowLocal = calendar:local_time(),
487 {{Annee, Mois, Jour}, {Heure, Minute, Seconde}} = DateLocal,
488 {{AnneeNow, _, _}, {_, _, _}} = DateNowLocal,
489 Hier = calendar:date_to_gregorian_days(element(1, DateLocal)) =:= calendar:date_to_gregorian_days(element(1, DateNowLocal)) - 1,
490 lists:flatten(
491 if element(1, DateLocal) =:= element(1, DateNowLocal) ->
492 "";
493 Hier ->
494 "Hier ";
495 Annee =:= AnneeNow ->
496 io_lib:format("~2.10.0B/~2.10.0B ", [Jour, Mois]);
497 true ->
498 io_lib:format("~2.10.0B/~2.10.0B/~B ", [Jour, Mois, Annee])
499 end ++
500 io_lib:format("~2.10.0B:~2.10.0B:~2.10.0B", [Heure, Minute, Seconde])
501 ).
502
503
504 json_reponse_ok() ->
505 {struct, [{reply, "ok"}]}.
506
507
508 json_reponse_login_ok(User) ->
509 {
510 struct, [
511 {reply, "login"},
512 {status, if (User#user.password =/= []) and (User#user.login =/= []) -> "auth_registered"; true -> "auth_not_registered" end},
513 {cookie, User#user.cookie},
514 {id, User#user.id},
515 {nick, User#user.pseudo},
516 {login, User#user.login},
517 {email, User#user.email},
518 {css, User#user.css},
519 {nick_format, atom_to_list(User#user.nick_format)},
520 {main_page, User#user.page_principale},
521 {conversations,
522 {array,
523 lists:map(
524 fun(C) ->
525 {struct,
526 [
527 {root, element(1, C)},
528 {page, element(2, C)}
529 ]
530 }
531 end,
532 User#user.conversations
533 )
534 }
535 },
536 {ek_master, User#user.ek_master}
537 ]
538 }.