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
).
41 -include("../include/euphorik_bd.hrl").
42 -include("../include/euphorik_defines.hrl").
45 % Une utilisateur s'enregistre avec un tuple {Login, Password}.
46 register([{login
, Login
}, {password
, Password
}, {profile_infos
, Profile_infos
}], IP
) ->
47 Can_register
= euphorik_bd:can_register(IP
),
49 case euphorik_bd:user_by_login(Login
) of
51 erreur("Login déjà existant");
53 User
= euphorik_bd:nouveau_user(Login
, Password
, generer_cookie(), user_from_json(Profile_infos
)),
54 euphorik_bd:update_ip(User#user
.id
, IP
),
55 json_reponse_login_ok(User
)
58 erreur_register_flood()
60 % Enregistrement sans {Login, Password}
61 register([{profile_infos
, Profile_infos
}], IP
) ->
62 Can_register
= euphorik_bd:can_register(IP
),
64 User
= euphorik_bd:nouveau_user("<nick>", generer_cookie(), user_from_json(Profile_infos
)),
65 euphorik_bd:update_ip(User#user
.id
, IP
),
66 json_reponse_login_ok(User
);
68 erreur_register_flood()
71 erreur_register_flood() ->
72 erreur("Trop de 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
),
82 erreur("Couple login/pass introuvable")
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
),
91 erreur("Authentification impossible par cookie")
94 loginUser(User
, IP
) ->
95 euphorik_bd:update_ip(User#user
.id
, IP
),
96 euphorik_bd:update_date_derniere_connexion(User#user
.id
),
97 json_reponse_login_ok(User
).
100 % Renvoie un string() représentant un cookie en base 36. Il y a 10^32 possibillités.
102 {A1
, A2
, A3
} = now(),
103 random:seed(A1
, A2
, A3
),
104 erlang:integer_to_list(random:uniform(math:pow(10, 32)), 36).
107 % Un utilisateur se délogge.
112 % Modification du profile.
117 {password
, Password
},
118 {profile_infos
, Profile_infos
}
121 case user_from_json(Profile_infos
) of
124 User
= UserInfos#user
{
129 % TODO : pas très beau, mieux vaut construire un #user
130 case euphorik_bd:set_profile(User
) of
134 erreur("Login déjà pris");
136 erreur("Impossible de mettre à jour le profile")
141 % Construit un #user à partir des données JSON
148 {chat_order
, Chat_order_str
},
149 {nick_format
, Nick_format_str
},
150 {view_times
, View_times
},
151 {view_tooltips
, View_tooltips
},
152 {conversations
, {array
, Conversations_json
}},
153 {ostentatious_master
, Ostentatious_master_str
}
157 % décomposition de la strucure JSON
158 Conversations
= lists:foldr(
159 fun({struct
, [{root
, Racine
}, {minimized
, Reduit
}]}, A
) ->
160 % virage des messages qui n'existent pas
161 Message_exite
= euphorik_bd:message_existe(Racine
),
163 [ {Racine
, Reduit
} | A
];
171 % vérification des données JSON
172 Chat_order
= list_to_atom(Chat_order_str
),
173 Chat_order_valide
= lists:any(fun(E
) -> E
=:= Chat_order
end, [reverse
, chrono
]),
174 if not Chat_order_valide
->
175 {erreur
, Chat_order_str
++ " n'est pas une valeur acceptée pour 'chat_order'"};
177 Nick_format
= list_to_atom(Nick_format_str
),
178 Nick_format_valide
= lists:any(fun(E
) -> E
=:= Nick_format
end, [nick
, login
, nick_login
]),
179 if not Nick_format_valide
->
180 {erreur
, Nick_format_str
++ " n'est pas une valeur acceptée pour 'nick_format'"};
182 Ostentatious_master
= list_to_atom(Ostentatious_master_str
),
183 Ostentatious_master_valide
= lists:any(fun(E
) -> E
=:= Ostentatious_master
end, [invisible
, light
, heavy
]),
184 if not Ostentatious_master_valide
->
185 {erreur
, Ostentatious_master_str
++ " n'est pas une valeur acceptée pour 'ostentatious_master'"};
191 chat_order
= Chat_order
,
192 nick_format
= Nick_format
,
193 view_times
= View_times
,
194 view_tooltips
= View_tooltips
,
195 conversations
= Conversations
,
196 ostentatious_master
= Ostentatious_master
203 % Renvoie les messages appropriés.
204 % last_message id et cookie sont facultatifs
205 wait_event([{page
, "chat"} | Data
]) ->
206 % traitement des inputs
207 Cookie
= case lists:keysearch(cookie
, 1, Data
) of {value
, {_
, C
}} -> C
; _
-> inconnu
end,
208 Last_message_id
= case lists:keysearch(last_message_id
, 1, Data
) of {value
, {_
, Id
}} -> Id
; _
-> 0 end,
209 {value
, {_
, Message_count
}} = lists:keysearch(message_count
, 1, Data
),
210 Main_page
= case lists:keysearch(main_page
, 1, Data
) of {value
, {_
, P
}} -> P
; _
-> 1 end,
211 Troll_id
= case lists:keysearch(troll_id
, 1, Data
) of {value
, {_
, T
}} -> T
; _
-> 0 end,
212 {value
, {_
, {array
, Conversations_json
}}} = lists:keysearch(conversations
, 1, Data
),
213 Racines_conversations
= lists:map(
214 fun({struct
, [{root
, Racine
}, {page
, Page
} | Reste
]}) ->
215 Last_mess_conv
= case Reste
of [{last_message_id
, L
}] -> L
; _
-> 0 end,
216 {Racine
, Page
, Last_mess_conv
}
220 User
= case euphorik_bd:user_by_cookie(Cookie
) of
224 case {mnesia:subscribe({table
, minichat
, detailed
}), mnesia:subscribe({table
, troll
, detailed
})} of
225 {{error
, E
}, _
} -> E
;
226 {_
, {error
, E
}} -> E
;
228 % attente d'événements
229 R
= wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
, Troll_id
),
230 mnesia:unsubscribe({table
, minichat
, detailed
}),
231 mnesia:unsubscribe({table
, troll
, detailed
}),
234 wait_event([{page
, "admin"}, {last_troll
, Last_troll
}]) ->
235 case wait_event_page_admin(Last_troll
) of
236 banned_ips_refresh
->
239 {reply
, "banned_ips_refresh"}
245 {reply
, "troll_modified"},
246 {troll_id
, Troll#troll
.id
},
247 {content
, Troll#troll
.content
}
253 {reply
, "troll_added"},
257 {ok
, User
} = euphorik_bd:user_by_id(T#troll
.id_user
),
260 {troll_id
, T#troll
.id
},
261 {content
, T#troll
.content
},
262 {author
, User#user
.pseudo
},
263 {author_id
, User#user
.id
}
275 {reply
, "troll_deleted"},
283 erreur("Page inconnue").
286 wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
, Troll_id
) ->
287 % est-ce que le troll est à jour ?
288 case euphorik_bd:current_troll() of
289 Current
when is_record(Current
, troll
), Current#troll
.id
=/= Troll_id
->
291 {reply
, "new_troll"},
292 {troll_id
, Current#troll
.id
},
293 {message_id
, euphorik_bd:message_id_associe(Current#troll
.id
)},
294 {content
, Current#troll
.content
}
297 % est-ce qu'il y a des nouveaux messages ?
298 case euphorik_minichat_conversation:conversations(Racines_conversations
, Message_count
, Last_message_id
, Main_page
) of
300 wait_event_bd_page_chat(),
301 wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
, Troll_id
);
303 % accrochez-vous ca va siouxer ;)
305 {reply
, "new_messages"},
306 {conversations
, {array
,
308 fun({Racine
, {Conv
, Plus
}}) ->
310 {last_page
, not Plus
},
311 {first
, % le premier message de la conversation
312 if Racine
=:= undefined orelse Conv
=:= [] ->
315 {Racine_id
, _
, _
} = Racine
,
316 case euphorik_bd:message_by_id(Racine_id
) of
318 json_message(Mess
, euphorik_bd:parents(Racine
), User
);
326 fun({Mess
, Repond_a
}) ->
327 json_message(Mess
, Repond_a
, User
)
334 % on ajoute un 'undefined' correspondant à la premier conversation qui ne possède pas de racine
335 % TODO : peut être à revoir car un peu lourd est compliqué
336 aggregation_racines_conversations([undefined
| Racines_conversations
], Conversations
)
344 aggregation_racines_conversations(L1
, L2
) ->
345 aggregation_racines_conversations(L1
, L2
, []).
346 aggregation_racines_conversations([], [], L
) -> lists:reverse(L
);
347 aggregation_racines_conversations([E1
|R1
], [E2
|R2
], L
) ->
348 aggregation_racines_conversations(R1
, R2
, [{E1
, E2
} | L
]).
352 % Attend un événement lié à la page 'chat'.
353 wait_event_bd_page_chat() ->
354 receive % attente d'un post
355 {mnesia_table_event
, {write
, minichat
, _Message
, [], _
}} ->
357 {mnesia_table_event
, {write
, troll
, Troll
, [Old_troll
| _
], _
}} when Troll#troll
.date_post
=/= undefined
, Old_troll#troll
.date_post
== undefined
->
362 wait_event_bd_page_chat()
363 % 60 minutes de timeout (on ne sais jamais)
364 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
365 after 1000 * 60 * 60 ->
370 % Attent un événement concernant la page admin
371 % Renvoie les trolls manquants posté après Last_id ou banned_ips_refresh.
372 % Si pas de trolls alors attend un événement tel qu'un ajout, une modification ou une suppression.
377 % ou banned_ips_refresh
379 wait_event_page_admin(Last_id
) ->
380 case {mnesia:subscribe({table
, troll
, detailed
}), mnesia:subscribe({table
, ip_table
, detailed
})} of
381 {{error
, E
}, _
} -> E
;
382 {_
, {error
, E
}} -> E
;
384 R
= case euphorik_bd:trolls(Last_id
) of
385 [] -> % pas de trolls
386 wait_event_page_admin();
390 mnesia:unsubscribe({table
, troll
, detailed
}),
391 mnesia:unsubscribe({table
, ip_table
, detailed
}),
395 wait_event_page_admin() ->
396 % s'il n'y a pas de trolls que l'utilisateur n'a pas connaissance alors on attend un événement
398 % cas où un troll est choisit comme courant
399 {mnesia_table_event
, {write
, troll
, Troll
, [Old_troll
| _
], _
}}
400 when Old_troll#troll
.date_post
=:= undefined
, Troll#troll
.date_post
=/= undefined
->
401 {del
, Troll#troll
.id
};
402 {mnesia_table_event
, {write
, troll
, Troll
, [_Old_troll
| _
], _
}} ->
404 {mnesia_table_event
, {write
, troll
, Troll
, [], _
}} ->
406 {mnesia_table_event
, {delete
, troll
, {troll
, Id
}, _
, _
}} ->
408 {mnesia_table_event
, {write
, ip_table
, IP
, [Old_IP
| _
], _
}}
409 when Old_IP#ip_table
.ban
=/= IP#ip_table
.ban
; Old_IP#ip_table
.ban_duration
=/= IP#ip_table
.ban_duration
->
414 wait_event_page_admin()
415 % 60 minutes de timeout (on ne sais jamais)
416 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
417 after 1000 * 60 * 60 ->
422 % Un utilisateur envoie un message
423 % Answer_to est une liste d'id (int)
429 {answer_to
, {array
, Answer_to
}}
432 case euphorik_bd:user_by_cookie(Cookie
) of
434 case euphorik_bd:est_banni(User#user
.id
) of
435 {true
, Temps_restant
} ->
436 erreur("Vous êtes banni pour encore " ++ format_minutes(Temps_restant
));
438 Strip_content
= string:strip(Content
),
439 if Strip_content
=:= [] ->
440 erreur("Message vide");
442 % attention : non-atomique (update_pseudo+nouveau_message)
443 euphorik_bd:update_pseudo_user(User#user
.id
, Nick
),
444 case euphorik_bd:nouveau_message(Strip_content
, User#user
.id
, Answer_to
) of
445 {erreur
, R
} -> erreur("Impossible d'ajouter un nouveau message. Raison : " ++ R
);
452 erreur("Utilisateur inconnu")
456 % bannissement d'un utilisateur (son ip est bannie)
460 {duration
, Duration
},
464 % controle que l'utilisateur est un admin
465 case euphorik_bd:user_by_cookie(Cookie
) of
466 {ok
, User1
= #user
{ek_master
= true
}} ->
467 case euphorik_bd:user_by_id(User_id
) of
469 erreur("Il n'est pas possible de s'auto bannir");
470 {ok
, User2
= #user
{ek_master
= false
}} ->
471 euphorik_bd:ban(User2#user
.last_ip
, Duration
),
472 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("''~s~s'' est ~s pour ~s.~s",
475 if User2#user
.login
=:= [] -> ""; true
-> " (" ++ User2#user
.login
++ ")" end,
476 if Duration
=< 15 -> "kické"; true
-> "banni" end,
477 format_minutes(Duration
),
478 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end ++ "."
483 erreur("L'utilisateur est lui même un ekMaster");
485 erreur("Utilisateur à bannir inconnu")
488 erreur("Utilisateur inconnu ou non ek master")
492 % slapage d'un user (avertissement)
499 % controle que l'utilisateur est un admin
500 case euphorik_bd:user_by_cookie(Cookie
) of
501 {ok
, User1
= #user
{ek_master
= true
}} ->
502 case euphorik_bd:user_by_id(User_id
) of
504 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s s'auto slap~s.",
507 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end
511 {ok
, User2
= #user
{ek_master
= false
}} ->
512 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s se fait slaper par ~s.~s",
516 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end ++ "."
521 erreur("L'utilisateur est lui même un ekMaster");
523 erreur("Utilisateur à slaper inconnu")
526 erreur("Utilisateur inconnu ou non ek master")
536 % controle que l'utilisateur est un admin
537 case euphorik_bd:user_by_cookie(Cookie
) of
538 {ok
, User
= #user
{ek_master
= true
}} ->
539 case euphorik_bd:put_troll(User#user
.id
, Content
) of
540 max_troll_reached_per_user
->
541 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum par utilisateur est atteint : ~w ", [?NB_MAX_TROLL_WAITING_BY_USER
])));
543 erreur(lists:flatten(io_lib:format("Le nombre de troll maximum en attente est atteint : ~w ", [?NB_MAX_TROLL_WAITING
])));
548 erreur("Seul les ekMaster peuvent proposer des trolls")
555 {troll_id
, Troll_id
},
559 % controle que l'utilisateur est un admin
560 case euphorik_bd:user_by_cookie(Cookie
) of
561 {ok
, User
= #user
{ek_master
= true
}} ->
562 User_id
= User#user
.id
,
563 case euphorik_bd:troll_by_id(Troll_id
) of
564 {ok
, #troll
{id_user
= User_id
}} ->
565 euphorik_bd:mod_troll(Troll_id
, Content
),
568 erreur("Vous ne posséder pas ce troll")
571 erreur("Seul les ekMaster peuvent proposer des trolls")
581 % controle que l'utilisateur est un admin
582 case euphorik_bd:user_by_cookie(Cookie
) of
583 {ok
, User
= #user
{ek_master
= true
}} ->
584 User_id
= User#user
.id
,
585 case euphorik_bd:troll_by_id(Troll_id
) of
586 {ok
, #troll
{id_user
= User_id
}} ->
587 euphorik_bd:del_troll(Troll_id
),
590 erreur("Vous ne posséder pas ce troll")
593 erreur("Seul les ekMaster peuvent proposer des trolls")
603 case euphorik_bd:user_by_cookie(Cookie
) of
604 {ok
, #user
{ek_master
= true
}} ->
605 euphorik_bd:deban(euphorik_common:unserialize_ip(IP
)),
608 erreur("Seul les ekMaster peuvent connaitre la liste des ips bannies")
617 case euphorik_bd:user_by_cookie(Cookie
) of
618 {ok
, #user
{ek_master
= true
}} ->
622 {reply
, "list_banned_ips"},
623 {list, {array
, lists:map(
624 fun({IP
, T
, Users
}) ->
627 {ip
, euphorik_common:serialize_ip(IP
)},
628 {remaining_time
, format_minutes(T
)},
629 {users
, {array
, lists:map(
630 fun({Pseudo
, Login
}) ->
643 euphorik_bd:list_ban()
648 erreur("Seul les ekMaster peuvent connaitre la liste des ips bannies")
652 % Construit une erreur
657 {error_message
, Message
}
662 % Formatage de minutes.
663 % par exemple : "1min", "45min", "1h23min", "1jour 2h34min"
664 format_minutes(Min
) ->
665 Jours
= Min
div (60 * 24),
666 Heures
= Min
rem (60 * 24) div
60,
667 Minutes
= Min
rem (60),
668 if Jours
=/= 0 -> integer_to_list(Jours
) ++ " Jour" ++ if Jours
> 1 -> "s"; true
-> "" end ++ " "; true
-> "" end ++
669 if Heures
=/= 0 -> integer_to_list(Heures
) ++ " heure" ++ if Heures
> 1 -> "s"; true
-> "" end; true
-> "" end ++
673 " " ++ integer_to_list(Minutes
) ++ " minute" ++ if Minutes
> 1 -> "s"; true
-> "" end
677 % Formatage d'une heure
678 % local_time() -> string
680 DateLocal
= calendar:now_to_local_time(Date
),
681 DateNowLocal
= calendar:local_time(),
682 {{Annee
, Mois
, Jour
}, {Heure
, Minute
, Seconde
}} = DateLocal
,
683 {{AnneeNow
, _
, _
}, {_
, _
, _
}} = DateNowLocal
,
684 Hier
= calendar:date_to_gregorian_days(element(1, DateLocal
)) =:= calendar:date_to_gregorian_days(element(1, DateNowLocal
)) - 1,
686 if element(1, DateLocal
) =:= element(1, DateNowLocal
) ->
690 Annee
=:= AnneeNow
->
691 io_lib:format("~2.10.0B/~2.10.0B ", [Jour
, Mois
]);
693 io_lib:format("~2.10.0B/~2.10.0B/~B ", [Jour
, Mois
, Annee
])
695 io_lib:format("~2.10.0B:~2.10.0B:~2.10.0B", [Heure
, Minute
, Seconde
])
700 {struct
, [{reply
, "ok"}]}.
703 json_reponse_login_ok(User
) ->
707 {status
, if (User#user
.password
=/= []) and (User#user
.login
=/= []) -> "auth_registered"; true
-> "auth_not_registered" end},
708 {cookie
, User#user
.cookie
},
710 {nick
, User#user
.pseudo
},
711 {login
, User#user
.login
},
712 {email
, User#user
.email
},
713 {css
, User#user
.css
},
714 {chat_order
, atom_to_list(User#user
.chat_order
)},
715 {nick_format
, atom_to_list(User#user
.nick_format
)},
716 {view_times
, User#user
.view_times
},
717 {view_tooltips
, User#user
.view_tooltips
},
718 {conversations
, {array
, lists:map(
719 fun({Racine
, Reduit
}) ->
720 {struct
, [{root
, Racine
}, {minimized
, Reduit
}]}
722 User#user
.conversations
724 {ek_master
, User#user
.ek_master
},
725 {ostentatious_master
, atom_to_list(User#user
.ostentatious_master
)}
729 % Renvoie le message formaté en JSON.
730 % Mess est de type #minichat
731 % Repond_a est une liste d'id des messages auquel répond Mess
732 % User est l'utilisateur courant de type #user
733 json_message(Mess
, Repond_a
, User
) ->
734 Est_proprietaire
= User
=/= inconnu andalso User#user
.id
=:= Mess#minichat
.auteur_id
,
735 A_repondu_a_message
= User
=/= inconnu andalso
euphorik_bd:a_repondu_a_message(User#user
.id
, Mess#minichat
.id
),
736 Est_une_reponse_a_user
= User
=/= inconnu andalso
euphorik_bd:est_une_reponse_a_user(User#user
.id
, Mess#minichat
.id
),
737 {ok
, User_mess
} = euphorik_bd:user_by_id(Mess#minichat
.auteur_id
),
739 {id
, Mess#minichat
.id
},
740 {user_id
, User_mess#user
.id
},
741 {date, format_date(Mess#minichat
.date)},
742 {system
, Mess#minichat
.auteur_id
=:= 0},
743 {owner
, Est_proprietaire
},
744 {answered
, A_repondu_a_message
},
745 {is_a_reply
, Est_une_reponse_a_user
},
746 {nick
, Mess#minichat
.pseudo
},
747 {login
, User_mess#user
.login
},
748 {content
, Mess#minichat
.contenu
},
749 {root
, Mess#minichat
.racine_id
},
750 {answer_to
, {array
, lists:map(
752 {ok
, M
} = euphorik_bd:message_by_id(Id_mess
),
753 {ok
, User_reponse
} = euphorik_bd:user_by_mess(M#minichat
.id
),
754 {struct
, [{id
, M#minichat
.id
}, {nick
, M#minichat
.pseudo
}, {login
, User_reponse#user
.login
}]}
758 {ek_master
, User_mess#user
.ek_master
},
759 {ostentatious_master
, atom_to_list(User_mess#user
.ostentatious_master
)}