2 янв. 2010 г.

Twitter to vkontakte. Содержательная часть

в предыдущем посте было вступление о том, как я собирался написать эту программу. теперь она сама:

это literate-haskell-код, то есть текст со вставками кода. этот пост можно просто сохранить целиком в файл с расширением .lhs и скормить интерпритатору/компилятору. всё должно нормально работать.

начнём с импортирования необходимых библиотек. как их установить, если их у Вас нет, я расскажу потом.

пару раз я использовал регулярные выражения:

> 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 = "ваша почта, использованная для регистрации вконтакте"
> uid = "ваш user id вконтакте"
> pass = "ваш пароль от вконтакта"

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

все операции осуществляются обычными GET запросами на сервер (всё та же функция curlGetString), с соответствующими хитрыми адресами. строятся они следующим образом:
базовый адрес (например http://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)]

обратите внимание на то, что третий параметр функции formUrl - sid, не указан. это частичное применение - у функции 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 ("ok! your new status: " ++ 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

выглядит до крайности просто, не правда ли? тут уж комментарии излишни.
думаю, что и все остальные функции выглядят достаточно понятно с моими пояснениями.


заключение

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

Twitter to vkontakte. Вступление

с месяц назад, на выходных, когда нужно было готовиться к семинару по гомологической алгебре, у меня проснулась жажда творчества. мне надо было штудировать всякие умные книжки, а я не мог себя заставить (особенно ближе к ночи), таки немного попрограммировал.
суть того, чем я занимался отражена в заголовке - я написал программу, которая репостит твиты в статус во вконтакте.
задача довольно простая и совершенно неоригинальная. собственно началось с того, что я прочитал статью на Хабре о том, как это решается на python и аналогичную статью про php. в интернетах вроде бы даже какие-то онлайн сервисы есть специально для этой задачи. но тут весь цимус в том, чтобы решить эту несложную задачу самому, используя свои любимые инструменты. собственно решение на php появилось позже и с такой же целью.

ну и на чём же писал я? ответ очевиден - на haskell'е, natürlich!

в реализации решения мне помогли те две статьи и статья про репостинг из rss в livejournal на хаскелле.
сначала я хотел по-честному сделать работу с твиттером через twitter-api: потыкал соответствующую библиотечку из hackage, но она сходу не заработала и я её оставил - мне хотелось побыстрее получить результат и было лень копаться и разбираться, что не так. а поскольку твиттер транслируется по rss и чтение rss на haskell' е - уже решённая задача, я пошёл этим путём.

это первое отличие от уже имеющихся решений.
мнимый минус такого подхода в том, что это может медленнее работать, так как твиты получаются несколько опосредовано,, но поскольку срочности особой нету и счёт не идёт на секунды, этот минус отменяется.
второй мнимый минус в том, что так не получить доступ к закрытым страницам. если ваши твиты доступны только для ваших фоловеров, то по rss они не транслируются. но значит вам и не надо, чтобы такие твиты были во вконтакте! так что этот минус тоже отменяется.
плюс же состоит в том, что это более универсальное решение. можно транслировать любой rss-канал во вконтакт. для этого надо будет внести небольшие изменения. но важно то, что нынешняя заготовка даёт возможности для улучшений. можно даже сказать, что это не twitter2vkontakte, а rss2vkontakte.

втрое отличие: я пользовался vkontakte-api, а не парсил страницу в поисках статуса, как мои предшественники. и это правильно. это значит входить в дом через парадную дверь, а не через задний двор.

это было очень длинное вступление, которое можно было не читать (:

остальное - это literate haskell. то есть не код с комментариями, а подробные комментарии с кусочками кода, которые являются обычными исходниками на haskell'е.