Сегодня мы напишем детектор троллей для нашего форума. Почему в decl? Потому что на эрланге.
Обнаруживать троллей мы будем простым и эффективгым способом: будем строить граф интересующей нас ветки. Вершинами графа будут юзеры, а ребра (соответствующего веса и толщины) будут связывать тех юзеров, которые что-то друг другу писали. По такому графу нетрудно догадаться, кто есть кто в ветке.
Для вытягивания тем мы воспользуемся веб-сервисом rsdn (
http://rsdn.ru/ws/janusAT.asmx). К сожалению, в wsdl сервиса (
http://rsdn.ru/ws/janusAT.asmx?WSDL) значится левый порт (8888), поэтому wsdl мы сохраним на диск и уберем из него ошибочный порт, вот так:
http://gist.github.com/145674.
В качестве soap-клиента будем использовать оный из yaws, про который можно прочитать пару строк здесь:
http://yaws.hyber.org/soap_intro.yaws. Таким образом, из эрланговых либ нам понадобятся:
1. yaws
2. erlsom
3. ibrowse (к сожалению, веб-сервис не желает работать, когда в качестве http-клиента используется оный из http).
Из тулзов:
1. graphviz
2. imagemagick
Первым делом, сгенерируем hrl со структурами для веб-сервиса:
http://gist.github.com/145677.
Далее напишем функцию, которая достанет все сообщения ветки по id этой самой ветки (вообще, сервис вроде бы достает ветку по id сообщения в ней, но id ветки совпадает с id первого сообщения в ней):
get_topic(Topic) ->
ibrowse:start(),
JanusWsdl = yaws_soap_lib:initModel("janusAT.wsdl"),
{ok, _,
[#'p:GetTopicByMessageResponse'{
'GetTopicByMessageResult'=
#'p:TopicResponse' {
'Messages'=
#'p:ArrayOfJanusMessageInfo'{'JanusMessageInfo' = Messages}}}]} =
yaws_soap_lib:call(JanusWsdl, "GetTopicByMessage",
[#'p:TopicRequest'{ userName = "username",
password = "password",
messageIds = #'p:ArrayOfInt'{int = [Topic]}}]),
Messages.
Эта штука возвращает список структур 'p:JanusMessageInfo'. Далее, пользуясь тем, что веб-сервис отдает сообшения отсортированными по времени, посчитаем количество ответов, которые пользователи отправляли друг другу.
links(Messages)->
links(Messages, dict:new(), dict:new()).
links([Message|Tail], UserDict, LinkDict) ->
MessageId = Message#'p:JanusMessageInfo'.messageId,
MessageNick = Message#'p:JanusMessageInfo'.userNick,
NewUserDict = dict:store(MessageId, MessageNick, UserDict),
ParentId = Message#'p:JanusMessageInfo'.parentId,
case ParentId of
0 -> links(Tail, NewUserDict, LinkDict);
_ -> ParentNick = dict:fetch(ParentId, UserDict),
links(Tail, NewUserDict,
dict:update_counter(case MessageNick < ParentNick of
true -> {MessageNick, ParentNick};
false -> {ParentNick, MessageNick}
end, 1, LinkDict))
end;
links([], _, LinkDict) ->
dict:to_list(LinkDict).
Эта функция для некоторой гипотетической ветки возвращает список такого вот рода туплов:
[{{"DarkGray","eao197"},3},
{{"Ikemefula","thesz"},2},
{{"VGn","thesz"},4},
{{"jazzer","lomeo"},1},
{{"VladD2","thesz"},2},
{{"DarkGray","thesz"},4},
{{"StevenIvanov","thesz"},2},
{{"Calabon","thesz"},1},
{{"Ikemefula","eao197"},2},
{{"eao197","thesz"},1},
{{"Ikemefula","VladD2"},1},
{{"jazzer","thesz"},10}]
Попрошу заметить, что все имена вымышленные и любые их совпадения с никами реальных людей случайны.

Теперь этот список преобразуем в граф для graphviz:
links2dot(Links) ->
links2dot(Links, "}\n").
links2dot([{{Nick1, Nick2}, Weight}|Tail], Acc) ->
links2dot(Tail, [io_lib:format("\"~s\" -- \"~s\" [weight=~B, label=~B, style=\"setlinewidth(~B)\"];\n", [Nick1, Nick2, Weight, Weight, linewidth(Weight)]) | Acc]);
links2dot([], Acc) ->
lists:flatten(["graph Links {\noverlap=false;\n" | Acc]).
linewidth(Weight) ->
if
Weight < 3 -> 1;
Weight < 10 -> 2;
Weight < 50 -> 4;
true -> 8
end.
Этот граф остается записать в файл и сконвертить в картинку. Для выявления троллей, на мой взгляд, больше всего подходит neato. Для нашей гипотетической ветки мы получим вот такой граф:

Итак, наиболее вероятно, что в данной ветке троллем является thesz. Другие участники дискуссии практически не общаются друг с другом — только лишь с thesz-ом. Также заметно, что основным оппонентом thesz-а является jazzer. Но поскольку ему (кроме thesz-а) практически никто не отвечает — можно предположить, что jazzer представляет мнение большинства, тогда как thesz играет роль провокатора.
В качестве последнего штриха сделаем вот что. Будем строить несколько графов, представляющих состояния ветки в разные моменты времени. А потом сделаем gif.
link_progress(Topic) ->
link_progress([links2dot(links(M)) || M <- heads(get_topic(Topic))], 0).
link_progress([Dot|Tail], N) ->
file:write_file(lists:flatten(io_lib:format("~4.10.0B.dot", [N])), Dot),
link_progress(Tail, N+1);
link_progress([], _) ->
ok.
heads(List)->
heads([], List).
heads([], [Head|Tail]) ->
heads([[Head]], Tail);
heads([List|_]=Acc, [Head|Tail]) ->
heads([[Head|List]|Acc], Tail);
heads(Acc, []) ->
lists:reverse(lists:map(fun lists:reverse/1, Acc)).
Функция link_progress наполнит текущий каталог кучкой .dot файлов, которые мы сперва преобразуем в картинки:
for dot in *.dot; do neato -Tpng -o${dot}.png $dot; done
Потом первую (пустую) картинку заресайзим до размера, который должен быть у гифа. Вообще, это надо делать автоматически, а не вручную, но мне влом.
сonvert 0000.dot.png -resize 775x375\! 0000.dot.png
И теперь сделаем гиф из картинок с графами:
convert -delay 100 -loop 0 -dispose previous -dispose background *.png thread.gif
И получится у нас вот такая штука:

Такие дела. Как видите, мы написали очень полезную тулзу: она поможет модераторам более эффективно раздавать предупреждения, банить пользователей и применять другие меры наказания. Достаточно одного взгляда на граф "горячей" ветки, чтобы понять кто в ней троллит — и принять соответствующие меры. Ура, товарищи. Банхаммер, вперед!