в предыдущем посте было вступление о том, как я собирался написать эту программу. теперь она сама:
это 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
выглядит до крайности просто, не правда ли? тут уж комментарии излишни.
думаю, что и все остальные функции выглядят достаточно понятно с моими пояснениями.
заключениене знаю, нужен ли ещё один пост, рассказывающий о том, как установить необходимые библиотеки, как запустить этот код и как автоматизировать этот запуск.
надеюсь, это было кому-нибудь интересно читать. если таковые найдутся, жду Ваши вопросы/предложения/возражения (: