ADD Bannissement (sans le slap)
[euphorik.git] / modules / erl / euphorik_bd.erl
1 % coding: utf-8
2 % Ce module permet de gérer les données persistantes lié au site d'euphorik.ch.
3 % Il permet d'ajouter des message, de demande les messages sur une page donnée, etc..
4 % Ce module utilise une base mnesia.
5 % @author G.Burri
6
7 -module(euphorik_bd).
8 -export([
9 % gestion :
10 create/0,
11 connect/0,
12 connect/1,
13 reset/0,
14
15 % users :
16 nouveau_user/2,
17 nouveau_user/3,
18 set_profile/9,
19 update_date_derniere_connexion/1,
20 update_ip/2,
21 update_pseudo_user/2,
22 users/0,
23 user_by_cookie/1,
24 user_by_id/1,
25 user_by_login/1,
26 user_by_login_password/2,
27 user_by_mess/1,
28 toggle_ek_master/1,
29
30 % messages :
31 nouveau_message/3,
32 nouveau_message_sys/1,
33 messages/1,
34 messages/2,
35 messages/3,
36 message_by_id/1,
37 messages_by_ids/1,
38 message_existe/1,
39 reponses/0,
40 repond_a/1,
41 est_une_reponse_a_user/2,
42 a_repondu_a_message/2,
43 possede_message/2,
44
45 % ip :
46 ip_table/0,
47 ban/2,
48 deban/1,
49 est_banni/1,
50 can_register/1,
51
52 % versions :
53 update_version/1,
54
55 % utiles :
56 resultat_transaction/1
57 ]).
58
59 -include("../include/euphorik_bd.hrl").
60 -include("../include/euphorik_defines.hrl").
61 -include_lib("stdlib/include/qlc.hrl").
62
63
64 % Instructions pour créer une nouvelle base :
65 % $erl -sname yaws -mnesia dir '"/projets/euphorik/BD"'
66 % voir doc/installation.txt
67 % >l(euphorik_bd).
68 % >euphorik_bd:create().
69 create() ->
70 mnesia:stop(),
71 mnesia:delete_schema([node()]),
72 mnesia:create_schema([node()]), % nécessaire pour les tables sur disc
73 mnesia:start(),
74 create_tables(),
75 reset().
76 create_tables() ->
77 mnesia:create_table(counter, [
78 {attributes, record_info(fields, counter)},
79 {disc_copies, [node()]}
80 ]),
81 mnesia:create_table(minichat, [
82 {attributes, record_info(fields, minichat)},
83 {index, [auteur_id]},
84 {disc_copies, [node()]}
85 ]),
86 mnesia:create_table(reponse_minichat, [
87 {type, bag},
88 {attributes, record_info(fields, reponse_minichat)},
89 {index, [cible]},
90 {disc_copies, [node()]}
91 ]),
92 mnesia:create_table(user, [
93 {attributes, record_info(fields, user)},
94 {index, [cookie, login]},
95 {disc_copies, [node()]}
96 ]),
97 mnesia:create_table(ip_table, [
98 {attributes, record_info(fields, ip_table)},
99 {disc_copies, [node()]}
100 ]),
101 mnesia:create_table(troll, [
102 {attributes, record_info(fields, troll)},
103 {disc_copies, [node()]}
104 ]).
105
106
107 % Connexion à la base de données de yaws sur overnux
108 connect() ->
109 connect(yaws@flynux).
110 connect(Node) ->
111 mnesia:start(),
112 mnesia:change_config(extra_db_nodes, [Node]).
113
114
115 % Efface tous les users, minichat_reponse et minichat.
116 reset() ->
117 mnesia:clear_table(counter),
118 mnesia:clear_table(user),
119 mnesia:clear_table(reponse_minichat),
120 mnesia:clear_table(minichat),
121 mnesia:clear_table(ip_table),
122 % crée l'utilisateur root
123 mnesia:transaction(fun() ->
124 User = #user{id = 0, pseudo = "Sys", login = "Sys", date_creation = now(), date_derniere_connexion = now(), ek_master = true},
125 mnesia:write(User),
126 User
127 end).
128
129
130 % Ajoute un nouveau user et le renvoie
131 nouveau_user(Pseudo, Cookie) ->
132 F = fun() ->
133 Id = nouvel_id(user),
134 User = #user{id = Id, cookie = Cookie, pseudo = Pseudo, date_creation = now(), date_derniere_connexion = now()},
135 mnesia:write(User),
136 User
137 end,
138 resultat_transaction(mnesia:transaction(F)).
139
140
141 % Ajoute un nouveau user et le renvoie
142 nouveau_user(Login, Password, Cookie) ->
143 F = fun() ->
144 Id = nouvel_id(user),
145 User = #user{id = Id, cookie = Cookie, pseudo = Login, login = Login, password = Password, date_creation = now(), date_derniere_connexion = now()},
146 mnesia:write(User),
147 User
148 end,
149 resultat_transaction(mnesia:transaction(F)).
150
151
152 % Mise à par Cookie les autres peuvent être undefined ce qui veut dire qu'ils ne seront pas modifié.
153 set_profile(Cookie, Login, Password, Pseudo, Email, Css, Nick_format, Page_principale, Conversations) ->
154 if Nick_format =:= nick; Nick_format =:= login; Nick_format =:= nick_login ->
155 resultat_transaction(mnesia:transaction(
156 fun() ->
157 case user_by_cookie(Cookie) of
158 {ok, User} ->
159 case user_by_login(Login) of
160 {ok, U} when User#user.login =/= [], U#user.id =/= User#user.id ->
161 login_deja_pris;
162 _ ->
163 User_modifie = User#user{
164 % TODO : pourquoi ne pas tester avec la valeur "undefined" plutôt qu'avec "is_list" ?
165 % TODO : validation plus strict des données (pas de page négative dans les conv par exemple)
166 login = if is_list(Login) -> Login; true -> User#user.login end,
167 password = if is_list(Password) andalso Password =/= [] -> Password; true -> User#user.password end,
168 pseudo = if is_list(Pseudo) -> Pseudo; true -> User#user.pseudo end,
169 email = if is_list(Email) -> Email; true -> User#user.email end,
170 css = if is_list(Css) -> Css; true -> User#user.css end,
171 nick_format = Nick_format,
172 page_principale = if is_integer(Page_principale), Page_principale > 0 -> Page_principale; true -> User#user.page_principale end,
173 conversations = if is_list(Conversations) -> Conversations; true -> User#user.conversations end
174 },
175 mnesia:write(User_modifie),
176 ok
177 end;
178 _ -> erreur
179 end
180 end
181 ));
182 true ->
183 erreur
184 end.
185
186
187 % Met à jour la date de la dernière connexion d'un utilisateur à maintenant
188 update_date_derniere_connexion(User_id) ->
189 mnesia:transaction(
190 fun() ->
191 case mnesia:wread({user, User_id}) of
192 [User] ->
193 mnesia:write(User#user{date_derniere_connexion = now()});
194 _ ->
195 mnesia:abort("update_date_derniere_connexion: User inconnu")
196 end
197 end
198 ).
199
200
201 % Met à jour l'ip d'un user
202 update_ip(User_id, IP) ->
203 mnesia:transaction(
204 fun() ->
205 case mnesia:wread({user, User_id}) of
206 [User] ->
207 mnesia:write(User#user{last_ip = IP});
208 _ ->
209 mnesia:abort("update_ip: User inconnu")
210 end
211 end
212 ).
213
214
215 % Met à jour le pseudo du user
216 update_pseudo_user(UserId, Pseudo) ->
217 mnesia:transaction(
218 fun() ->
219 case mnesia:wread({user, UserId}) of
220 [User] when User#user.pseudo =/= Pseudo ->
221 mnesia:write(User#user{pseudo = Pseudo});
222 _ ->
223 mnesia:abort("update_pseudo_user: User inconnu ou pseudo deja à jour")
224 end
225 end
226 ).
227
228
229 % Renvoie tous les users.
230 users() ->
231 resultat_transaction(mnesia:transaction(fun() ->
232 qlc:e(qlc:q([E || E <- mnesia:table(user)]))
233 end)).
234
235
236 % Est-ce qu'un utilisateur existe en fonction de son cookie ?
237 % Renvoie {ok, User} ou erreur
238 user_by_cookie(Cookie) ->
239 resultat_transaction(mnesia:transaction(
240 fun() ->
241 Users = qlc:e(qlc:q([E || E <- mnesia:table(user), E#user.cookie =:= Cookie])),
242 case Users of
243 [User] -> {ok, User};
244 _ -> erreur
245 end
246 end
247 )).
248
249
250 user_by_id(ID) ->
251 resultat_transaction(mnesia:transaction(
252 fun() ->
253 Users = qlc:e(qlc:q([E || E <- mnesia:table(user), E#user.id =:= ID])),
254 case Users of
255 [User] -> {ok, User};
256 _ -> erreur
257 end
258 end
259 )).
260
261
262 user_by_login(Login) ->
263 resultat_transaction(mnesia:transaction(
264 fun() ->
265 Users = qlc:e(qlc:q([E || E <- mnesia:table(user), E#user.login =:= Login])),
266 case Users of
267 [User] -> {ok, User};
268 _ -> erreur
269 end
270 end
271 )).
272
273
274 toggle_ek_master(User_id) ->
275 resultat_transaction(mnesia:transaction(
276 fun() ->
277 Users = qlc:e(qlc:q([E || E <- mnesia:table(user), E#user.id =:= User_id])),
278 case Users of
279 [User] ->
280 mnesia:write(User#user{ek_master = not User#user.ek_master});
281 _ -> erreur
282 end
283 end
284 )).
285
286
287 user_by_login_password(Login, Password) ->
288 resultat_transaction(mnesia:transaction(
289 fun() ->
290 Users = qlc:e(qlc:q([E || E <- mnesia:table(user), E#user.login =:= Login, E#user.password =:= Password])),
291 case Users of
292 [User] -> {ok, User};
293 _ -> erreur
294 end
295 end
296 )).
297
298
299 % Renvoie {ok, User} où User est un #user possédant le message donné.
300 user_by_mess(Id) ->
301 resultat_transaction(mnesia:transaction(
302 fun() ->
303 case qlc:e(qlc:q([U || U <- mnesia:table(user), M <- mnesia:table(minichat), M#minichat.id =:= Id, M#minichat.auteur_id =:= U#user.id])) of
304 [User] -> {ok, User};
305 _ -> erreur
306 end
307 end
308 )).
309
310
311 % Ajoute un message. Repond_A est une liste d'id auquel le message répond
312 % retourne soit l'id du message soit erreur.
313 nouveau_message(Mess, Auteur_id, Repond_A) ->
314 % regarde si les id 'Repond_A' existent
315 F = fun() ->
316 Nb_id_trouve = length(qlc:e(qlc:q([E#minichat.id || E <- mnesia:table(minichat), lists:member(E#minichat.id, Repond_A)]))),
317 % est-ce que l'auteur existe ?
318 Auteur = case qlc:e(qlc:q([E || E <- mnesia:table(user), E#user.id =:= Auteur_id])) of
319 [A] -> A;
320 _ -> throw("L'auteur du message est introuvable")
321 end,
322 if Nb_id_trouve =/= length(Repond_A) -> throw("Un ou plusieurs messages introuvable");
323 true -> ok
324 end,
325 % compare les dernière
326 Delta = delta_date_ms(Auteur#user.date_derniere_connexion, now()),
327 Nouvel_indice_flood = Auteur#user.indice_flood + if Delta =< ?DUREE_SPAM -> 2; true -> -1 end,
328 Auteur_maj = Auteur#user{
329 indice_flood = if Nouvel_indice_flood > ?INDICE_SPAM_MAX -> ?INDICE_SPAM_MAX; Nouvel_indice_flood < 0 -> 0; true -> Nouvel_indice_flood end,
330 date_derniere_connexion = now()
331 },
332 % est-ce que l'auteur à trop floodé ?
333 if Auteur#user.indice_flood =/= ?INDICE_SPAM_MAX, Auteur_maj#user.indice_flood =:= ?INDICE_SPAM_MAX, Delta =< ?DUREE_BLOCAGE_SPAM ->
334 mnesia:write(Auteur#user{indice_flood = Auteur_maj#user.indice_flood}),
335 nouveau_message_sys(Auteur#user.pseudo ++ "(" ++ Auteur#user.login ++ ") est bloqué pour " ++ integer_to_list(trunc(?DUREE_BLOCAGE_SPAM / 1000)) ++ " secondes pour cause de flood..");
336 Auteur#user.indice_flood =:= ?INDICE_SPAM_MAX, Delta =< ?DUREE_BLOCAGE_SPAM ->
337 erreur;
338 true ->
339 mnesia:write(Auteur_maj),
340 Id = nouvel_id(minichat),
341 inserer_reponses(Id, Repond_A),
342 mnesia:write(#minichat{id=Id, auteur_id=Auteur#user.id, date=now(), pseudo=Auteur#user.pseudo, contenu=Mess}),
343 Id
344 end
345 end,
346 resultat_transaction(mnesia:transaction(F)).
347
348 % Définit Id_repondant comme étant la réponse à Ids. Ids est une liste d'id.
349 inserer_reponses(Id_repondant, [Id_mess | Reste]) ->
350 mnesia:write(#reponse_minichat{repondant = Id_repondant, cible = Id_mess}),
351 inserer_reponses(Id_repondant, Reste);
352 inserer_reponses(_, []) ->
353 ok.
354
355
356 % Permet de créer un message système.
357 % Renvoie l'id du message système
358 nouveau_message_sys(Mess) ->
359 {ok, Root} = user_by_id(0),
360 mnesia:transaction(
361 fun() ->
362 Id = nouvel_id(minichat),
363 mnesia:write(#minichat{id=Id, auteur_id=0, date=now(), pseudo=Root#user.pseudo, contenu=Mess})
364 end
365 ).
366
367
368 % Renvoie N messages se trouvant sur la première page
369 messages(N) ->
370 messages(N, 1).
371
372
373 % Renvoie N messages se trouvant sur la page P
374 messages(N, P) ->
375 F = fun() ->
376 C = qlc:cursor(qlc:q([E || E <- qlc:keysort(2, mnesia:table(minichat), [{order, descending}])])),
377 if P > 1 -> qlc:next_answers(C, N * (P - 1));
378 true -> ok
379 end,
380 R = qlc:next_answers(C, N),
381 qlc:delete_cursor(C),
382 lists:reverse(R)
383 end,
384 resultat_transaction(mnesia:transaction(F)).
385
386
387 % Renvoie les messages manquants pour la page P en sachant qu'il y a N message
388 % par page et que le dernier message que l'on possède est Id
389 messages(Id, N, P) ->
390 lists:filter(fun (M) -> M#minichat.id > Id end, messages(N, P)).
391
392
393 % Renvoie {ok, #minichat} (voir #minichat de euphorik_bd.hrl) à partir de son id.
394 message_by_id(Id) ->
395 case resultat_transaction(mnesia:transaction(
396 fun() ->
397 qlc:e(qlc:q([E || E <- qlc:keysort(2, mnesia:table(minichat), [{order, ascending}]), Id =:= E#minichat.id]))
398 end
399 )) of
400 [M] -> {ok, M};
401 _ -> erreur
402 end.
403
404
405 % Renvoie une liste de message (voir #minichat de euphorik_bd.hrl) à partir d'une liste d'id (Ids).
406 messages_by_ids(Ids) ->
407 resultat_transaction(mnesia:transaction(
408 fun() ->
409 % TODO : optimisations ? serait-ce du O(n) ?
410 Query = qlc:q([E || E <- qlc:keysort(2, mnesia:table(minichat), [{order, ascending}]), lists:any(fun(Id) -> Id =:= E#minichat.id end, Ids)]),
411 qlc:e(Query)
412 end
413 )).
414
415
416 % Est-ce qu'un message existe ? Renvoie un boolean.
417 % TODO : ya pas plus simple ?
418 message_existe(Id) ->
419 resultat_transaction(mnesia:transaction(fun() ->
420 length(qlc:e(qlc:q([E#minichat.id || E <- mnesia:table(minichat), E#minichat.id =:= Id]))) =:= 1
421 end)).
422
423
424 % Renvoie les reponses (utilisé normalement uniquement pendant le debug).
425 reponses() ->
426 F = fun() ->
427 qlc:e(qlc:q([E || E <- mnesia:table(reponse_minichat)]))
428 end,
429 resultat_transaction(mnesia:transaction(F)).
430
431
432 % Renvoie les messages auquel M_id répond.
433 repond_a(M_id) ->
434 resultat_transaction(mnesia:transaction(
435 fun() ->
436 qlc:e(qlc:q(
437 [M || E <- mnesia:table(reponse_minichat),
438 M <- mnesia:table(minichat),
439 E#reponse_minichat.repondant =:= M_id,
440 M#minichat.id =:= E#reponse_minichat.cible]))
441 end
442 )).
443
444
445 % Est-ce que le message Id_mess est une réponse d'une message de Id_user ?
446 est_une_reponse_a_user(Id_user, Id_mess) ->
447 case mnesia:transaction(
448 fun() ->
449 qlc:e(qlc:q([
450 M#minichat.auteur_id || M <- mnesia:table(minichat), R <- mnesia:table(reponse_minichat),
451 M#minichat.auteur_id =:= Id_user, M#minichat.id =:= R#reponse_minichat.cible, R#reponse_minichat.repondant =:= Id_mess
452 ]), [{unique_all, true}])
453 end
454 ) of
455 {atomic, [_]} -> true;
456 _ -> false
457 end.
458
459
460 % Est-ce que Id_user à répondu au message Id_mess
461 a_repondu_a_message(Id_user, Id_mess) ->
462 case mnesia:transaction(
463 fun() ->
464 qlc:e(qlc:q([
465 M#minichat.auteur_id || M <- mnesia:table(minichat), R <- mnesia:table(reponse_minichat),
466 R#reponse_minichat.cible =:= Id_mess, R#reponse_minichat.repondant =:= M#minichat.id, M#minichat.auteur_id =:= Id_user
467 ]), [{unique_all, true}])
468 end
469 ) of
470 {atomic, [_]} -> true;
471 _ -> false
472 end.
473
474
475 % Est-ce que Id_user possède Id_mess ?
476 possede_message(Id_user, Id_mess) ->
477 case mnesia:transaction(
478 fun() ->
479 qlc:e(qlc:q([E#minichat.auteur_id || E <- mnesia:table(minichat), E#minichat.id =:= Id_mess]))
480 end
481 ) of
482 {atomic, [Id_user | []]} -> true;
483 _ -> false
484 end.
485
486
487 ip_table() ->
488 resultat_transaction(mnesia:transaction(
489 fun() ->
490 qlc:e(qlc:q([IP || IP <- mnesia:table(ip_table)]))
491 end
492 )).
493
494
495 % Bannie une ip pour un certain temps (en minute).
496 ban(IP, Duration) ->
497 mnesia:transaction(
498 fun() ->
499 case mnesia:wread({ip_table, IP}) of
500 [IP_tuple] ->
501 mnesia:write(IP_tuple#ip_table{ban = now(), ban_duration = Duration});
502 _ ->
503 mnesia:write(#ip_table{ip = IP, ban = now(), ban_duration = Duration})
504 end
505 end
506 ).
507
508
509 % Débanni une ip
510 deban(IP) ->
511 ban(IP, 0).
512
513
514 % Renvoie soit {true, Temps} où Temps est le temps en minutes pendant lequel le user est encore banni
515 % ou false.
516 est_banni(User_id) ->
517 resultat_transaction(mnesia:transaction(
518 fun() ->
519 case qlc:e(qlc:q([
520 {IP#ip_table.ban, IP#ip_table.ban_duration} ||
521 U <- mnesia:table(user),
522 U#user.id =:= User_id,
523 IP <- mnesia:table(ip_table),
524 IP#ip_table.ip =:= U#user.last_ip
525 ])) of
526 [{Ban, Ban_duration}] ->
527 Echeance = date_plus_minutes(Ban, Ban_duration),
528 Now = now(),
529 if Echeance < Now -> % l'échéance est passée
530 false;
531 true ->
532 {true, trunc(delta_date_ms(Echeance, Now) / 1000 / 60)}
533 end;
534 _ ->
535 false
536 end
537 end
538 )).
539
540
541 % Ban est une date tel que retourner par now().
542 % Ban_duration est un temps en minutes.
543 % retourne une date.
544 date_plus_minutes(Ban, Ban_duration) ->
545 Duration_sec = Ban_duration * 60,
546 {MegaSec, Sec, MicroSec} = Ban,
547 {MegaSec + if Sec + Duration_sec >= 1000000 -> 1; true -> 0 end,(Sec + Duration_sec) rem 1000000, MicroSec}.
548
549
550 % Si deux enregistrements consequtifs de la même IP sont fait en moins d'une seconde alors
551 % ip_table.nb_try_register est incrémenté de 1 sinon il est décrémenté de 1 (jusqu'a 0).
552 % Si ip_table.nb_try_register vaut 5 alors l'ip ne peux plus s'enregistrer pour une heure.
553 can_register(IP) ->
554 resultat_transaction(mnesia:transaction(
555 fun() ->
556 case qlc:e(qlc:q([I || I <- mnesia:table(ip_table), I#ip_table.ip =:= IP])) of
557 [] ->
558 mnesia:write(#ip_table{ip = IP, date_last_try_register = now()}),
559 true;
560 [T] ->
561 Delta = delta_date_ms(T#ip_table.date_last_try_register, now()),
562 if T#ip_table.nb_try_register =:= ?NB_MAX_FLOOD_REGISTER, Delta < ?TEMPS_BAN_FLOOD_REGISTER ->
563 false;
564 true ->
565 mnesia:write(#ip_table{
566 ip = IP,
567 date_last_try_register = now(),
568 nb_try_register = T#ip_table.nb_try_register + if Delta < ?TEMPS_FLOOD_REGISTER -> 1; T#ip_table.nb_try_register > 0 -> -1; true -> 0 end
569 }),
570 true
571 end
572 end
573 end
574 )).
575
576
577 update_version(1) ->
578 mnesia:transform_table(
579 ip_table,
580 fun() -> null end,
581 record_info(fields, ip_table),
582 ip_table
583 ).
584
585
586 % Renvoie le résultat d'une transaction (en décomposant le tuple fournit)
587 resultat_transaction({_, T}) ->
588 T.
589
590
591 % Retourne la difference entre deux timestamp (erlang:now()) en miliseconde
592 delta_date_ms(D1, D2) ->
593 1000000000 * abs(element(1, D1) - element(1, D2)) + 1000 * abs(element(2, D1) - element(2, D2)) + trunc(abs(element(3, D1) - element(3, D2)) / 1000).
594
595
596 % Bizarre, cette fonction n'existe pas dans la stdlib.
597 % Pas utilisé mais bon ca me fait de la peine de l'enlever.
598 ceiling(X) ->
599 T = trunc(X),
600 case (X - T) of
601 Neg when Neg < 0 -> T;
602 Pos when Pos > 0 -> T + 1;
603 _ -> T
604 end.
605
606
607 % Renvoie un nouvel id pour une table donnée
608 nouvel_id(Table) ->
609 mnesia:dirty_update_counter(counter, Table, 1).
610