В этой статье я расскажу о своем опыте разработки кросспостинга из моего Facebook в мой Livejournal (далее – ЖЖ), а также поделюсь исходными текстами, готовыми к старту на ваших аккаунтах.
Причиной написания скриптов было получение возможности поиска по своим записям – возможности, которую Facebook никак не может запустить как часть своего сервиса, а также «оживление» своего ЖЖ. Поскольку доступ к любым постам в Фейсбуке требует обязательной авторизации, поисковых роботов сервис, очевидно, не пускает. Конкретно в моем случае это неудобно: ссылки, видео и мысли, которые я публикую в соцсети, зачастую я публикую «на будущее» – и часто настает тот момент, когда эта информация становится необходимой, но ее уже практически не найти.
Также с использованием опубликованных здесь скриптов удалось перенести архив существующих записей Facebook: более 2000 архивных сообщений моего Facebook перешли в ЖЖ с соответствующими датами. То есть, если у вас еще не было ЖЖ, его можно сразу наполнить информацией за все время.
Также в статье выкладываю готовые скрипты на Perl, с использованием которых можно транслировать статусы Facebook в Livejournal, а оттуда, при наличии соответствующих настроек, в Вконтакте, Twitter и RSS, а с использованием дополнительных веб-сервисов – практически во все блог-движки.
Преамбула
Так получилось, что я два года назад ушел из ЖЖ в Facebook. Это было связано с кучей проблем в самом ЖЖ, заторможенность с развитием сервиса, и, как следствие, массового ухода оттуда моих друзей в сине-белую соцсеть.
При этом ЖЖ остается единственной открытой блог-площадкой с гибким форматом постов, не накладывающей существенных технических ограничений – на на размер поста или ни на его оформление. В ЖЖ до сих пор остается масса интересных личностей, из которых много моих друзей.
Очень большое преимущество ЖЖ было в его открытости поисковым системам. То, что я написал в блог, я могу потом найти через «поисковики», причем по практике индекс Яндекса или Гугла обновляется спустя несколько минут от публикации поста. Понимание того, что твоя статья, заметка или находка может быть полезна не только твоим друзьям, неплохо мотивирует на то, чтобы туда это полезное писать еще и еще. Впрочем, такая же логика работает в отношении Хабры.
Я изучил сервисы кросспостинга, позволяющие автоматически постить сообщение в несколько соцсетей. К сожалению, ничего толкового из них не нашлось. Почти везде принуждают писать пост в специальном интерфейсе, либо в качестве основного места используется ЖЖ, что лично для меня неудобно. Самым любопытным сервисом оказался стартап IFTTT.com, позволяющий создавать универсальные правила вида “пояивилось в твиттере – отправь на смс”, “появилось в фейсбуке – отправь в твиттер”, “собирается дождь – предупреди по смс” и т.д. Да, его можно приспособить для некоторых нужд, но из фейсбука в ЖЖ он все равно постить не умеет. Во всех сервисах кросспостинга есть один большой недостаток – они слишком универсальны и плохо настраиваемы, да и редкие из них включают популярные российские соцсети. Тот же ЖЖ в списке встречается крайне редко. И если получить пост в ЖЖ можно через RSS, то заслать его можно туда только через API.
Есть и еще одна причина. Я хочу иметь возможность управлять тем, что куда постится, в зависимости от контента. Например, я могу посчитать нужным автоматически публиковать в ЖЖ фотографию, а не ссылку на нее. Или в твиттер переносить целиком пост, а не ссылку на него, как это делают многие сервисы. Для этого я должен иметь возможность поправить скрипт самому, под свои нужды.
В итоге, стояла цель охватить соцсети Twitter, ЖЖ, Facebook и Вконтакте, оставив Facebook «стартовой площадкой» для поста. Поскольку сам ЖЖ умеет постить в Вконтакте и в Твиттер, а также экспортировать посты в RSS для интеграции с Drupal, нарисовалась следующая схема:
Про API
Facebook имеет, на мой взгляд, самое шикарное API. Этот интерфейс позволяет делать что угодно с данными, которые вы вносите в соцсеть – получать их в структурированном виде, изменять или удалять через массу удобных механизмов, FQL, HTTP-запросы. И на этом фоне удивило относительно небольшое число сторонних приложений, работающих с Facebook.
Кроме использования API я также пробовал парсинг страниц упрощенной мобильной версии Facebook-а – это позволяло вытащить больше информации, чем дает API. В некоторых случаях это довольно полезный механизм. В данном случае удалось вполне обойтись стандартными возможностями.
По библиотекам для Perl все оказалось прекрасно: для Perl на CPAN обнаружилось несколько модулей, реализующих работу с Facebook, но из-за простоты протокола нужды в них мало. Данные запроса передаются через URL, результат возвращается в JSON. Что касается ЖЖ, то он имеет несколько разных API, из которых простейшим является LJ XML-RPC. Я воспользовался готовым модулем для Perl, реализующим довольно стабильную работу с ЖЖ – LJ::Simple.
Особенности кросспостинга из Facebook
Доступ к фейсбуку осуществляется через access token, получаемый внешним приложением на ограниченное время с ограниченными правами. Время протухания сессии и полученного access_token измеряется от 2 до 25 часов. Есть возможность получить long-live token со временем жизни до 60 дней. По логике, нужно обновлять access_token после протухания каждый раз – через 5 дней это случится или через 60. В приведенных скриптах автоматическое обновление не предусмотрено, как и уведомление о том, что протух access token.
Вторая особенность заключается в том, что ваши сообщения доступны на стене вместе с сообщениями других юзеров, и если не задуматься о том, что кросспостится должны только ваши, у других пользователей может появиться возможность опубликовать у вас в ЖЖ через фейсбук что-нибудь не то. По умолчанию в Facebook у всех ваших друзей есть возможность публиковать вам на стену что угодно, особенно радует возможность получить тучу комментариев от друзей к чужой фотографии, на которой вас «отметили». Если не включить модерирование чужих сообщений на стену, то с появлением кросспостинга эти фото уйдут еще и за пределы фейсбука.
Некоторые посты в фейсбуке являются «неинформативными» – как некоторые сообщения, отправляемые приложениями. Кросспостер умеет их фильтровать и не переносить в Livejournal, но тут каждый настраивает под себя, конечно. Какие-то посты специально принимают другой вид: например, фотография превращается из ссылки на Facebook во встроенную в пост на ЖЖ крупную фото.
Также нужно обязательно учесть необходимость простановки флага “backdate” при публикации поста задним числом на ЖЖ. Для этого есть специальная константа в начале скрипта. Особенность заключается в том, что если вы публикуете пост в ЖЖ за март 2011 года, то в ленту друзей он помещается как свежий (хоть и со старой датой), а при связке с твиттером, публикуется и в него как свежий. Если же поставить в интерфейсе специальную галочку, или же установить свойство backdate через API, то из ленты друзей он исключается. Для переноса архива устанавливать backdate – обязательное дело, потом его можно отключить.
Ну и нужно убрать связь с Facebook, иначе получится замкнутый цикл (в скрипте на всякий случай стоит защита).
Что получилось
Сюда я публикую скрипты, разработанные в процессе исследования, эдакого proof of concept. Также они адаптированы для публикации в составе статьи – например, общие параметры подключения к БД не вынесены в отдельные файлы, код не разбит по функциям и файлам, убраны неважные и т.д.
Архитектура кросспостера предполагает двухэтапную работу: сохранение промежуточных результатов в БД и экспорт записей из БД в ЖЖ. В дальнейшем эту БД можно использовать как самостоятельную базу, также имея локальные данные легко дописывать скрипты для экспорта в другие социальные сети, RSS.
Для сохранения в БД используется скрипт facebook.pl. Его назначение – получить страницу со стены с Facebook и, в случае, если есть следующая страница, выдать ее идентификатор, если нет, выдать «all done». Идентификатор является параметром скрипта, так что для загрузки следующей страницы нужно вызвать скрипт с этим идентификатором и т.д., пока в ответ не получим all done.
Обратите внимание, что для работы facebook.pl нужно изменить параметры подключения к БД, идентификатор вашего журнала в ЖЖ, а также ввести access_token. Для отладки сформировать короткоживущий access token можно в Facebook Graph API Explorer. Для того, чтобы получить доступ на 60 дней, нужно создать приложение, получить AppId и SecretId, после сформировать access token по приведенной ссылке, выбрав это приложение из ниспадающего списка. Обратите внимание на перечень прав – недостаток некоторых галочек может ограничить доступ к записям на вашей стене: например, внешнему приложению перестанут быть видны reshares от других пользователей или фотографии или что-то другое. Если не боитесь в скриптах оставлять лишние доступы, лучше поместить вообще все галочки.
Для постинга на ЖЖ из БД используется скрипт update_lj.pl. В этой статье используется его отладочная версия – он берет из базы один вакантный пост, подготовленный ранее facebook.pl, отправляет его на ЖЖ, возвращает идентификатор страницы на ЖЖ, помечает пост как отправленный. Это промежуточная версия, и я здесь оставляю именно ее, потому что в случае каких-либо проблем удалять из ЖЖ созданные скриптом посты массово очень и очень неудобно.
На тот случай, если все-таки ЖЖ пополнился кучей автоматически созданных ошибочных постов, выполнить выборочное редактирование, удаление или изменение свойств можно скриптом lj_change.pl, приведенным в конце поста.
В итоге, для переноса архива необходимо пройтись по всем страницам bash-скриптом, вызывающим нужное число раз facebook.pl, после чего выполнить updatelj.pl столько раз, сколько у вас в базе получилось записей. Для регулярного обновления facebook.pl достаточно раз в час или раз в сутки вызывать по крону, после подобным bash-скриптом вызывать updatelj.pl.
Буду рад любым комментариям и дополнениям, а также тем энтузиастам, которые захотят или смогут сделать из этого внешний универсальный сервис.
CREATE TABLE `myposts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ctime` datetime DEFAULT NULL, // время создания
`message` text, // собственно пост
`link` text, // ссылка к посту
`picture_fb` text, // картинка к посту
`posted_to_lj` int(11) DEFAULT NULL, // было ли отправлено на ЖЖ?
`lj_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // время отправки в ЖЖ
`lj_item_id` int(11) DEFAULT NULL, // идентификатор в ЖЖ (внутренний)
`lj_anum` int(11) DEFAULT NULL, // константа для журнала, связывающая item_id и html_id
`lj_html_id` int(11) DEFAULT NULL, // идентификатор в ЖЖ (внешний, тот, что перед .html)
`user` varchar(50) DEFAULT NULL, // пользователь (для использования базы для нескольких пользователей)
PRIMARY KEY (`id`)
)
#!/bin/bash
#для создания архива
NEXT=`./facebook.pl`;
echo $NEXT;
while [ "$NEXT" != "all done" ]
do
NEXT=`$NEXT`;
echo $NEXT;
done
#!/usr/bin/perl
use open qw(:std :utf8);
use LWP::Simple;
use YAML::Tiny;
use JSON;
use URI;
use DBI;
use DBD::MySQL;
$DB_LOGIN = "========DB-USER==========";
$DB_PASS = "========DB-PASS==========";
$DATABASE= "======== DB-NAME ==========";
$USER = '========YOUR-LJ-USER==========';
my $access_token = '========YOUR-ACCESS-TOKEN-SEE-GRAPH.FACEBOOK.COM-FOR-THE-DETAILS==========';
# ARGV[0] is the value which facebook places into its next page URL as the value of its "until" CGI parameter
$until = $ARGV[0];
my $dbh=DBI->connect("DBI:mysql:database=mysql;host=localhost", $DB_LOGIN, $DB_PASS) || die "Error connecting to database: $!n";
$dbh->do("use $DATABASE;");
# next four lines are for facebook graph api request
my $uri = new URI('https://graph.facebook.com/me/feed' );
$uri->query_form( {access_token => $access_token, until => $until, base_amount=>1, value=>1 });
my $resp = get("$uri");
$resp = defined $resp ? decode_json($resp) : undef;
$next = $resp->{paging}->{next};
$next =~ /until=(.+)$/;
$until = $1;
# as a result of the script I've decided to show either all done message or bash command for the next iteration. It looks a bit queer…
if ($until != "") {
print "./facebook.pl ".$until. "n";
} else
{
print "all done";
}
# updating database
for my $post (@{ $resp->{data} }) {
$ctime = $post->{created_time};
$ctime =~ /(dddd)-(dd)-(dd).(dd):(dd):(dd)/;
($y,$m,$d,$h,$i,$s) = ($1, $2, $3, $4, $5, $6);
$time = "$h:$i:$s";
$sqltime = $y.$m.$d.$h.$i.$s;
$message = $post->{message};
$link = $post->{link};
$picture = $post->{picture};
$sql = "select * from myposts where user='$USER' and ctime='$sqltime'";
$sth = $dbh->prepare($sql);
$sth->execute;
if ($sth->rows == 0) {
$sql = "insert into myposts set user='$USER', link=".$dbh->quote( $link ).", picture_fb=". $dbh->quote( $picture ) .", message=". $dbh->quote( $message).", ctime='$sqltime'";
# the following line is intended for filtering twitter-like posts. I've decided to skip them because the majority of them are the the links to the existing facebook or livejournal posts.
if ($message !~ /t.co//) {
$dbh->do($sql);
}
}
}
#!/usr/bin/perl
use LJ::Simple;
use Date::Manip;
use DBI;
use DBD::MySQL;
$USER = '========YOUR-LJ-USER==========';
$DB_LOGIN = "========DB-USER==========";
$DB_PASS = "========DB-PASS==========";
$LJ_NAME = "========LJ-USER==========";
$LJ_PASS = "========LJ-PASS==========";
$DATABASE = 'facebook';
$DEBUG = 1;
# it's extremely important to set the following constant to "1"
# if you've decided to migrate all the old records using this script.
$HIDE_FROM_FRIENDS_WALLS = 0;
my $dbh=DBI->connect(
"DBI:mysql:database=mysql;host=localhost",
$DB_LOGIN,
$DB_PASS,
) || die "Error connecting to database: $!n";
my $lj = new LJ::Simple ({
user => $LJ_NAME,
pass => $LJ_PASS,
site => "livejournal.com:80",
});
(defined $lj)
|| die "$0: Failed to log into LiveJournal: $LJ::Simple::errorn";
$sql = "select ctime, UNIX_TIMESTAMP(ctime), link, message, picture_fb, id from myposts where user='$USER' and lj_html_id is NULL order by ctime desc limit 0,1;";
$dbh->do("use $DATABASE;");
@row_ary = $dbh->selectrow_array($sql);
if ($row_ary[0] == "") { exit; }
($ctime, $ctime_ts, $link, $message, $picture_fb, $id ) = @row_ary;
$message =~ s/n/<br><br>/g; # preparing CRs for html
if ($DEBUG) { print $message."n"; }
# building the subject from the text of the post
$messagelength = length($message);
if ($messagelength > 50) {
$i = index($message." ", " ");
do {
$j = $i;
$i = index($message." ", " ", $i+1);
} while ($i < 50);
$subject = (length($message)>50) ? substr($message,0,$j)."…" : $message;
if ($DEBUG) { print "posting $subject...n"; }
my %Entry=(); $lj->NewEntry(%Entry) || die "$0: Failed to create new entry: $LJ::Simple::errorn";
# croppedlink
$croppedlink = (length($link) > 50 ?
(substr($link, 0, 50)."…")
:
$link) ;
# replacing the small picture with the big one
if ($picture_fb =~ /https://fbcdn/) {
$largepicture = $picture_fb;
$largepicture =~ s/_s/_n/g;
$entry="<a href='$link'>". $croppedlink . "</a><br>" .
$message .
"<br><img src='$largepicture'>";
} else {
if ($link ne "")
{
$entry="<table><tr><td><img src="". $picture_fb. "" align=left></td><td><a href='". $link. "'>".
$croppedlink .
"</a><br>$message<br clear=all></td></tr></table>";
} else
{
$entry="$messagenn<a href='$link'>$link</a><br><img src="$picture_fb">";
}
}
$lj->SetEntry (%Entry, $entry) || die "$0: Failed to prepare new post - $LJ::Simple::errorn";
$lj->SetSubject(%Entry, $subject);
$lj->SetDate(%Entry, $ctime_ts);
if ($HIDE_FROM_FRIENDS_WALLS) { $lj->Setprop_backdate(%Entry,1);}
my ($item_id,$anum,$html_id)=$lj->PostEntry(%Entry);
(defined $item_id)
|| die "$0: Failed to post journal entry: $LJ::Simple::errorn";
if ($DEBUG) { print "created item_id: ". $item_id.", anum: ".$anum.",html_id: ".$html_id."n"; }
$sql = "update myposts set user='$USER', lj_ts=now(), lj_item_id = '$item_id', lj_anum = '$anum', lj_html_id = '".$html_id."' where id=".$id;
$dbh->do($sql);
!/usr/bin/perl
use Data::Dumper;
use POSIX;
use LJ::Simple;
use Time::Local;
use DBI;
use DBD::MySQL;
$LJ_LOGIN = "===========LJ-LOGIN============";
$LJ_PASS = "===========LJ-PASS============";
$DB_LOGIN = "===========DB-LOGIN============";
$DB_PASS = "===========DB-PASS============";
$DATABASE = "===========DATABASE============";
$operation= "..."; #setbackdate, purge …
my $dbh=DBI->connect(
"DBI:mysql:database=mysql;host=localhost",
$DB_LOGIN,
$DB_PASS,
) || die "Error connecting to database: $!n";
$dbh->do("use $DATABASE;;");
$sql = "select lj_item_id from myposts where lj_html_id is not NULL order by ctime desc;";
$results = $dbh->selectall_hashref($sql, 'lj_item_id');
foreach my $id (keys %$results) {
#$id2 = $results->{lj_item_id};
push @ids, $id;
}
my $lj = new LJ::Simple ({
user => $LJ_LOGIN,
pass => $LJ_PASS,
site => undef,
proxy => undef,
});
(defined $lj)
|| die "$0: Failed to log into LiveJournal: $LJ::Simple::errorn";
print "logged on...n";
my %Entries=();
for ( @ids ) {
print "requesting entry $_...n";
(defined $lj->GetEntries(%Entries,undef,"one", $_)) or print "$0: Failed to get entries - $LJ::Simple::errorn";
my $item = $Entries{$_};
if ($operation == 'setbackdate') {
$lj->Setprop_backdate($item, 1) or print "$0: Failed to set back date property - $LJ::Simple::errorn";
$lj->EditEntry($item) or print "$0: Failed to edit entry - $LJ::Simple::errorn";
}
if ($operation == 'purge') {
$lj->EditEntry($item, undef) ;
}
$sql = "update myposts set ...";
$dbh->do($sql);
}
print "done.n";
exit(0);
P.S. К слову сказать, подсветка синтаксиса средствами хабраредактора работает из рук вон плохо. В первом скрипте подсветка работает до строки с подключением к БД (DBI->connect): стоит ей появиться, как тег source перестает выделять синтаксис Perl-а. Во втором, вероятно, тоже на ней сбоит. Пришлось делать подсветку через тег font.
Автор: raliev