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
).
37 -include("../include/euphorik_bd.hrl").
38 -include("../include/euphorik_defines.hrl").
41 % Une utilisateur s'enregistre avec un tuple {Login, Password}.
42 register([{login
, Login
}, {password
, Password
}, {profile
, Profile_json
}], IP
) ->
43 Can_register
= euphorik_bd:can_register(IP
),
45 case euphorik_bd:user_by_login(Login
) of
49 User
= euphorik_bd:nouveau_user(Login
, Password
, generer_cookie(), profile_from_json(Profile_json
)),
50 euphorik_bd:update_ip(User#user
.id
, IP
),
51 json_reponse_login_ok(User
)
54 erreur_register_flood()
56 % Enregistrement sans {Login, Password}
57 register([{profile
, Profile_json
}], IP
) ->
58 Can_register
= euphorik_bd:can_register(IP
),
60 Profile
= profile_from_json(Profile_json
),
61 User
= euphorik_bd:nouveau_user(generer_cookie(), Profile#profile
{pseudo
= "<nick>"}),
62 euphorik_bd:update_ip(User#user
.id
, IP
),
63 json_reponse_login_ok(User
);
65 erreur_register_flood()
68 erreur_register_flood() ->
72 % Un utilisateur se logge (avec un couple {login, mot de passe})
73 login([{login
, Login
}, {password
, Password
}], IP
) ->
74 case euphorik_bd:user_by_login_password(Login
, Password
) of
78 timer:sleep(?TEMPS_ATTENTE_ERREUR_LOGIN
),
81 % Un utilisateur se logge (avec un cookie)
82 login([{cookie
, Cookie
}], IP
) ->
83 case euphorik_bd:user_by_cookie(Cookie
) of
87 timer:sleep(?TEMPS_ATTENTE_ERREUR_LOGIN
),
92 % L'utilisateur donné se logge avec l'ip donnée.
93 loginUser(User
, IP
) ->
94 euphorik_bd:update_ip(User#user
.id
, IP
),
95 euphorik_bd:update_date_derniere_connexion(User#user
.id
),
96 json_reponse_login_ok(User
).
99 % Renvoie un string() représentant un cookie en base 36. Il y a 10^32 possibillités.
101 {A1
, A2
, A3
} = erlang:timestamp(),
102 random:seed(A1
, A2
, A3
),
103 erlang:integer_to_list(random:uniform(trunc(math:pow(10, 32))), 36).
106 % Modification du profile.
111 {password
, Password
},
112 {profile
, Profile_json
}
115 case profile_from_json(Profile_json
) of
118 case euphorik_bd:set_profile(Cookie
, Login
, Password
, Profile
) of
129 % Construit un #user à partir des données JSON
136 {chat_order
, Chat_order_str
},
137 {nick_format
, Nick_format_str
},
138 {view_times
, View_times
},
139 {view_tooltips
, View_tooltips
},
140 {conversations
, {array
, Conversations_json
}},
141 {ostentatious_master
, Ostentatious_master_str
}
145 % décomposition de la strucure JSON
146 Conversations
= lists:foldr(
147 fun({struct
, [{root
, Racine
}, {minimized
, Reduit
}]}, A
) ->
148 % virage des messages qui n'existent pas
149 Message_exite
= euphorik_bd:message_existe(Racine
),
151 [ {Racine
, Reduit
} | A
];
159 % vérification des données JSON
160 Chat_order
= list_to_atom(Chat_order_str
),
161 Chat_order_valide
= lists:any(fun(E
) -> E
=:= Chat_order
end, [reverse
, chrono
]),
162 if not Chat_order_valide
->
163 {erreur
, Chat_order_str
++ " n'est pas une valeur acceptée pour 'chat_order'"};
165 Nick_format
= list_to_atom(Nick_format_str
),
166 Nick_format_valide
= lists:any(fun(E
) -> E
=:= Nick_format
end, [nick
, login
, nick_login
]),
167 if not Nick_format_valide
->
168 {erreur
, Nick_format_str
++ " n'est pas une valeur acceptée pour 'nick_format'"};
170 Ostentatious_master
= list_to_atom(Ostentatious_master_str
),
171 Ostentatious_master_valide
= lists:any(fun(E
) -> E
=:= Ostentatious_master
end, [invisible
, light
, heavy
]),
172 if not Ostentatious_master_valide
->
173 {erreur
, Ostentatious_master_str
++ " n'est pas une valeur acceptée pour 'ostentatious_master'"};
179 chat_order
= Chat_order
,
180 nick_format
= Nick_format
,
181 view_times
= View_times
,
182 view_tooltips
= View_tooltips
,
183 conversations
= Conversations
,
184 ostentatious_master
= Ostentatious_master
191 % Attend un événement pour la page "Chat"
192 % last_message id et cookie sont facultatifs
193 wait_event([{page
, "chat"} | Data
]) ->
194 % traitement des inputs
195 Cookie
= case lists:keysearch(cookie
, 1, Data
) of {value
, {_
, C
}} -> C
; _
-> inconnu
end,
196 Last_message_id
= case lists:keysearch(last_message_id
, 1, Data
) of {value
, {_
, Id
}} -> Id
; _
-> 0 end,
197 {value
, {_
, Message_count
}} = lists:keysearch(message_count
, 1, Data
),
198 Main_page
= case lists:keysearch(main_page
, 1, Data
) of {value
, {_
, P
}} -> P
; _
-> 1 end,
199 {value
, {_
, {array
, Conversations_json
}}} = lists:keysearch(conversations
, 1, Data
),
200 Racines_conversations
= lists:map(
201 fun({struct
, [{root
, Racine
}, {page
, Page
} | Reste
]}) ->
202 Last_mess_conv
= case Reste
of [{last_message_id
, L
}] -> L
; _
-> 0 end,
203 {Racine
, Page
, Last_mess_conv
}
207 User
= case euphorik_bd:user_by_cookie(Cookie
) of
211 case mnesia:subscribe({table
, minichat
, detailed
}) of
214 % attente d'événements
215 R
= wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
),
216 mnesia:unsubscribe({table
, minichat
, detailed
}),
219 % Attend un événement pour la page "Admin"
220 wait_event([{page
, "admin"}]) ->
221 case wait_event_page_admin() of
222 banned_ips_refresh
->
225 {reply
, "banned_ips_refresh"}
235 % Attend un événement pour la page "Chat" et renvoie les messages manquants au client.
236 wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
) ->
237 % est-ce qu'il y a des nouveaux messages ?
238 case euphorik_minichat_conversation:conversations(Racines_conversations
, Message_count
, Last_message_id
, Main_page
) of
240 wait_event_bd_page_chat(),
241 wait_event_page_chat(User
, Racines_conversations
, Message_count
, Last_message_id
, Main_page
);
243 % Accrochez-vous ca va siouxer ;).
245 {reply
, "new_messages"},
246 {conversations
, {array
,
248 fun({Racine
, {Conv
, Plus
}}) ->
250 {last_page
, not Plus
},
251 {first
, % le premier message de la conversation
252 if Racine
=:= undefined orelse Conv
=:= [] ->
255 {Racine_id
, _
, _
} = Racine
,
256 case euphorik_bd:message_by_id(Racine_id
) of
258 json_message(Mess
, euphorik_bd:parents_id(Racine_id
), User
);
266 fun({Mess
, Repond_a
}) ->
267 json_message(Mess
, Repond_a
, User
)
274 % on ajoute un 'undefined' correspondant à la premier conversation qui ne possède pas de racine
275 % TODO : peut être à revoir car un peu lourd est compliqué
276 aggregation_racines_conversations([undefined
| Racines_conversations
], Conversations
)
283 aggregation_racines_conversations(L1
, L2
) ->
284 aggregation_racines_conversations(L1
, L2
, []).
285 aggregation_racines_conversations([], [], L
) -> lists:reverse(L
);
286 aggregation_racines_conversations([E1
|R1
], [E2
|R2
], L
) ->
287 aggregation_racines_conversations(R1
, R2
, [{E1
, E2
} | L
]).
291 % Attend un événement lié à la page 'chat'.
292 wait_event_bd_page_chat() ->
293 receive % attente d'un post
294 {mnesia_table_event
, {write
, minichat
, _Message
, [], _
}} ->
299 wait_event_bd_page_chat()
300 % 60 minutes de timeout (on ne sais jamais)
301 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
302 after 1000 * 60 * 60 ->
307 % Attent un événement concernant la page admin
310 wait_event_page_admin() ->
311 case mnesia:subscribe({table
, ip_table
, detailed
}) of
315 {mnesia_table_event
, {write
, ip_table
, IP
, [Old_IP
| _
], _
}}
316 when Old_IP#ip_table
.ban
=/= IP#ip_table
.ban
; Old_IP#ip_table
.ban_duration
=/= IP#ip_table
.ban_duration
->
321 wait_event_page_admin()
322 % 60 minutes de timeout (on ne sais jamais)
323 % Après 60 minutes de connexion, le client doit donc reétablir une connexion
324 after 1000 * 60 * 60 ->
327 mnesia:unsubscribe({table
, ip_table
, detailed
}),
332 % Un utilisateur envoie un message
333 % Answer_to est une liste d'id (int)
339 {answer_to
, {array
, Answer_to
}}
342 case euphorik_bd:user_by_cookie(Cookie
) of
344 case euphorik_bd:est_banni(User#user
.id
) of
345 {true
, Temps_restant
} ->
346 erreur(80, [format_minutes(Temps_restant
)]);
348 Strip_content
= string:strip(Content
),
349 if Strip_content
=:= [] ->
352 % attention : non-atomique (update_pseudo+nouveau_message)
353 euphorik_bd:update_pseudo_user(User#user
.id
, Nick
),
354 case euphorik_bd:nouveau_message(Strip_content
, User#user
.id
, Answer_to
) of
355 {erreur
, R
} -> erreur(100, [R
]);
366 % bannissement d'un utilisateur (son ip est bannie)
370 {duration
, Duration
},
374 % controle que l'utilisateur est un admin
375 case euphorik_bd:user_by_cookie(Cookie
) of
376 {ok
, User1
= #user
{ek_master
= true
}} ->
377 case euphorik_bd:user_by_id(User_id
) of
380 {ok
, #user
{ek_master
= false
, profile
= Profile2
} = User2
} ->
381 euphorik_bd:ban(User2#user
.last_ip
, Duration
),
382 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("\"~s~s\" est ~s pour ~s.~s",
384 Profile2#profile
.pseudo
,
385 if User2#user
.login
=:= [] -> ""; true
-> " (" ++ User2#user
.login
++ ")" end,
386 if Duration
=< 15 -> "kické"; true
-> "banni" end,
387 format_minutes(Duration
),
388 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end ++ "."
402 % slapage d'un user (avertissement)
409 % controle que l'utilisateur est un admin
410 case euphorik_bd:user_by_cookie(Cookie
) of
411 {ok
, User1
= #user
{ek_master
= true
, profile
= Profile1
}} ->
412 case euphorik_bd:user_by_id(User_id
) of
414 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s s'auto slap~s.",
416 Profile1#profile
.pseudo
,
417 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end
421 {ok
, #user
{ek_master
= false
, profile
= Profile2
}} ->
422 euphorik_bd:nouveau_message_sys(lists:flatten(io_lib:format("~s se fait slaper par ~s.~s",
424 Profile2#profile
.pseudo
,
425 Profile1#profile
.pseudo
,
426 if Reason
=/= [] -> " - Raison: " ++ Reason
; true
-> "" end ++ "."
446 case euphorik_bd:user_by_cookie(Cookie
) of
447 {ok
, #user
{ek_master
= true
}} ->
448 euphorik_bd:deban(euphorik_common:unserialize_ip(IP
)),
460 case euphorik_bd:user_by_cookie(Cookie
) of
461 {ok
, #user
{ek_master
= true
}} ->
465 {reply
, "list_banned_ips"},
466 {list, {array
, lists:map(
467 fun({IP
, T
, Users
}) ->
470 {ip
, euphorik_common:serialize_ip(IP
)},
471 {remaining_time
, format_minutes(T
)},
472 {users
, {array
, lists:map(
473 fun({Pseudo
, Login
}) ->
486 euphorik_bd:list_ban()
495 % Construit une erreur
497 erreur_json(Num
, lists:flatten(io_lib:format(euphorik_bd:get_texte(Num
), Args
))).
501 erreur_json(Num
, euphorik_bd:get_texte(Num
)).
504 erreur_json(Num
, Mess
) ->
509 {error_message
, Mess
}
514 % Formatage de minutes.
515 % par exemple : "1min", "45min", "1h23min", "1jour 2h34min"
516 format_minutes(Min
) ->
517 Jours
= Min
div (60 * 24),
518 Heures
= Min
rem (60 * 24) div
60,
519 Minutes
= Min
rem (60),
520 if Jours
=/= 0 -> integer_to_list(Jours
) ++ " Jour" ++ if Jours
> 1 -> "s"; true
-> "" end ++ " "; true
-> "" end ++
521 if Heures
=/= 0 -> integer_to_list(Heures
) ++ " heure" ++ if Heures
> 1 -> "s"; true
-> "" end; true
-> "" end ++
525 " " ++ integer_to_list(Minutes
) ++ " minute" ++ if Minutes
> 1 -> "s"; true
-> "" end
529 % Formatage d'une heure
530 % local_time() -> string
532 DateLocal
= calendar:now_to_local_time(Date
),
533 DateNowLocal
= calendar:local_time(),
534 {{Annee
, Mois
, Jour
}, {Heure
, Minute
, Seconde
}} = DateLocal
,
535 {{AnneeNow
, _
, _
}, {_
, _
, _
}} = DateNowLocal
,
536 Hier
= calendar:date_to_gregorian_days(element(1, DateLocal
)) =:= calendar:date_to_gregorian_days(element(1, DateNowLocal
)) - 1,
538 if element(1, DateLocal
) =:= element(1, DateNowLocal
) ->
542 Annee
=:= AnneeNow
->
543 io_lib:format("~2.10.0B/~2.10.0B ", [Jour
, Mois
]);
545 io_lib:format("~2.10.0B/~2.10.0B/~B ", [Jour
, Mois
, Annee
])
547 io_lib:format("~2.10.0B:~2.10.0B:~2.10.0B", [Heure
, Minute
, Seconde
])
552 {struct
, [{reply
, "ok"}]}.
555 json_reponse_login_ok(#user
{profile
= Profile
} = User
) ->
559 {status
, if (User#user
.password
=/= []) and (User#user
.login
=/= []) -> "auth_registered"; true
-> "auth_not_registered" end},
560 {cookie
, User#user
.cookie
},
562 {login
, User#user
.login
},
563 {ek_master
, User#user
.ek_master
},
566 {nick
, Profile#profile
.pseudo
},
567 {email
, Profile#profile
.email
},
568 {css
, Profile#profile
.css
},
569 {chat_order
, atom_to_list(Profile#profile
.chat_order
)},
570 {nick_format
, atom_to_list(Profile#profile
.nick_format
)},
571 {view_times
, Profile#profile
.view_times
},
572 {view_tooltips
, Profile#profile
.view_tooltips
},
573 {conversations
, {array
, lists:map(
574 fun({Racine
, Reduit
}) ->
575 {struct
, [{root
, Racine
}, {minimized
, Reduit
}]}
577 Profile#profile
.conversations
579 {ostentatious_master
, atom_to_list(Profile#profile
.ostentatious_master
)}
585 % Renvoie le message formaté en JSON.
586 % Mess est de type #minichat
587 % Repond_a est une liste d'id des messages auquel répond Mess
588 % User est l'utilisateur courant de type #user
589 json_message(Mess
, Repond_a
, User
) ->
590 Est_proprietaire
= User
=/= inconnu andalso User#user
.id
=:= Mess#minichat
.auteur_id
,
591 A_repondu_a_message
= User
=/= inconnu andalso
euphorik_bd:a_repondu_a_message(User#user
.id
, Mess#minichat
.id
),
592 Est_une_reponse_a_user
= User
=/= inconnu andalso
euphorik_bd:est_une_reponse_a_user(User#user
.id
, Mess#minichat
.id
),
593 {ok
, #user
{profile
= Profile_mess
} = User_mess
} = euphorik_bd:user_by_id(Mess#minichat
.auteur_id
),
595 {id
, Mess#minichat
.id
},
596 {user_id
, User_mess#user
.id
},
597 {date, case Mess#minichat
.date of undefined
-> "?"; _
-> format_date(Mess#minichat
.date) end},
598 {system
, Mess#minichat
.auteur_id
=:= 0},
599 {owner
, Est_proprietaire
},
600 {answered
, A_repondu_a_message
},
601 {is_a_reply
, Est_une_reponse_a_user
},
602 {nick
, Mess#minichat
.pseudo
},
603 {login
, User_mess#user
.login
},
604 {content
, Mess#minichat
.contenu
},
605 {root
, Mess#minichat
.racine_id
},
606 {answer_to
, {array
, lists:map(
608 {ok
, M
} = euphorik_bd:message_by_id(Id_mess
),
609 {ok
, User_reponse
} = euphorik_bd:user_by_mess(M#minichat
.id
),
610 {struct
, [{id
, M#minichat
.id
}, {nick
, M#minichat
.pseudo
}, {login
, User_reponse#user
.login
}]}
614 {ek_master
, User_mess#user
.ek_master
},
615 {ostentatious_master
, atom_to_list(Profile_mess#profile
.ostentatious_master
)}