у меня довольно давно были претензии к Google Reader. Некоторые из них касались интерфейса, поэтому я от него отказался и стал использовать собственный интерфейс: Open Google Reader. Но прочие претензии были к серверной части, например, отсутствие поддержки авторизации для подзамочных постов в жж, или невозможность получить полный текст статьи для куцых потоков. Поэтому уже несколько лет я прикидывал, как можно сделать собственный агрегатор новостей, хотя никак не мог собраться.
волшебный стимулирующий пендель пришёл с неожиданной стороны, когда Гугл объявил о скором прекращении поддержки ридера. Пришлось браться за работу. К счастью, у меня был довольно ограниченный набор требований к серверной части, поэтому возиться пришлось недолго. И использование готовых модулей тоже сильно помогло.
я уже писал о своей любви к статическим файлам как основе для веб-сервисов. Для агрегатора мне тоже получилось обойтись без кода, исполняющегося в контексте веб-сервера. Регистрации нет: продукт однопользовательский. Аутентификация — HTTP Auth. REST API (удалить из непрочитанных, сохранить на будущее) сделано через WebDav в nginx. Агрегированные списки строятся при помощи ls и cat, которые запускает incron при изменении списка файлов.
ну а собственно сборщик новостей — это работающий по крону скрипт на питоне, использующий feedparser. Он проходит по списку потоков, вынимает из них новые записи и складывает их в папку непрочитанных. При необходимости ходит по ссылкам и достаёт оттуда полезное содержимое по CSS selector. Дата обновления потока хранится в файле.
нельзя не упомянуть недостатки решения, с которыми я легко мирюсь. Нет подписки на PubSubHubbub, поэтому приходится подождать (максимум 15 минут — такая периодичность обновления). Нет веб-интерфейса для управления списком потоков. Если запись со вчерашней датой попадает в поток только сегодня (как это часто бывает у хабра, баша, и т.п.) она будет проигнорирована. При обновлении записи она может снова попасть в непрочитанные. Но всё это мелочи для меня.
в итоге уже где-то месяц я пользуюсь своим агрегатором, а с пятницы вообще перестал заглядывать в Google Reader.
любопытно, что гугл стимулирует меня отказываться от его продуктов в очень подходящие моменты: как раз когда мои навыки развиваются до нужного уровня, чтобы настроить или сделать альтернативу. Вначале была почта, потом интерфейс ридера, потом система рекомендаций ридера, потом фотогалерея, за ней агрегатор. А сейчас гугл собирается предать XMPP, так что будет у меня собственный джаббер-сервер, который я тоже давно хотел.
кнопки open google reader выглядят по-разному в обычной и мобильной версиях. В обычной на них есть текст, в мобильной только юникодные символы-иконки. Долгое время за выбор одного или другого отвечал яваскрипт, но тут я решил, что пора бы это перевести на CSS. В нём как раз есть удобное свойство content для того, чтобы подставлять нужный текст. И перевёл, и возрадовался.
потом, однако, я решил посмотреть, как оно выглядит в хроме и фаерфоксе: опера-то скоро сменит движок. И увидел я в этих браузерах пустые кнопки без содержимого. Какой интересный кроссбраузерный баг!… Когда же я стал разбираться, оказалось, что по спецификации этот самый content работает только на псевдоэлементах :before и :after. В общем, использовать его для текста кнопок не получится без дополнительного вложенного элемента. Пришлось его добавлять.
жаль, конечно, что это не сработало. Та же проблема, что и с элементом img: посредством CSS его пока что не изменишь.
в общем, это был ещё один пример того, как самым безглючным браузером кажется именно тот, в котором ведёшь разработку.
сегодня я попался в восхитительную ловушку, возникшую в результате сочетания двух безобидных на первый взгляд техник: наследования и оптимизации. Конечно, в минимальных примерах кода в этой записи всё будет более заметно, тем более, что вы знаете, что ловушка тут есть.
итак, пример прототипного наследования:
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 в локальную переменную неизвестно какая, вероятно, этого делать не имеет смысла.
я как-то прозевал появившиеся за последний год подходы к встраиванию метаданных в 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. Концептуально это мне очень нравится, хотя не могу не обратить внимания на недостатки: такой интерфейс сложнее исследовать браузером, и тип всё-таки нужно регистрировать. Но красиво.
три года назад я решил, что лучший микроблог — это статус в джаббере. Правда, у него был один недостаток: его было нетривиально вставлять в мой RSS-поток рекомендаций. Эта проблема легко решалась сторонним сервисом — у френдфида был джаббер-бот, который мог отправлять статусы во френдфид, а оттуда уже можно было вынуть RSS.
к сожалению, фейсбук купил команду френдфида, и тот потихоньку начал разлагаться. Бот не работает уже давно. Впрочем, технически задача очень проста: подключиться к серверу, ждать обновлений статуса, сохранять их в поток. К сожалению, на моём сервере оказываются только LTS-версии убунты, и предыдущая версия 10.04 не имела какой-то из нужных мне библиотек на питоне. Зато недавно я сервер обновил, и вскоре после этого смог завершить и запустить своего бота. Теперь мои статусы оказываются в специальном потоке.
когда всё уже готово, код кажется простым и очевидным. Для подключения к серверу достаточно создать и использовать такой класс:
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
после этого оставалось только научить программу на питоне вести себя, как демон, это тоже было довольно просто: