Пульс твиттера о веб-разработке

Пульс твиттера о веб-разработке Наверное, уже прошёл год с тех пор, как я написал скрипт для трансляции сообщений из Twitter. Изначально я это делал для маркетинговой конференции Digitale, но востребованность трансляции оказалась намного выше. И в качестве очередного примера я добавил трансляцию сообщений о веб-разработке. Но об этом чуть ниже. Потому что начать я хотел бы с грустного.

Twitter API 1.1

Twitter закрыл первую версию своего API. Все запросы к старой версии будут возвращать статус HTTP 410 Gone. Главное изменение заключается в авторизации. Теперь работает только OAuth авторизация. Ну что же, давайте переделаем нашу трансляцию.

Я не хочу ничего переделывать, я хочу смотреть демонстрацию.

Twitter Streaming API

Для начала, выделим код работы с Twitter Streaming API в отдельную функцию.

http = require 'request'

module.exports = (track, oauth, fn) ->
  params =
    url: 'https://stream.twitter.com/1.1/statuses/filter.json'
    oauth: oauth
    form:
      include_entities: true
      track: track

  request = http.post params, (error) ->
    fn error, false if error

  request.on 'data', (buffer) ->
    try
      tweet = JSON.parse buffer.toString()
    catch error
      tweet = false

    if tweet
      data =
        id: tweet.id_str
        link: "http://twitter.com/#{tweet.user.screen_name}"
        avatar: tweet.user.profile_image_url
        login: tweet.user.screen_name
        name: tweet.user.name or tweet.user.screen_name
        text: tweet.text

      fn null, data

Как видите, я решил использовать request модуль. С ним будет проще проходить OAuth авторизацию и разбирать ответ сервера. Вызов функции будет выглядеть так.

oauth =
  consumer_key: 'consumer key'
  consumer_secret: 'consumer secret'
  token: 'oauth token'
  token_secret: 'token secret'
 
filter = require './twitter/filter'
filter 'html5,css3', oauth, (error, tweet) ->
  console.log tweet unless error

Остаётся только передавать все новые сообщения клиентам. Например, это можно сделать с помощью socket.io.

clients = []

io = require('socket.io').listen(8080)
io.sockets.on 'connection', (socket) ->
  clients.push socket

oauth =
  consumer_key: 'consumer key'
  consumer_secret: 'consumer secret'
  token: 'oauth token'
  token_secret: 'token secret'

send = (tweet) ->
  client.send JSON.stringify tweet for client in clients

filter = require './twitter/filter'
filter 'html5,css3', oauth, (error, tweet) ->
  send tweet unless error

OAuth авторизация для Twitter API

Осталось получить OAuth ключи и можно запускать. Я думал, что этот процесс достаточно простой, но вчера увидел сообщение от @miripiruni и решил подробнее остановиться на этой задаче. Так как трансляция будет работать на серверной стороне, то нам будет достаточно девелоперских ключей.

Новое OAuth приложение для Twitter
Заходим на сайт для разработчиков и создаём новое приложение.
OAuth ключи для Twitter
Получаем «consumer key» и «consumer secret» ключи.
Access token для OAuth
Создаём девелоперский «access token» ключ.

Итак, у нас есть все четыре ключа: «consumer key», «consumer secret», «access token» и «access token secret».

Вывод через socket.io

Теперь дело за клиентской частью. Простой вариант вывода сообщений трансляции на страницу.

(function(window, document) {
  'use strict';

  var socket = window.io.connect('http://127.0.0.1:8080'),

  // Получаем шаблон
  template = document.getElementById('twitter-template').innerHTML,

  // Контейнер со всеми сообщениями
  container = document.getElementById('twitter-feed'),

  messages = [];

  // Все новые сообщения добавляем в массив messages
  socket.on('connect', function() {
    socket.on('message', function(result) {
      messages.push(JSON.parse(result));
    });
  });

  // Каждые 2 секунды берём первое сообщение из массива и показываем
  window.setInterval(function() {
    if (messages.length > 0) {
      var message = messages.shift(),

      // Получаем все сообщения в ленте
      tweets = container.querySelectorAll('section'),
      length = tweets.length,

      section = document.createElement('section');

      // Создаём HTML шаблон для нового сообщения
      section.innerHTML = mustache(template, message);

      if (length > 0) {
        container.insertBefore(section, tweets[0]);
      } else {
        container.appendChild(section);
      }

      // Если в ленте больше 20 сообщений, удаляем самое старое
      if (length > 20) {
        container.removeChild(tweets[length-1]);
      }
    }
  }, 2000);
})(window, document);

В таком виде мы получаем работающую трансляцию всех сообщений по заданным хештегам.

Пульс твиттера о веб-разработке

Для демонстрации я решил подготовить немного улучшенный вариант. Он транслирует большинство сообщений о веб-разработке на русском и английском языке. Как правильно заметил @nesterov_vit — очень похоже на телевизор.

Теперь предлагаю остановиться подробнее на тех улучшениях, которые я добавил.

Серверная часть

Во-первых, хотелось убрать нежелательные сообщения из трансляции. Для этого я подготовил несколько правил:

  1. сообщения только на русском и английском языке;
  2. сообщения от пользователей с нестандартными аватарами;
  3. игнорировать ретвиты и ответы;
  4. игнорировать заданный список аккаунтов;
  5. игнорировать заданные слова в сообщении;
  6. игнорировать сообщения с переизбытком хештегов.

Все эти правила я выделил в отдельную функцию.

twitter = require 'twitter-text'

options =
  # список нежелательных аккаунтов (для тролей)
  mute: ['']
  # список стоп-слов (для спама)
  spam: ['купить javascript без смс']

module.exports = (tweet) ->
  return false if not tweet or not tweet.user or not tweet.text

  # Разрешены сообщения только на русском и английском
  if tweet.lang?
    return false if tweet.lang not in ['en', 'ru']
  else if tweet.user.lang?
    return false if tweet.user.lang not in ['en', 'ru']

  # Пропускаем нежелательные аккаунты
  if options.mute.length > 0
    account = "#{tweet.user.screen_name}".toLowerCase()
    return false for mute in options.mute when account is mute

  # Пропускаем пользователей с аватаром по-умолчанию ^_^
  if tweet.user.profile_image_url.indexOf('default_profile_images') > -1
    return false

  text = "#{tweet.text}"

  # Пропускаем старые ретвиты и ответы на сообщения
  if text.indexOf('RT ') is 0 or text.indexOf('@') is 0 or text.indexOf('.@') is 0
    return false

  if options.spam.length > 0
    # Преобразуем сжатые ссылки в полный вид
    if tweet.entities.urls? and tweet.entities.urls.length > 0
      for entity in tweet.entities.urls
        text = text.replace entity.url, entity.expanded_url

    text = text.toLowerCase()

    # Пропускаем твиты содержащие в себе стоп-слова
    return false for spam in options.spam when text.indexOf(spam) isnt -1

  # Пропускаем твиты содержащие переизбыток хештегов ^_^
  hashtags = twitter.extractHashtagsWithIndices text
  return false if hashtags and hashtags.length > 4

  return true

Во-вторых, можно поработать над текстом сообщений. Вот, что мы с ними сделаем:

  1. все сжатые ссылки преобразуем в нормальные;
  2. все ссылки на картинки преобразуем в изображения;
  3. заменим размер аватаров на максимально возможный.

Опять же, этим будет заниматься отдельная функция.

twitter = require 'twitter-text'

module.exports = (tweet) ->
  # Преобразуем только ссылки, хештеги и аккаунты оставляем текстом
  text = twitter.autoLinkUrlsCustom tweet.text, target: '_blank'

  # Заменяем сжатые ссылки на нормальные
  if tweet.entities.urls? and tweet.entities.urls.length > 0
    for entity in tweet.entities.urls
      text = text.replace entity.url, entity.expanded_url
      text = text.replace entity.url, entity.display_url

  # Заменяем ссылки на картинки на сами изображения
  tweet.media = false
  if tweet.entities.media? and tweet.entities.media.length > 0
    for entity in tweet.entities.media
      text = text.replace entity.url, entity.expanded_url
      text = text.replace entity.url, entity.display_url

      tweet.media =
        url: entity.media_url
        w: entity.sizes.small.w
        h: entity.sizes.small.h

  tweet.text = text

  # Заменяем размеры аватаров на максимальные
  image = tweet.user.profile_image_url
  pos = image.lastIndexOf '_'
  tweet.user.profile_image_url = image.substring(0, pos) + '_bigger' + image.substring(pos + 7)

  tweet

Клиентская часть

В этой части я постарался сконцентрироваться на удобстве.

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

Во-вторых, из-за того, что Twitter Streaming API возвращает только текущие сообщения, то может произойти так, что новый пользователь долгое время ничего не увидит. Чтобы этого избежать, сразу показываю 5-10 сообщений из поиска. Для этого я использую Twitter REST API. При этом новые сообщения из Streaming API будут поступать в очередь сразу за старыми.

В-третьих, время сообщения отображается, как время прошедшее с публикации. И, чтобы оно оставалось в актуальном состоянии, раз в минуту оно пересчитывается.

В-четвёртых, вся видимая лента хранится в sessionStorage. Это сделано, чтобы избежать случайных обновлений страницы и других похожих ситуаций. Таким образом информация всегда будет на своём месте.

Ну что, посмотрим демонстрацию?

Анализ сообщений

Было бы здорово к этому прикрутить анализ сообщений. И пытаться выявить наиболее интересные темы за день. Но это, видимо, не мой уровень. Может кто-то из Яндекса придёт и скажет, как это сделать, а?

Что ещё почитать?

← Плавное проявление, мгновенное затуБудущее за веб-компонентами →