2 % Copyright 2008 Grégory Burri
4 % This file is part of Euphorik.
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.
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.
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/>.
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.
24 -module(euphorik_protocole
).
40 -include("../include/euphorik_bd.hrl").
41 -include("../include/euphorik_defines.hrl").
44 % Une utilisateur s'enregistre avec un tuple {Login, Password}.
45 register([{"login", Login
}, {"password", Password
}, {"profile", Profile_json
}], IP
) ->
46 Can_register
= euphorik_bd:can_register(IP
),
48 case euphorik_bd:user_by_login(Login
) of
52 User
= euphorik_bd:nouveau_user(Login
, Password
, generer_cookie(), profile_from_json(Profile_json
)),
53 euphorik_bd:update_ip(User#user
.id
, IP
),
54 json_reponse_login_ok(User
)
57 erreur_register_flood()
59 % Enregistrement sans {Login, Password}
60 register([{"profile", Profile_json
}], IP
) ->
61 Can_register
= euphorik_bd:can_register(IP
),
63 Profile
= profile_from_json(Profile_json
),
64 User
= euphorik_bd:nouveau_user(generer_cookie(), Profile#profile
{pseudo
= "<nick>"}),
65 euphorik_bd:update_ip(User#user
.id
, IP
),
66 json_reponse_login_ok(User
);
68 erreur_register_flood()
71 erreur_register_flood() ->
75 % Un utilisateur se logge (avec un couple {login, mot de passe})
76 login([{"login", Login
}, {"password", Password
}], IP
) ->
77 case euphorik_bd:user_by_login_password(Login
, Password
) of
81 timer:sleep(?TEMPS_ATTENTE_ERREUR_LOGIN
),
84 % Un utilisateur se logge (avec un cookie)
85 login([{"cookie", Cookie
}], IP
) ->
86 case euphorik_bd:user_by_cookie(Cookie
) of
90 timer:sleep(?TEMPS_ATTENTE_ERREUR_LOGIN
),
95 % L'utilisateur donné se logge avec l'ip donnée.
96 loginUser(User
, IP
) ->
97 euphorik_bd:update_ip(User#user
.id
, IP
),
98 euphorik_bd:update_date_derniere_connexion(User#user
.id
),
99 json_reponse_login_ok(User
).
102 % Renvoie un string() représentant un cookie en base 36. Il y a 10^32 possibillités.
104 {A1
, A2
, A3
} = erlang:timestamp(),
105 random:seed(A1
, A2
, A3
),
106 erlang:integer_to_list(random:uniform(trunc(math:pow(10, 32))), 36).
109 % Modification du profile.
114 {"password", Password
},
115 {"profile", Profile_json
}
118 case profile_from_json(Profile_json
) of
121 case euphorik_bd:set_profile(Cookie
, Login
, Password
, Profile
) of
132 % Construit un #user à partir des données JSON
139 {"chat_order", Chat_order_str
},
140 {"nick_format", Nick_format_str
},
141 {"view_times", View_times
},
142 {"view_tooltips", View_tooltips
},
143 {"conversations", {array
, Conversations_json
}},
144 {"ostentatious_master", Ostentatious_master_str
}
148 % décomposition de la strucure JSON
149 Conversations
= lists:foldr(
150 fun({struct
, [{"root", Racine
}, {"minimized", Reduit
}]}, A
) ->
151 % virage des messages qui n'existent pas
152 Message_exite
= euphorik_bd:message_existe(Racine
),
154 [ {Racine
, Reduit
} | A
];
162 % vérification des données JSON
163 Chat_order
= list_to_atom(Chat_order_str
),
164 Chat_order_valide
= lists:any(fun(E
) -> E
=:= Chat_order
end, [reverse
, chrono
]),
165 if not Chat_order_valide
->
166 {erreur
, Chat_order_str
++ " n'est pas une valeur acceptée pour 'chat_order'"};
168 Nick_format
= list_to_atom(Nick_format_str
),
169 Nick_format_valide
= lists:any(fun(E
) -> E
=:= Nick_format
end, [nick
, login
, nick_login
]),
170 if not Nick_format_valide
->
171 {erreur
, Nick_format_str
++ " n'est pas une valeur acceptée pour 'nick_format'"};
173 Ostentatious_master
= list_to_atom(Ostentatious_master_str
),
174 Ostentatious_master_valide
= lists:any(fun(E
) -> E
=:= Ostentatious_master
end, [invisible
, light
, heavy
]),
175 if not Ostentatious_master_valide
->
176 {erreur
, Ostentatious_master_str
++ " n'est pas une valeur acceptée pour 'ostentatious_master'"};
182 chat_order
= Chat_order
,
183 nick_format
= Nick_format
,
184 view_times
= View_times
,
185 view_tooltips
= View_tooltips
,
186 conversations
= Conversations
,
187 ostentatious_master
= Ostentatious_master
194 % Attend un événement pour la page "Chat"
195 % last_message id et cookie sont facultatifs
196 wait_event([{"page", "chat"} | Data
]) ->
197 % traitement des inputs
198 Cookie
= case lists:keysearch("cookie", 1, Data
) of {value
, {_
, C
}} -> C
; _
-> inconnu
end,
199 Last_message_id
= case lists:keysearch("last_message_id", 1, Data
) of {value
, {_
, Id
}} -> Id
; _
-> 0 end,
200 {value
, {_
, Message_count
}} = lists:keysearch("message_count", 1, Data
),
201 Main_page
= case lists:keysearch("main_page", 1, Data
) of {value
, {_
, P
}} -> P
; _
-> 1 end,
202 Troll_id
= case lists:keysearch("troll_id", 1, Data
) of {value
, {_
, T
}} -> T
; _
-> 0 end,
203 {value
, {_
, {array
, Conversations_json
}}} = lists:keysearch("conversations", 1, Data
),
205 Racines_conversations
= lists:map(
206 fun({struct
, [{"root", Racine
}, {"page", Page
} | Reste
]}) ->
207 Last_mess_conv
= case Reste
of [{"last_message_id", L
}] -> L
; _
-> 0 end,
208 {Racine
, Page
, Last_mess_conv
}
212 User
= case euphorik_bd:user_by_cookie(Cookie
) of
217 case {mnesia:subscribe({table
, minichat
, detailed
}), mnesia:subscribe({table
, troll
, detailed
})} of
218 {{error
, E
}, _
} -> E
;
219 {_
, {error
, E
}} -> E
;
221 % attente d'événements
222 R
= wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
, Troll_id
),
223 mnesia:unsubscribe({table
, minichat
, detailed
}),
224 mnesia:unsubscribe({table
, troll
, detailed
}),
227 % Attend un événement pour la page "Admin"
228 wait_event([{"page", "admin"}, {"last_troll", Last_troll
}]) ->
229 case wait_event_page_admin(Last_troll
) of
230 banned_ips_refresh
->
233 {"reply", "banned_ips_refresh"}
239 {"reply", "troll_modified"},
240 {"troll_id", Troll#troll
.id
},
241 {"content", Troll#troll
.content
}
247 {"reply", "troll_added"},
251 {ok
, #user
{profile
= Profile
} = User
} = euphorik_bd:user_by_id(T#troll
.id_user
),
254 {"troll_id", T#troll
.id
},
255 {"content", T#troll
.content
},
256 {"author", Profile#profile
.pseudo
},
257 {"author_id", User#user
.id
}
269 {"reply", "troll_deleted"},
270 {"troll_id", Troll_id
}
280 % Attend un événement pour la page "Chat" et renvoie soit un troll soit les messages manquants au client.
281 wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
, Troll_id
) ->
282 % est-ce que le troll est à jour ?
283 case euphorik_bd:current_troll() of
284 Current
when is_record(Current
, troll
), Current#troll
.id
=/= Troll_id
->
286 {"reply", "new_troll"},
287 {"troll_id", Current#troll
.id
},
288 {"message_id", Current#troll
.id_minichat
},
289 {"content", Current#troll
.content
}
292 % est-ce qu'il y a des nouveaux messages ?
293 case euphorik_minichat_conversation:conversations(Racines_conversations
, Message_count
, Last_message_id
, Main_page
) of
295 wait_event_bd_page_chat(),
296 wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
, Troll_id
);
298 % accrochez-vous ca va siouxer ;)
300 {"reply", "new_messages"},
301 {"conversations", {array
,
303 fun({Racine
, {Conv
, Plus
}}) ->
305 {"last_page", not Plus
},
306 {"first", % le premier message de la conversation
307 if Racine
=:= undefined orelse Conv
=:= [] ->
310 {Racine_id
, _
, _
} = Racine
,
311 case euphorik_bd:message_by_id(Racine_id
) of
313 json_message(Mess
, euphorik_bd:parents_id(Racine_id
), User
);
321 fun({Mess
, Repond_a
}) ->
322 json_message(Mess
, Repond_a
, User
)
329 % on ajoute un 'undefined' correspondant à la premier conversation qui ne possède pas de racine
330 % TODO : peut être à revoir car un peu lourd est compliqué
331 aggregation_racines_conversations([undefined
| Racines_conversations
], Conversations
)
339 aggregation_racines_conversations(L1
, L2
) ->
340 aggregation_racines_conversations(L1
, L2
, []).
341 aggregation_racines_conversations([], [], L
) -> lists:reverse(L
);
342 aggregation_racines_conversations([E1
|R1
], [E2
|R2
], L
) ->
343 aggregation_racines_conversations(R1
, R2
, [{E1
, E2
} | L
]).
347 % Attend un événement lié à la page 'chat'.
348 wait_event_bd_page_chat() ->
349 receive % attente d'un post
350 {mnesia_table_event
, {write
, minichat
, _Message
, [], _
}} ->
352 {mnesia_table_event
, {write
, troll
, Troll
, [Old_troll
| _
], _
}} when Troll#troll
.date_post
=/= undefined
, Old_troll#troll
.date_post
== undefined
->
357 wait_event_bd_page_chat()
358 % 60 minutes de timeout (on ne sais jamais)
359 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
360 after 1000 * 60 * 60 ->
365 % Attent un événement concernant la page admin
366 % Renvoie les trolls manquants posté après Last_id ou banned_ips_refresh.
367 % Si pas de trolls alors attend un événement tel qu'un ajout, une modification ou une suppression.
372 % ou banned_ips_refresh
374 wait_event_page_admin(Last_id
) ->
375 case {mnesia:subscribe({table
, troll
, detailed
}), mnesia:subscribe({table
, ip_table
, detailed
})} of
376 {{error
, E
}, _
} -> E
;
377 {_
, {error
, E
}} -> E
;
379 R
= case euphorik_bd:trolls(Last_id
) of
380 [] -> % pas de trolls
381 wait_event_page_admin();
385 mnesia:unsubscribe({table
, troll
, detailed
}),
386 mnesia:unsubscribe({table
, ip_table
, detailed
}),
390 wait_event_page_admin() ->
391 % s'il n'y a pas de trolls que l'utilisateur n'a pas connaissance alors on attend un événement
393 % cas où un troll est choisit comme courant
394 {mnesia_table_event
, {write
, troll
, Troll
, [Old_troll
| _
], _
}}
395 when Old_troll#troll
.date_post
=:= undefined
, Troll#troll
.date_post
=/= undefined
->
396 {del
, Troll#troll
.id
};
397 {mnesia_table_event
, {write
, troll
, Troll
, [_Old_troll
| _
], _
}} ->
399 {mnesia_table_event
, {write
, troll
, Troll
, [], _
}} ->
401 {mnesia_table_event
, {delete
, troll
, {troll
, Id
}, _
, _
}} ->
403 {mnesia_table_event
, {write
, ip_table
, IP
, [Old_IP
| _
], _
}}
404 when Old_IP#ip_table
.ban
=/= IP#ip_table
.ban
; Old_IP#ip_table
.ban_duration
=/= IP#ip_table
.ban_duration
->
409 wait_event_page_admin()
410 % 60 minutes de timeout (on ne sais jamais)
411 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
412 after 1000 * 60 * 60 ->
417 % Un utilisateur envoie un message
418 % Answer_to est une liste d'id (int)
423 {"content", Content
},
424 {"answer_to", {array
, Answer_to
}}
427 case euphorik_bd:user_by_cookie(Cookie
) of
429 case euphorik_bd:est_banni(User#user
.id
) of
430 {true
, Temps_restant
} ->
431 erreur(80, [format_minutes(Temps_restant
)]);
433 Strip_content
= string:strip(Content
),
434 if Strip_content
=:= [] ->
437 % attention : non-atomique (update_pseudo+nouveau_message)
438 euphorik_bd:update_pseudo_user(User#user
.id
, Nick
),
439 case euphorik_bd:nouveau_message(Strip_content
, User#user
.id
, Answer_to
) of
440 {erreur
, R
} -> erreur(100, [R
]);
451 % bannissement d'un utilisateur (son ip est bannie)
455 {"duration", Duration
},
456 {"user_id", User_id
},
459 % controle que l'utilisateur est un admin
460 case euphorik_bd:user_by_cookie(Cookie
) of
461 {ok
, User1
= #user
{ek_master
= true
}} ->
462 case euphorik_bd:user_by_id(User_id
) of
465 {ok
, #user
{ek_master
= false
, profile
= Profile2
} = User2
} ->
466 euphorik_bd:ban(User2#user
.last_ip
, Duration
),
467 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("\"~s~s\" est ~s pour ~s.~s",
469 Profile2#profile
.pseudo
,
470 if User2#user
.login
=:= [] -> ""; true
-> " (" ++ User2#user
.login
++ ")" end,
471 if Duration
=< 15 -> "kické"; true
-> "banni" end,
472 format_minutes(Duration
),
473 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end ++ "."
487 % slapage d'un user (avertissement)
491 {"user_id", User_id
},
494 % controle que l'utilisateur est un admin
495 case euphorik_bd:user_by_cookie(Cookie
) of
496 {ok
, User1
= #user
{ek_master
= true
, profile
= Profile1
}} ->
497 case euphorik_bd:user_by_id(User_id
) of
499 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s s'auto slap~s.",
501 Profile1#profile
.pseudo
,
502 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end
506 {ok
, #user
{ek_master
= false
, profile
= Profile2
}} ->
507 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s se fait slaper par ~s.~s",
509 Profile2#profile
.pseudo
,
510 Profile1#profile
.pseudo
,
511 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end ++ "."
531 % controle que l'utilisateur est un admin
532 case euphorik_bd:user_by_cookie(Cookie
) of
533 {ok
, User
= #user
{ek_master
= true
}} ->
534 case euphorik_bd:put_troll(User#user
.id
, Content
) of
535 max_troll_reached_per_user
->
536 erreur(180, [?NB_MAX_TROLL_WAITING_BY_USER
]);
538 erreur(190, [?NB_MAX_TROLL_WAITING
]);
550 {"troll_id", Troll_id
},
554 % controle que l'utilisateur est un admin
555 case euphorik_bd:user_by_cookie(Cookie
) of
556 {ok
, User
= #user
{ek_master
= true
}} ->
557 User_id
= User#user
.id
,
558 case euphorik_bd:troll_by_id(Troll_id
) of
559 {ok
, #troll
{id_user
= User_id
}} ->
560 euphorik_bd:mod_troll(Troll_id
, Content
),
573 {"troll_id", Troll_id
}
576 % controle que l'utilisateur est un admin
577 case euphorik_bd:user_by_cookie(Cookie
) of
578 {ok
, User
= #user
{ek_master
= true
}} ->
579 User_id
= User#user
.id
,
580 case euphorik_bd:troll_by_id(Troll_id
) of
581 {ok
, #troll
{id_user
= User_id
}} ->
582 euphorik_bd:del_troll(Troll_id
),
598 case euphorik_bd:user_by_cookie(Cookie
) of
599 {ok
, #user
{ek_master
= true
}} ->
600 euphorik_bd:deban(euphorik_common:unserialize_ip(IP
)),
612 case euphorik_bd:user_by_cookie(Cookie
) of
613 {ok
, #user
{ek_master
= true
}} ->
617 {"reply", "list_banned_ips"},
618 {"list", {array
, lists:map(
619 fun({IP
, T
, Users
}) ->
622 {"ip", euphorik_common:serialize_ip(IP
)},
623 {"remaining_time", format_minutes(T
)},
624 {"users", {array
, lists:map(
625 fun({Pseudo
, Login
}) ->
638 euphorik_bd:list_ban()
647 % Construit une erreur
649 erreur_json(Num
, lists:flatten(io_lib:format(euphorik_bd:get_texte(Num
), Args
))).
653 erreur_json(Num
, euphorik_bd:get_texte(Num
)).
656 erreur_json(Num
, Mess
) ->
661 {"error_message", Mess
}
666 % Formatage de minutes.
667 % par exemple : "1min", "45min", "1h23min", "1jour 2h34min"
668 format_minutes(Min
) ->
669 Jours
= Min
div (60 * 24),
670 Heures
= Min
rem (60 * 24) div
60,
671 Minutes
= Min
rem (60),
672 if Jours
=/= 0 -> integer_to_list(Jours
) ++ " Jour" ++ if Jours
> 1 -> "s"; true
-> "" end ++ " "; true
-> "" end ++
673 if Heures
=/= 0 -> integer_to_list(Heures
) ++ " heure" ++ if Heures
> 1 -> "s"; true
-> "" end; true
-> "" end ++
677 " " ++ integer_to_list(Minutes
) ++ " minute" ++ if Minutes
> 1 -> "s"; true
-> "" end
681 % Formatage d'une heure
682 % local_time() -> string
684 DateLocal
= calendar:now_to_local_time(Date
),
685 DateNowLocal
= calendar:local_time(),
686 {{Annee
, Mois
, Jour
}, {Heure
, Minute
, Seconde
}} = DateLocal
,
687 {{AnneeNow
, _
, _
}, {_
, _
, _
}} = DateNowLocal
,
688 Hier
= calendar:date_to_gregorian_days(element(1, DateLocal
)) =:= calendar:date_to_gregorian_days(element(1, DateNowLocal
)) - 1,
690 if element(1, DateLocal
) =:= element(1, DateNowLocal
) ->
694 Annee
=:= AnneeNow
->
695 io_lib:format("~2.10.0B/~2.10.0B ", [Jour
, Mois
]);
697 io_lib:format("~2.10.0B/~2.10.0B/~B ", [Jour
, Mois
, Annee
])
699 io_lib:format("~2.10.0B:~2.10.0B:~2.10.0B", [Heure
, Minute
, Seconde
])
704 {struct
, [{"reply", "ok"}]}.
707 json_reponse_login_ok(#user
{profile
= Profile
} = User
) ->
711 {"status", if (User#user
.password
=/= []) and (User#user
.login
=/= []) -> "auth_registered"; true
-> "auth_not_registered" end},
712 {"cookie", User#user
.cookie
},
713 {"id", User#user
.id
},
714 {"login", User#user
.login
},
715 {"ek_master", User#user
.ek_master
},
718 {"nick", Profile#profile
.pseudo
},
719 {"email", Profile#profile
.email
},
720 {"css", Profile#profile
.css
},
721 {"chat_order", atom_to_list(Profile#profile
.chat_order
)},
722 {"nick_format", atom_to_list(Profile#profile
.nick_format
)},
723 {"view_times", Profile#profile
.view_times
},
724 {"view_tooltips", Profile#profile
.view_tooltips
},
725 {"conversations", {array
, lists:map(
726 fun({Racine
, Reduit
}) ->
727 {struct
, [{"root", Racine
}, {"minimized", Reduit
}]}
729 Profile#profile
.conversations
731 {"ostentatious_master", atom_to_list(Profile#profile
.ostentatious_master
)}
737 % Renvoie le message formaté en JSON.
738 % Mess est de type #minichat
739 % Repond_a est une liste d'id des messages auquel répond Mess
740 % User est l'utilisateur courant de type #user
741 json_message(Mess
, Repond_a
, User
) ->
742 Est_proprietaire
= User
=/= inconnu andalso User#user
.id
=:= Mess#minichat
.auteur_id
,
743 A_repondu_a_message
= User
=/= inconnu andalso
euphorik_bd:a_repondu_a_message(User#user
.id
, Mess#minichat
.id
),
744 Est_une_reponse_a_user
= User
=/= inconnu andalso
euphorik_bd:est_une_reponse_a_user(User#user
.id
, Mess#minichat
.id
),
745 {ok
, #user
{profile
= Profile_mess
} = User_mess
} = euphorik_bd:user_by_id(Mess#minichat
.auteur_id
),
747 {"id", Mess#minichat
.id
},
748 {"user_id", User_mess#user
.id
},
749 {"date", case Mess#minichat
.date of undefined
-> "?"; _
-> format_date(Mess#minichat
.date) end},
750 {"system", Mess#minichat
.auteur_id
=:= 0},
751 {"owner", Est_proprietaire
},
752 {"answered", A_repondu_a_message
},
753 {"is_a_reply", Est_une_reponse_a_user
},
754 {"nick", Mess#minichat
.pseudo
},
755 {"login", User_mess#user
.login
},
756 {"content", Mess#minichat
.contenu
},
757 {"root", Mess#minichat
.racine_id
},
758 {"answer_to", {array
, lists:map(
760 {ok
, M
} = euphorik_bd:message_by_id(Id_mess
),
761 {ok
, User_reponse
} = euphorik_bd:user_by_mess(M#minichat
.id
),
762 {struct
, [{"id", M#minichat
.id
}, {"nick", M#minichat
.pseudo
}, {"login", User_reponse#user
.login
}]}
766 {"ek_master", User_mess#user
.ek_master
},
767 {"ostentatious_master", atom_to_list(Profile_mess#profile
.ostentatious_master
)}