Pull to refresh

репостинг Twitter (или rss) в статус vkontakte.ru на Haskell

Reading time9 min
Views1.9K
В данной статье речь пойдёт о небольшой программке, которая репостит твиты в статус во вконтакте.
Задача довольно простая и совершенно неоригинальная. Началось всё с того, что я прочитал статью на Хабре о том, как это решается на python'е и аналогичную статью про php. В интернетах вроде бы даже какие-то онлайн сервисы есть специально для этой задачи. Но тут весь цимус в том, чтобы решить эту несложную задачу самому, используя свои любимые инструменты. Собственно решение на php появилось позже и с такой же целью.

Ну и на чём же писал я? На haskell, natürlich!
Дальше подробно расскажу о том, как я всё сделал и как это повторить. Никаких особых знаний для понимания, пожалуй, не требуется.

Вступление

В реализации решения мне помогли те две статьи и статья про репостинг из rss в livejournal на хаскелле.
Сначала я хотел по-честному сделать работу с твиттером через twitter-api: потыкал соответствующую библиотечку из hackage, но она сходу не заработала и я её оставил — мне хотелось побыстрее получить результат и было лень копаться и разбираться, что я не так делаю. А поскольку твиттер транслируется по rss и чтение rss на haskell' е — уже решённая задача, я пошёл этим путём.
Тем более, что это более универсальное решение. Можно транслировать любой rss-канал во вконтакт. Можно даже сказать, что это не twitter2vkontakte, а rss2vkontakte.
Кроме того, я пользовался vkontakte-api, а не парсил страницу в поисках статуса, как мои предшественники. Думаю, это плюс.

Остальное — это literate-haskell-код. То есть не код с комментариями, а подробные комментарии с кусочками кода, которые являются обычными исходниками на haskell'е. Этот пост можно просто сохранить целиком в файл с расширением .lhs и скормить интерпритатору/компилятору. Всё должно нормально работать.
Весь рабочий код здесь выделен вот такими символами: >

Необходимые приготовления

Предполагается, что компилятор Haskell и основной набор библиотек у Вас уже есть. Если нет, то это легко исправить — надо установить Haskell Platform. Это очень просто.

Теперь, чтобы установить дополнительные библиотеки, достаточно набрать в консоли:
cabal update
cabal install regex-tdfa curl feed utf8-string

Дальше небольшой список импортов с краткими пояснениями.
Пару раз я использовал регулярные выражения:

> import Text.Regex.TDFA ((=~))

Один раз разрезал и склеивал список:

> import Data.List (intercalate)

Для всех интернет-запросов пользовался библиотекой curl:

> import Network.Curl      (curlGetString)
> import Network.Curl.Opts

Читал и парсил rss-фиды:

> import Text.Feed.Import (parseFeedString)
> import Text.Feed.Query (getFeedItems, getItemSummary)

И даже разок кодировал строку в юникод:

> import Codec.Binary.UTF8.String (encodeString)


Дальше будет более содержательный код с более содержательными и, возможно, местами излишне подробными разъяснениями…

Twitter через rss

Первое, что нам понадобится — адрес нашей rss-ленты твитов. Его можно взять у себя на страничке в твиттере. Заведём для него отдельную константу:

> feedUrl = "https://twitter.com/statuses/user_timeline/22251772.rss"

Как забирать rss-фиды и парсить, я подсмотрел в статье про rss2lj. Но пользоваться этой библиотечкой я не стал. Там всё конечно хорошо сделано, но мне нужна одна простая функция, которая будет скачивать rss-фид, брать первый элемент и извлекать его содержание. И вот как я её сделал:

> getTweet :: IO String
> getTweet = do
>     (_,feed) <- curlGetString feedUrl []
>     return $ getMsg $ head $ getItems feed
>     where
>         getItems = maybe (error "rss parsing failed!") getFeedItems . parseFeedString
>         getMsg   = maybe (error "rss-item parsing failed!")  format . getItemSummary
>         format   = unwords . ("twitter:":. tail . words . encodeString

Поясню, что в ней происходит. Функция curlGetString :: URLString -> [CurlOption] -> IO (CurlCode, String) берёт url-адрес, список опций, и выдаёт код операции (CurlOk, если всё прошло успешно) и ответ сервера. В данном случае, в качестве адреса мы указываем нашу twitter-rss ленту, и не даём никаких опций. На код завершения не обращаем внимания. А вот содержательную часть ответа обзываем feed.
Следующую строку надо читать справа налево: извлекаем элементы фида (getItems feed), получаем список, берём из него первый элемент (head), извлекаем из него собственно сообщение (getMsg) и возвращаем на выход.
А теперь поподробнее об этих функциях, в таком же порядке. Каждая из них написана в point-free-style, то есть без указания аргумента, просто как композиция (точка .) Других функций.
Композицию тоже можно читать справа налево, по точкам (: то есть в порядке применения фукнций: в getItems сначала применяется функция parseFeedString (из библиотеки Feed), она имеет тип (String -> Maybe Feed), то есть на вход получает строку со всякой кашей из rss-тегов, а выдаёт абстрактный тип фида, с которым уже можно что-то делать. Поскольку возвращается значение Maybe Feed («Может быть фид»), может статься, что парсер подавится, и вернёт Nothing — тогда мы выдаём ошибку с текстом «rss parsing failed!». Если же парсинг пройдёт удачно, мы получим значение (Just фид), и тогда применим к нему функцию getFeedItems, которая извлекает из фида элементы в виде списка. Это ветвление (Nothing или Just ...) реализуется стандартной функцией maybe.
После работы getItems мы получим список элементов фида: [Item]. Нам нужен только первый из них (то есть последний по дате). Берём его функцией head. И теперь хотим выковырять из него текст сообщения: getMsg.
Эта функция имеет схожую с getItems структуру: сначала применяется getItemSummary, которая возвращяет Maybe String. Если извлечь содержание не удалось, выдаём соответствующую ошибку. Иначе, форматируем полученное сообщение.
Форматирование (format) производится вкратце следующим образом (опять справа налево): кодируем строку в unicode, разбиваем на слова (по пробелам), выбрасываем первое слово, вставляем вместо него «twitter:» (по желанию), склеиваем обратно все слова в одну строку. Первое слово в rss-твитах — это всегда ваш ник. Поэтому мы его выкидываем.

Вот и всё с rss. Я, возможно, описал всё излишне подробно, но думаю, для любопытствующих, незнакомых с haskell'ем, это описание было содержательным.

Vkontakte api

Первым делом заведём несколько констант для работы с вконтактом:

> email = "Ваш e-mail"
> uid = "Ваш user-id вконтакте"
> pass = "Ваш пароль"

Это данные, соответствующие вашей регистрации во вконтакте.

Все операции осуществляются GET запросами на сервер (всё та же функция curlGetString), с соответствующими хитрыми адресами. Строятся они следующим образом:
базовый адрес (например userapi.com/data?) плюс список параметров в форме ключ=значение, разделённых амперсандами &.
Чтобы формировать такие адреса, напишем пару вспомогательных функций:

> param :: (String, String) -> String
> param (key, value) = key ++ "=" ++ value ++ "&"

Эта функция просто берёт пару (ключ, значение) и делает из неё строку нужного формата.

> formUrl :: String -> [(String, String)] -> String -> String
> formUrl base opts sid = base ++ ( concatMap param (opts++[("id",uid)]) ) ++ sid

Формируем url нужного формата из базового адреса base, списка опций opts (ввиде пар), и идентификатора сессии sid (о нём позже).
Содержательная часть находится в скобках: map берёт функцию и список, и применяет функцию к каждому элементу списка. То есть из списка пар (ключ, значение), делает список строк "ключ=значение&". А concat просто склеивает все эти строки в одну (concatMap = concat . map).
Для разных задач набор опций отличается, но во всех случаях нужно указывать идентификатор пользователя (uid), поэтому, чтобы не писать эту опцию каждый раз, мы добавляем её в определении этой функции.

Чтобы как-то работать с вконтактом, нужно сначала авторизоваться. Тогда сервер даст нам печеньки (cookies) и идентификатор сессии (sid = session id). Печеньками я пользоваться не стал, а вот sid нужен практически для любой операции с получением/изменением данных пользователя.

> login :: IO String
> login = do
>     (_,headers) <- curlGetString authUrl [CurlHeader True]
>     return ( headers =~ "sid=[a-z0-9]*"  :: String )
>     where
>         authUrl = formUrl "http://login.userapi.com/auth?"
>                           [("site","2"), ("fccode","0"),
>                            ("fcsid","0"), ("login","force"),
>                            ("email",email), ("pass",pass)]  ""

Адрес для аутентификации имеет кучу опций, назначение которых я не понял, но взял из документации и без них ничего не работает. Формируем этот адрес спомощью только что написанной функции formUrl, при этом в последние две опции вставляются наш email и пароль. А параметр sid остётся пустым — у нас его пока нет, и собственно ради него мы и написали функцию login.
Что в ней происходит: посылается curl-запрос, по адресу authUrl, который возвращает заголовки headers (для этого выставляется опция CurlHeader). В них собственно печеньки, адрес перенаправления и что-то ещё. Вот в адресе, куда нас посылает сервер, и спрятано то, что мы ищем. С помощью секретной техники регулярных выражений, из headers выдирается заветный session id, вида «sid=35dfe55b09b599c9fx622fcx8cd83a37».
На регулярных выражениях в haskell'е я останавливаться не буду — это отдельная тема. Можно считать, что это просто поиск подстроки нужного вида.

Замечательно! sid мы получили, теперь перед нами открыты все возможности api вконтакта. Для нашей задачи нужна только одна — изменение статуса.
В принципе любое взаимодествие с вконтактом будет свобиться к следующей команде:

(_,answer) <- curlGetString someUrl []

где someUrl — соотвествующий запрос (смотреть в документации), а answer — ответ сервера. Вот как выглядит запрос на изменение статуса:

> setActivityUrl :: String -> String -> String
> setActivityUrl text = formUrl "http://userapi.com/data?" [("act""set_activity"), ("text", text)]

Обратите внимание на то, что третий параметр функции formUrlsid, не указан. Это частичное применение — у функции 3 параметра, а мы дали только 2, значит получилась функция от оставшегося одного параметра. То есть setActivityUrl — функция не только от параметра text (собственно новый статус), но и от второго параметра sid, который как бы дописывается справа.

Ещё одна мелочь: в тексте твита будут пробелы, а это недопустимо для url-запроса. Поэтому мы сделаем простенькую функцию, заменяющую все пробелы, на %20:

> escSpaces = intercalate "%20" . words

Она разбивает строку на список слов, вставляет между соседними элементами этого списка строку "%20", а потом склеивает всё снова в одну строку (последние два действия делает функция intercalate).

Теперь мы можем собрать из уже обсуждённых частей, функцию изменения статуса:

> setStatus :: String -> String -> IO ()
> setStatus text sid = do
>     (_,answer) <- curlGetString url [] 
>     if answer =~ "\"ok\":1"   :: Bool
>        then putStrLn text
>        else error "something is bad with vkontakte-api..."
>     where
>         url = setActivityUrl (escSpaces text) sid

Можно было бы написать эту функцию и проще, в одну строку:

setStatus text sid = curlGetString (setActivityUrl (escSpaces text) sid) []

Но первый вариант нагляднее, там делается проверка ответа сервера — если ответ содержит "ok":1, то всё хорошо — статус сменился, о чём мы и сообщаем пользователю (себе то есть).
Всё! Теперь у нас есть все части мозаики и собрать её очень просто.

main

То, ради чего было были написаны все эти функции:

> main = do
>     tweet <- getTweet
>     sid   <- login
>     setStatus tweet sid

Выглядит до крайности просто, не правда ли? Тут уж комментарии излишни.
Думаю, что и все остальные функции выглядят достаточно понятно с моими пояснениями.
Статистики ради: ~40 LinesOfCode.

Заключение

Чтобы запустить этот код, надо как уже говорилось, просто сохранить весь пост в файл с расширением .lhs и набрать в консоли:

runhaskell имя_файла.lhs

Вот и всё.
Не знаю, нужно ли продолжение, рассказывающее о том, как автоматизировать этот запуск.
Лично для себя я (как пользователь Mac OS X) решил это созданием «Службы» в Automator и назначением горячей клавиши, для её быстрого вызова — это только автоматизация запуска, но для меня этого достаточно.

Надеюсь, это было кому-нибудь интересно читать. Жду Ваши вопросы/предложения/возражения (:

upd: переместил в тематический блог.
Tags:
Hubs:
+8
Comments11

Articles