3 декабря 2010, 12:55 (5082 дня назад, №8807)Приложение для социальных сетей - SMS Пушка
По работе написал приложение SMS Direct для соцсетей (aka SMS Пушка), которое предоставляет возможность посылать SMS из соцсети на указанный номер, а также получать ответы, просматривать историю входящих и исходящих сообщений и прочее.
За год приложение установило полмиллиона пользователей и теперь его решили развивать в сторону версии для групп, уже другие люди. Пока свежи воспоминания, хочу немного поделиться полученным опытом разработки и работы с основными нашими социальными сетями (ВКонтакте, Мой Мир, Facebook, Одноклассники).
Технологии
Приложение состоит из клиентской части (единой для всех соцсетей), написанной на Flex, и серверной, на PHP + PostgreSQL.
Обмен данными между клиентом и сервером осуществляется в виде бинарных AMF3 пакетов поверх HTTP. Для Flex этот формат (Action Message Format) является родным, а в PHP для распаковки и упаковки данных используется AMFPHP (хотя, возможны варианты - ZendAMF, SabreAMF, WebORB).
При реализации клиента использовался EasyMVC. Хотя сейчас, пожалуй, я бы использовал Cairngorm - можно проще и аккуратнее добавлять новую функциональность.
Все обращения к серверной части выглядят как dispatchEvent(new GetUserInfEvent(Model.GET_USERINF_EVENT_TYPE), params);
Соответственно, в Controller’е это событие ловится и выполняется gateway.call("phpService.getUserInf", new Responder(onResult, onFault), evt).
По факту при этом осуществляется HTTP POST запрос к серверу, просто название метода и параметры вызова упакованы в AMF. На стороне сервера AMFPHP распаковывает всё обратно и на входе в function getUserInf($v) получаем ассоциативный массив с переданными параметрами. В конец метода делаем return $result и происходит обратная процедура - в результате чего, в клиенте в onResult() получаем объект, соответствующий массиву $result.
Серверная часть состоит из двух скриптов:
Первый запускается раз в минуту по крону, просматривает базу на предмет ещё не отправленных сообщений (если такие есть - отправляет) и опрашивает SMS гейт по-поводу новых входящих (если есть, помещает их в базу).
Второй скрипт представляет собой класс с набором методов, доступных снаружи - получение информации о пользователе, отправка сообщения, получение данных записной книжки, изменение, удаление и т.п.
Здесь стоит остановиться на том, как обеспечивается контекст для этих операций (сессия текущего пользователя):
В клиентской части все event’ы наследуются от CommonUserEvent. Поэтому, к примеру, DispatchEvent (.. MarkMessagesInEvent ..) автоматически передаёт user_id / net_id текущего пользователя приложения внутрь MarkMessagesInEvent и далее - в handler_MarkMessagesInEvent, откуда уже происходит gateway.call(), в итоге отсылающий в одном AMF пакете данные специфичные для MarkMessagesInEvent (id помечаемого сообщения) и контекст (user_id / net_id).
На сервере первой строчкой любого метода является вызов UpdateUserInfo(), который достаёт из базы всё необходимую информацию о текущем пользователе по переданным user_id / net_id (если такого пользователя в базе ещё нет, он создаётся).
Интеграция с социальными сетями
Может некоторые из перечисленных социальных сетей чего-то там друг у друга и копировали но, к большому сожалению, это не касается API. Каждый делал так, как ему показалось правильным. Поскольку поддерживать четыре разных приложения не слишком интересное занятие (равно как и создание промежуточного SWF, который подгружал бы в себя основной), было решено сделать приложение универсальным.
По существу, от SWF клиента требовалось не так уж и много - он должен получить от сети id пользователя, его имя и признак установки приложения. id и признак установки получаются автоматически, так как передаются в виде flashvars - никакого API задействовать не надо.
С именем - не так (кстати, весьма жаль, что оно не передаётся также через flashvars - ведь очень многим приложениям трёх перечисленных параметров вполне достаточно - зачем лишний раз дёргать API..)
Итак, первым делом клиент должен определить, в какую социальную сеть он попал. Определяем по параметрам, которые передаются SWF’ке:
if ( (parameters.vid!='') && (parameters.vid!=undefined) )
user_net_id = Model.MAILRU;
else if ( (parameters.api_url!='') && (parameters.api_url!=undefined) )
user_net_id = Model.VKONTAKTE;
else if ( (parameters.fb_sig!='') && (parameters.fb_sig!=undefined) )
user_net_id = Model.FACEBOOK;
else if ( (parameters.sig!='') && (parameters.sig!=undefined) )
user_net_id = Model.ODNOKLASSNIKI;
else
{
user_net_id = Model.NOWHERE;
}//else
Определяем id пользователя (viewer_id) и признак установки приложения на страницу пользователя:
Одноклассники: parameters.logged_user_id, parameters.authorized
Facebook: parameters.fb_sig_user, parameters.fb_sig_added
Мой Мир: parameters.vid, parameters.is_app_user
ВКонтакте: parameters.viewer_id, parameters.is_app_user
Теперь, нужно получить имя пользователя. Как я уже сказал, реализация API везде сильно отличается. Где-то достаточно простого GET запроса, где-то надо сначала установить соединение, дождаться когда оно будет установлено и только потом запрашивать имя (Facebook, Мой Мир).
Раньше из соцсети брался ещё один параметр - timezone. В старой версии приложения время отправки сообщения можно было задавать в формате ЧАСЫ:МИНУТЫ, но от этого пришлось отказаться и теперь используется формат ЧЕРЕЗ СКОЛЬКО ЧАСОВ. Причина в том, что нет возможности достоверно узнать таймзону пользователя. Из социальных сетей её возвращает только ВКонтакт и Facebook, причём в обоих случаях эта информация слишком часто оказывается неверной: неправильно указан город (либо правильно, но для него у них в базе неверная таймзона), неверно учтено летнее/зимнее время для данного города и т.д. Была попытка подходить к вопросу иначе - смотреть разницу во времени между компьютером пользователя и нашим сервером. Однако, после сбора статистики от этого тоже пришлось отказаться - слишком часто Flash возвращает неправильное время/таймзону пользователя. В итоге, было решено указывать через сколько часов отсылать сообщение (считая от момента нажатия кнопки “Отправить”). Кстати, думаю и в ленте Facebook время и дата фигурирует в виде “16 hours ago” не случайно.
Ещё, взависимости от сети, можно использовать некоторые её дополнительные возможности. Так например, во ВКонтакте и в Фейсбуке можно выводить слева, рядом с коротким именем приложения, число новых сообщений. В Моём Мире есть понятие “виджет” - можно выводить на странице пользователя кусок HTML кода (опять же - для показа количества новых сообщений). В случае с ВКонтактом ещё можно получать и показывать их рекламные объявления.
Впечатления от соцсетей
В ходе работы сложилось определённое впечатление о каждой из четырёх соцсетей. Точнее, о тех аспектах, с которыми мне пришлось столкнуться.
ВКонтакте - наиболее устойчивая, с наименьшим количеством глюков. Однако и наименее предсказуемая. Без всяких предупреждений меняются ключевые правила игры - никаких roadmap’ов, уведомлений за несколько месяцев (как, допустим, в Facebook). Прямое взаимодействие между разработчиками и администрацией формально не предусмотрено - всё зависит от везения и случайностей.
API хорошо документировано, разобраться несложно. Много разработчиков, соответственно нетрудно найти ответ на любой вопрос.
При запросах к API ВКонтакта следует учитывать, что их сервера могут регулярно притормаживать с ответом. По моим измерениям, задержка с ответом в 3-4 секунды и более - это регулярное явление. В среднем, такое происходит чаще раза в минуту. Т.е. запросы нужно обрывать по какому-то разумному таймауту. И помнить, что если запросы происходят слишком часто (>3 раз в сек), возвращается “Too many requests”.
Facebook - очень много багов, подчас просто удивительных. Второстепенные не исправляются годами. Однако, существует публичный bugtracker, так что всегда можно посмотреть, является ли твоя личная проблема багом, и знают ли о нём фейсбуковцы. В последнее время они начали с багами бороться, но баги пока что побеждают. Регулярно происходят серьёзные изменения, однако о них заранее предупреждают в roadmap’e.
Возможностей связаться с администрацией ещё меньше, чем в случае с ВКонтактом (не считая bugtracker’a). Однако, опять же, в последнее время они озаботились этой проблемой.
API на недосягаемой высоте. Но документация постоянно находится в устаревшем состоянии. Также много разработчиков, можно найти много примеров.
Важная особенность Facebook’a - лишь в последнее время наблюдается приток туда русскоязычного населения. Да и то, чаще всего это люди, которые общаются с иностранцами или теми, кто живёт и работает за рубежом.
Мой Мир - пока всё весьма сырое и с массой проблем, однако разработчики оперативно реагируют, общаются, пытаются что-то предпринимать. API очень скромное и несколько сумбурное. Документация лаконичная, примеров доступно мало. В целом, на мой взгляд, сеть довольно странная, однако своя аудитория у неё есть - это совершенно однозначно.
Одноклассники - тут сложная ситуация. Закрытая структура - требуют заключения договора. На каждый чих и изменение нужно обращаться к администрации (правда, реагируют очень быстро - для разработчика открывается аккаунт в JIRA и там происходит всё общение). Очень строгие требования к приложениям. До одобрения приложение можно запускать только в совершенно отдельной “песочнице”. Затем, после разрешения, на боевом сайте, но видимым только для конкретных людей. Документация довольно скромная, разработчиков немного, соответственно примеры найти сложно. Багов и недоработок - хватает. Ситуация с API в чём-то напоминает Мой Мир.
Способны передумывать. Хотя наше приложение было полностью оттестировано и готово к запуску, пришлось пока временно приостановить работу с ними.
В итоге, наиболее успешно приложение пошло, конечно, ВКонтакте. Основные рассуждения и графики в этой статье - по ВКонтакту, где набрано более 500 тысяч человек. В Моём Мире - более 10 тысяч. В Facebook’е - незначительное количество, Одноклассники пока заморожены.
Здесь, правда, следует учитывать, что не использовалась агрессивная реклама (типа постингов на стены друзей и пр.) и, как таковой, не было цели набрать как можно больше пользователей за наименьшее время. Наоборот - сначала смотрели на нагрузку и как это всё будет работать.
О структуре базы
Всё достаточно тривиально, я лишь немного остановлюсь на вопросе поддержки несколько соцсетей.
Вводится понятие net_id - идентификтор социальной сети. Таким образом, каждый пользователь идентифицируется парой user_id / net_id. Чтобы не таскать net_id по всем таблицам, пара user_id / net_id существует только в таблице users, где ей соответствует user_id_int (users.id SERIAL). По user_id_int линкуются остальные таблицы - сообщений, контактов и пр.
Немаловажно, что user_id должен иметь тип DECIMAIL (20,0) - никаким UNSIGNED INT не обойтись, из-за гигантских идентификаторов в части соцсетей (планируют расширяться на видимую часть Вселенной ;) В скриптах, однако, с такими идентификаторами можно работать уже только как со строками - никаких float или Number не хватит.
Отмечу особо, что один и тот же человек в разных социальных сетях хранится в базе как два разных пользователя - т.е. двумя разными строчками в таблице users. Нет необходимости связывать их.
Хотя для данного приложения это не является принципиальным, однако если бы понадобилось обеспечить единство пользователя в разных соцсетях и глубокую интеграцию с ними, более правильным стало бы создание нескольких таблиц пользователей - одна общая и по одной на каждую соцсеть. В отдельных таблицах хранились бы данные, уникальные для каждой из сетей (поскольку доступный набор функций в них сильно отличается).
Дизайн и интерфейс
Интерфейс был нарисован сначала в виде наброска в Balsamiq Mockup, а затем реализован в виде mxml + actionscript.
Основано всё на компоненте TabNavigator, который весьма удобен для работы и понятен пользователям. Хотя, нужно оговориться, что во Flex 4 среди Spark компонент его нет - если не хочется мешать Halo и Spark (не советую) то придется что-то искать или изобретать самому.
Из особенностей:
Для того чтобы пользователи не нажимали много раз подряд “Отправить”, кнопка становится неактивной, пока а) не пройдет пара секунд И б) не будет изменен текст сообщения.
Помимо упомянутой фильтрации спецсимволов на стороне сервера, аналогичная фильтрация происходит непрерывно в процессе ввода текста, regexp’ом. При вводе недопустимых символов выводится предупреждение.
Выбор телефона из записной книжки может происходить как drag’n’drop’ом, так и просто кликом на строке (смотря до чего пользователь догадается). Точно также и удаление телефона из списка на отсылку.
Поскольку приложение предназначено для работы в разных соцсетях, было решено обеспечить внутренние средства коммуникации с пользователями.
1.Система уведомлений: При выходе новой версии приложения можно опубликовать новость, которая будет показываться всем пользователем (либо пользователям конкретной соцсети) при очередной загрузке ими приложения.
При этом: а) последующие новости накапливаются и показываются в виде списка, с указанием даты. б) пользователь может нажать “Прочёл”, тогда конкретно этому пользователю именно эта новость больше показываться не будет.
2.Форма обратной связи (во вкладке “Подсказка”). Любое отправленное через неё сообщение записывается в базу с пометкой, кем именно, когда и из какой соцсети оно отправлено. Соответственно, при желании можно ответить человеку уже через соцсеть. И можно посмотреть все сообщения от этого человека.
Аудитория приложения и статистика
Основная аудитория приложения - школьники из Москвы и Питера, экономящие на оплате за мобильник (либо забывающие за него вовремя заплатить). Причём, женщин почти в полтора раза больше, чем мужчин.
Приходится учитывать, что даже самые, казалось бы, стандартные подходы могут оказаться для этой аудитории неочевидными. Всё должно быть просто и допускать минимум вариантов использования (рекомендую вспомнить IPhone :)
В случае жалоб нужно в первую очередь смотреть на их повторяемость, а не на конкретику. Т.к. в большинстве случаев проблема носит случайный характер (например, у человека ухудшилось соединение с Интернетом, соответственно выдалась ошибка. Или он просто не тот номер телефона ввёл, не ту кнопку нажал и пр). При большом числе пользователей таких случайностей оказывается весьма много.
Отдельные товарищи умудряются набрать текст сообщения перейдя во вкладку “Подсказка” и вбив его внизу в окошко для отправки отзывов службе поддержки. Причём, это происходит довольно регулярно, несмотря на то, что сама форма, куда нужно вбивать сообщение, находится на главной странице приложения и занимает большую его часть.
Не так давно, учитывая специфику аудитории, по просьбе пользователей была добавлена проверка грамотности сообщений - через Yandex SpellChecker API (подсвечиваются слова с ошибками, при наведении курсора предлагаются варианты их написания).
Интересно соотношение между числом установок приложения и числом реальных посетителей. Установок около 500 тыс, при этом пользователей, отправивших хотя бы одно сообщение - около 300 тыс. Т.е. получается, что 200 тыс человек приложение поставили, но пользоваться по каким-то причинам [пока] не стали.
Что касается среднесуточного числа посетителей, его видно из второго графика (хорошо видно влияние летних каникул).
Распределение числа отправляемых сообщений в сутках на третьем графике (выборки каждую минуту)
Всего отправлено около 4.5 миллиона сообщений. Входящих сообщений сравнительно немного - 78 тыс. Получили хотя бы один ответ на своё сообщение - около 40 тыс человек.
На самом деле цифры немного больше, т.к. здесь не учтены данные по самой первой версии приложения.
Одно наблюдение: пользовательское соглашение, ссылка на которое находится рядом с кнопкой “Отправить” - смотрят очень многие. Я специально собирал статистику и был несколько удивлён.
Борьба со злоупотреблениями
В конец отправляемого сообщения серверная часть автоматически добавляет имя пользователя (взятое из социальной сети) и название самой сети.
В первоначальной версии приложения пользователю позволялось менять это имя (при этом отправка сообщения становилось платной). Эту практику пришлось вскоре прекратить, так как пользователи не довольствовались невинными розыгрышами - были попытки совершенно реальных разводов и угроз (с соответствующими обращениями в службу поддержки компании).
Проблема спама также имела место (и сейчас она редко, но ещё встречается). Поскольку в сутки один пользователь может отправить не более 10 бесплатных SMS, люди занимались регистрацией фейковых аккаунтов (причём, явно не вручную) и с них рассылали различный спам. Для борьбы с этим использовался комплекс средств:
- Различные виды банов: по телефону адресата, по тексту сообщения (regexp плюс логика), по ip, по id пользователя. Нецензурные выражения и их производные вырезаются автоматически. Отправка сообщений на короткие номера, а также за пределы РФ - не допускается.
- Фильтрация текста сообщения на предмет допустимых символов. В юникоде очень много вариантов написания одних и тех же букв и цифр, что используется спамерами. Соответственно, допускаются только русские и английские буквы, цифры, пунктуация.
- Проверка пользователя (отправляющего сообщение) на его реальность. Есть ряд признаков, по которым можно понять, не является ли создатель аккаунта ботом. Это количество друзей, рейтинг и т.п.
- Периодически, полуавтоматический анализ уже отправленных сообщений на спам. Для этого используется примерно такой запрос:
SELECT ip, msgs, ROUND(v) AS v, txt, arr FROM(
SELECT count(users.ip) AS msgs,users.ip, VAR_SAMP(length(msgs_out.txt)) AS v, MAX(msgs_out.txt) as txt,
ARRAY_ACCUM(users.user_id) as arr
FROM msgs_out, users
WHERE msgs_out.dt_crt > (NOW() - '5 DAY'::INTERVAL)
AND users.id = msgs_out.user_id_int
GROUP BY users.ip
ORDER BY msgs DESC
LIMIT 50
) AS tmp
WHERE msgs>11 ORDER BY v ASC LIMIT 10
Т.е. получаем список сообщений с похожим текстом, отправленных с одного ip. Смотрим на них, подозрительных пользователей или текст баним по нужным признакам. Несмотря на очевидные упрощения - это работает.
- В периоды обострений использовались еще некоторые временные меры, в частности анализировалась активность по отправке любых сообщений с одинаковых ip или id и бан происходил автоматически при достижении некоторого порога (а потом мы уже смотрели, кому не повезло).
Платежи и входящие сообщения
Помимо 10 бесплатных SMS в сутки, можно отправлять и больше, но за внутренние баллы. Эти баллы можно получить двумя способами:
1). Они начисляются, если абонент отвечает на сообщение. Смысл в том, что сообщения на короткий номер, от имени которого приложение посылает SMS - платные (об этом предупреждается). Каждую минуту наличие новых сообщений проверяет скрипт на сервере. Если сообщение появилось, оно заносится в базу, в таблицу входящих, а адресату добавляются баллы.
Клиентское приложение, в свою очередь, регулярно проверяет - не появились ли на сервере новые входящие, а также текущее количество баллов пользователя и отосланных/оставшихся SMS..
2). При переводе вконтактовских голосов на счёт приложения и нажатии “Пополнить”, голоса преобразуются в баллы.
Здесь надо отметить следующее: во-первых, никакие ключевые решения не должны приниматься приложением на стороне клиента. Другими словами, на сервере не должно быть внешних сервисов типа “Добавить N баллов” или “Снять N баллов”. Такие операции должны вызываться лишь в ходе выполнения других (отсылки SMS, например).
И, конечно, во всех API социальных сетей операции с балансом изначально не выполняются по запросу от клиентской части приложения - для этого предусмотрены отдельные серверные методы.
Кроме того, все транзакции связанные с платежами и баллами подробно (время, операции, баланс до операции, баланс после операции) фиксируются на сервере в специальном логе, чтобы в случае жалоб можно было полностью восстановить картину событий (в т.ч. сравнить её со списком транзакций в социальной сети, там где он есть).