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

    В данной статье речь пойдёт о небольшой программке, которая репостит твиты в статус во вконтакте.
    Задача довольно простая и совершенно неоригинальная. Началось всё с того, что я прочитал статью на Хабре о том, как это решается на 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: переместил в тематический блог.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      +3
      Заглавные нынче в дефиците. Или это побочный эффект от письма на хаскеле?
        0
        По-ходу это тенденция. Я сам стараюсь от этого отвыкнуть.
          0
          Да, это вредная привычка… Исправил.
          А вообще забавно — в Хаскелле действительно идентификаторы функций должны начинаться с маленькой буквы (:
          +2
          Одно не пойму, почему до сих пор нет такого веб-сервиса?
          0
          [оффтопик]Они передают пароль в plaintext по http. quadruple_facepalm.png[/оффтопик]
            –1
            ~40 LinesOfCode
            как бы доказывает превосходство ФЯ над императивщиной
              0
              как бы ничего не доказывает.
              написал же специально: «Статистики ради»
                0
                Ок, вам виднее. Тем не менее, ФЯ мне кажутся более предпочтительными средствами разработки, чем императивные
                  0
                  Пока что императивные лучше ложатся на физическую архитектуру и мозги среднего разработчика. Хотя в долгосрочной перспективе императивщина скорее всего будет вытеснена в системную область и пользовательские интерфейсы.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое