Tom Adler’s blog

мой агрегатор новостей

у меня довольно давно были претензии к Google Reader. Некоторые из них касались интерфейса, поэтому я от него отказался и стал использовать собственный интерфейс: Open Google Reader. Но прочие претензии были к серверной части, например, отсутствие поддержки авторизации для подзамочных постов в жж, или невозможность получить полный текст статьи для куцых потоков. Поэтому уже несколько лет я прикидывал, как можно сделать собственный агрегатор новостей, хотя никак не мог собраться.

волшебный стимулирующий пендель пришёл с неожиданной стороны, когда Гугл объявил о скором прекращении поддержки ридера. Пришлось браться за работу. К счастью, у меня был довольно ограниченный набор требований к серверной части, поэтому возиться пришлось недолго. И использование готовых модулей тоже сильно помогло.

я уже писал о своей любви к статическим файлам как основе для веб-сервисов. Для агрегатора мне тоже получилось обойтись без кода, исполняющегося в контексте веб-сервера. Регистрации нет: продукт однопользовательский. Аутентификация — HTTP Auth. REST API (удалить из непрочитанных, сохранить на будущее) сделано через WebDav в nginx. Агрегированные списки строятся при помощи ls и cat, которые запускает incron при изменении списка файлов.

ну а собственно сборщик новостей — это работающий по крону скрипт на питоне, использующий feedparser. Он проходит по списку потоков, вынимает из них новые записи и складывает их в папку непрочитанных. При необходимости ходит по ссылкам и достаёт оттуда полезное содержимое по CSS selector. Дата обновления потока хранится в файле.

нельзя не упомянуть недостатки решения, с которыми я легко мирюсь. Нет подписки на PubSubHubbub, поэтому приходится подождать (максимум 15 минут — такая периодичность обновления). Нет веб-интерфейса для управления списком потоков. Если запись со вчерашней датой попадает в поток только сегодня (как это часто бывает у хабра, баша, и т.п.) она будет проигнорирована. При обновлении записи она может снова попасть в непрочитанные. Но всё это мелочи для меня.

в итоге уже где-то месяц я пользуюсь своим агрегатором, а с пятницы вообще перестал заглядывать в Google Reader.

любопытно, что гугл стимулирует меня отказываться от его продуктов в очень подходящие моменты: как раз когда мои навыки развиваются до нужного уровня, чтобы настроить или сделать альтернативу. Вначале была почта, потом интерфейс ридера, потом система рекомендаций ридера, потом фотогалерея, за ней агрегатор. А сейчас гугл собирается предать XMPP, так что будет у меня собственный джаббер-сервер, который я тоже давно хотел.

,

CSS content и псевдоэлементы

кнопки open google reader выглядят по-разному в обычной и мобильной версиях. В обычной на них есть текст, в мобильной только юникодные символы-иконки. Долгое время за выбор одного или другого отвечал яваскрипт, но тут я решил, что пора бы это перевести на CSS. В нём как раз есть удобное свойство content для того, чтобы подставлять нужный текст. И перевёл, и возрадовался.

потом, однако, я решил посмотреть, как оно выглядит в хроме и фаерфоксе: опера-то скоро сменит движок. И увидел я в этих браузерах пустые кнопки без содержимого. Какой интересный кроссбраузерный баг!… Когда же я стал разбираться, оказалось, что по спецификации этот самый content работает только на псевдоэлементах :before и :after. В общем, использовать его для текста кнопок не получится без дополнительного вложенного элемента. Пришлось его добавлять.

жаль, конечно, что это не сработало. Та же проблема, что и с элементом img: посредством CSS его пока что не изменишь.

в общем, это был ещё один пример того, как самым безглючным браузером кажется именно тот, в котором ведёшь разработку.

,

ловушка self и this

сегодня я попался в восхитительную ловушку, возникшую в результате сочетания двух безобидных на первый взгляд техник: наследования и оптимизации. Конечно, в минимальных примерах кода в этой записи всё будет более заметно, тем более, что вы знаете, что ловушка тут есть.

итак, пример прототипного наследования:

function Parent() {
    this.test = function() {};
}
function Child() {}
Child.prototype = new Parent();
child = new Child();

пример оптимизации для лучшей минификации и быстрого выполнения:

function Example() {
    var NS = some.very.long.namespace,
        self = this;
    self.value = new NS.Value();
}

и комбинированный пример с ловушкой в действии:

function Parent() {
    var self = this;
    self.test = function() {
        self.value = true;
    };
}
function Child() {
    var self = this;
    self.value = false;
}
Child.prototype = new Parent();
child = new Child();
console.log(child.value); // false
child.test();
console.log(child.value); // false

Проблема возникает из-за того, что Child и Child.prototype — глобальные объекты. Когда создаётся Child.prototype, конструктор Parent сохраняет в self именно этот объект, общий для всех экземпляров Child. Естественно, он не совпадает ни с одним из этих экземпляров. И код внутри Parent работает со свойствами именно этого объекта.

Конечно, если бы мы объявляли методы для наследования по-человечески, а не внутри конструктора, этой проблемы бы не было. Собственно, и оптимизация от сохранения this в локальную переменную неизвестно какая, вероятно, этого делать не имеет смысла.

ссылки о hypermedia APIs

я как-то прозевал появившиеся за последний год подходы к встраиванию метаданных в JSON API, и вот недавно обнаружил целых две спецификации. Обе они пытаются вернуть в JSON что-то хорошее из XML. В частности это хорошее — гиперссылки, отсюда и такое название.

вкратце идею можно описать так: когда мы запрашиваем html-ресурс, мы обычно получаем и кучку ссылок на связанные объекты, а также возможные действия. Хочется иметь то же самое для JSON, ведь там обычно просто голые данные. Так давайте договоримся о стандартном способе прицеплять к ним ссылки и, возможно, сами связанные объекты. Hypertext Application Language (HAL) этим и ограничивается. Collections+JSON идёт дальше и определяет кучу других ограничений, поэтому и нравится мне меньше.

в тему метаданных хочу дать и ещё одну ссылку: про версионность API. Я пока что видел мало примеров этого, и лучшим вариантом мне казалось включение версии непосредственно в адрес: /api/v2/resource. Однако Peter Williams разумно замечает, что такой подход приводит к тому, что один и тот же ресурс с разными версиями API — это разные ресурсы. Он же предлагает и красивый альтернативный способ добавлять версию в API: Accept: application/vnd.mycompany.myapp-v2+xml. Концептуально это мне очень нравится, хотя не могу не обратить внимания на недостатки: такой интерфейс сложнее исследовать браузером, и тип всё-таки нужно регистрировать. Но красиво.

,

сохранение jabber-статусов в atom

три года назад я решил, что лучший микроблог — это статус в джаббере. Правда, у него был один недостаток: его было нетривиально вставлять в мой RSS-поток рекомендаций. Эта проблема легко решалась сторонним сервисом — у френдфида был джаббер-бот, который мог отправлять статусы во френдфид, а оттуда уже можно было вынуть RSS.

к сожалению, фейсбук купил команду френдфида, и тот потихоньку начал разлагаться. Бот не работает уже давно. Впрочем, технически задача очень проста: подключиться к серверу, ждать обновлений статуса, сохранять их в поток. К сожалению, на моём сервере оказываются только LTS-версии убунты, и предыдущая версия 10.04 не имела какой-то из нужных мне библиотек на питоне. Зато недавно я сервер обновил, и вскоре после этого смог завершить и запустить своего бота. Теперь мои статусы оказываются в специальном потоке.

когда всё уже готово, код кажется простым и очевидным. Для подключения к серверу достаточно создать и использовать такой класс:

class Client(JabberClient):
    def __init__(self):
        JabberClient.__init__(self,
            JID(JID_VALUE),
            PASSWORD,
            server='talk.google.com',
            auth_methods=['plain'],
            tls_settings=TLSSettings(require=True, verify_peer=False))

client = Client()
client.connect()
client.loop(1)

настроек у клиента могло бы быть и поменьше, но Google Talk очень особенный. Зато дальше становится проще. Вот так подключается обработчик событий:

self.get_stream().set_presence_handler('available', self.presence)

а вот, собственно, и сам обработчик:

def presence(self, stanza):
    status = stanza.get_status()

    if stanza.get_show() is not None or not status or status == self.current_status:
        return True

    self.current_status = status
    self.update_feed(status)
    publish(PUSH_URL, SELF_URL)

    syslog.syslog('New status: %s' % (status.encode('utf8')))

    return True

здесь пояснения требуют get_show и publish. Первый возвращает None, когда контакт именно в онлайне, а не отошёл, недоступен или что-то ещё. Второй публикует обновление на PubSubHubbub-хабе.

кстати, мне хотелось, чтобы этот бот был полноценным сервисом на моём сервисе. Для этого пришлось разобрать основы системы upstart, которая призвана заменить предыдущего управляющего сервисами init. Оказалось, с новой системой простые вещи действительно просты. Вот такой файл я добавил в /etc/init:

description "Logger of IM statuses"
author  "Artemy Tregubenko <me@arty.name>"

start on runlevel [234]
stop on runlevel [0156]

expect daemon
respawn

chdir /var/www/shared_arty_name/im-status/
exec python /var/www/shared_arty_name/im-status/IMStatus.py

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

def start():
    client = Client()
    client.connect()
    client.loop(1)

def stop():
    client.disconnect()

context = daemon.DaemonContext(
    signal_map={signal.SIGTERM: stop},
    gid=grp.getgrnam('www-data').gr_gid,
    uid=grp.getgrnam('www-data').gr_gid,
    detach_process=True,
)

with context: start()

а вот и код бота целиком. Интересно, есть ли смысл превращать его в сервис? Глядишь, кто-нибудь на пиво и пожертвует : )

, , ,