<?php
/**
* Unnamed tool to show overlap in subscribers and subscriptions for Laconica sites.
* http://statuses.org/tools/
* Copyright 2009, Robin Millette <robin@millette.info>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
====
TODO
====
1) overlap for single user (subcribers vs subscriptions) [in progress, missing text output]
2) group support (not to confuse with "support group")
3) best of, latest queries, etc.
4) support other instances (twit.tv, etc. Do I need to register at each of those sites to use that API??)
5) suggestions: you should subscribe to x, you should join y, etc.
6) add user names
*/
ini_set('user_agent', 'statuses.org');
require_once 'Cache/Lite/Function.php';
// the "cache" directory must be writable by the http process (often www-data)
define('CACHE_DIR', 'cache/');
// used to cache the source file
define('CACHE_LIFETIME', 604800); // 7 days
$app = new App;
class App {
var $api;
var $infos;
var $error;
var $log;
function __construct() {
$this->log = new OverlapLog;
if (isset($_REQUEST['source'])) {
$this->showSource(isset($_REQUEST['highlight']));
}
// this cleans up the supplied user names
$p = '|(\w+)|';
if (preg_match($p, $_REQUEST['user1'], $matches)) {
$this->user1 = $matches[0];
}
if (preg_match($p, $_REQUEST['user2'], $matches)) {
$this->user2 = $matches[0];
}
// a user account on identica is needed to use the API it seems
// although it works with no auth. on twitter
// so make sure you setup $api_username and $api_password
// with your account info and $root_url the server info
// like 'http://identi.ca' (no / at the end).
// It's never share or used to post anything
// it's only used to get subscribers and subscriptions for users
// We'll never ask for a user's password in the web interface because,
// well, that's just plain wrong.
// I stash $api_username and $api_password in config.php
// and make sure I never distribute that file.
// require_once 'config.php';
// $this->api = new TwitterApi($api_username, $api_password, $root_url);
$this->api = new TwitterApi;
if (0) {
if (isset($_REQUEST['server'])) {
switch ($_REQUEST['server']) {
case 'identica':
$root_url = 'http://identi.ca';
break;
case 'twit.tv':
$root_url = 'http://army.twit.tv';
break;
case 'bleeper.de':
$root_url = 'http://bleeper.de';
break;
}
}
}
// guess the user name if he comes from one of his pages on identi.ca
if (empty($this->user1) && empty($this->user2)) {
if (false !== ($user = $this->api->guessUsername($_SERVER['HTTP_REFERER']))) $this->user1 = $user;
return;
}
// we only have one submitted username
if (empty($this->user1) || empty($this->user2) || ($this->user1 === $this->user2)) {
$user = empty($this->user2) ? $this->user1 : $this->user2;
if (false === ($x1 = $this->api->call($user, true)))
$this->error = "Error reading $user's subscribers.";
elseif (false === ($x3 = $this->api->call($user, false)))
$this->error = "Error reading $user's subscriptions.";
else {
$this->infos = array($x1, $x3);
//update the db
$this->log->querySingle($user, count($x1), count($x3));
$this->log->hit($user, '');
}
return;
}
// reorder usernames, make sure user1 comes before user2 alphabetically
if ($this->user1 > $this->user2) {
$tmp = $this->user1;
$this->user1 = $this->user2;
$this->user2 = $tmp;
}
if (false === ($x1 = $this->api->call($this->user1, true)))
$this->error = "Error reading {$this->user1}'s subscribers.";
elseif (false === ($x2 = $this->api->call($this->user2, true)))
$this->error = "Error reading {$this->user2}'s subscribers.";
elseif (false === ($x3 = $this->api->call($this->user1, false)))
$this->error = "Error reading {$this->user1}'s subscriptions.";
elseif (false === ($x4 = $this->api->call($this->user2, false)))
$this->error = "Error reading {$this->user2}'s subscriptions.";
else {
$this->infos = array($x1, $x2, $x3, $x4);
$this->log->queryCompare($this->user1, $this->user2, count(array_intersect($x1, $x2)), count(array_intersect($x3, $x4)));
$this->log->hit($this->user1, $this->user2);
}
}
function getTopLine($o) {
$o[5] = date('r', $o[5]);
// 0 = user1
// 1 = user2
// 2 = subscribers
// 3 = subscriptions
// 4 = hits
// 5 = date
return "<p><a href='http://statuses.org/tools/overlap?user1={$o[0]}&user2={$o[1]}'>{$o[0]} and {$o[1]}</a> share {$o[2]} subscribers and {$o[3]} subscriptions.</p>";
}
function showTops() {
echo '<div id="tops">';
echo '<h2>Tops</h2>';
echo '<h3>Latest comparaisons</h3>';
foreach ($this->log->getLatestComparaisons() as $o) {
echo $this->getTopLine($o);
}
/*
echo '<h3>Latest profiles</h3>';
foreach ($this->log->getLatestProfiles() as $o) {
// $o[5] = date('r', $o[5]);
// echo '<p>' . join($o, ', ') . '</p>';
echo $this->getTopLine($o);
}
*/
echo '<h3>Top subscriber overlaps</h3>';
foreach ($this->log->getTopBySubscribers() as $o) {
echo $this->getTopLine($o);
}
echo '<h3>Top subscription overlaps</h3>';
foreach ($this->log->getTopBySubscriptions() as $o) {
echo $this->getTopLine($o);
}
echo '</div>';
}
// Create a lighter shade of a hex color:
// http://www.php.happycodings.com/Graphics/code8.html
function hexLighter($hex,$factor = 30) {
$new_hex = '';
$base['R'] = hexdec($hex{0}.$hex{1});
$base['G'] = hexdec($hex{2}.$hex{3});
$base['B'] = hexdec($hex{4}.$hex{5});
foreach ($base as $k => $v) {
$amount = 255 - $v;
$amount = $amount / 100;
$amount = round($amount * $factor);
$new_decimal = $v + $amount;
$new_hex_component = dechex($new_decimal);
if (strlen($new_hex_component) < 2) {
$new_hex_component = "0".$new_hex_component;
}
$new_hex .= $new_hex_component;
}
return $new_hex;
}
function getSingleChart($user, $x1, $x3) {
$overlap = array_intersect($x1, $x3);
$overlap_count = count($overlap);
$user_subscribers_count = count($x1);
$user_subscriptions_count = count($x3);
$chart_url = 'http://chart.apis.google.com/chart?cht=v&chs=240x75&chd=t:';
$chart_url .= "$user_subscribers_count,$user_subscriptions_count,0,$overlap_count";
$chart_url .= "&chdl=Subscribers%20($user_subscribers_count)|Subscriptions%20($user_subscriptions_count)";
$chart_url .= '&chdlp=r';
$chart_url .= '&chds=0,' . max($user_subscribers_count, $user_subscriptions_count);
$chart_url .= '&chco=' . $this->getColor($user) . ',' . $this->getColor($user, true);
$chart_url .= "&chtt=$user";
return "<div style='float: left; width: 250px'><a href='http://identi.ca/$user'><img style='border: none; width: 240px; height: 75px' src='$chart_url' alt='Subscribers vs subscriptions overlap for $user: $overlap_count' /></a><br />Chech out <a href='http://www.macno.org/denticator/?service=identi.ca&user=$user&chart=gchart'>$user on denticator</a></div>";
}
function getColor($user, $lighter = false) {
// $color = dechex(crc32($user) & 0x00ffffff);
$color = dechex(intval(crc32($user) / 256));
if ($lighter) return $this->hexLighter($color, 30);
return str_pad($color, 6, '0', STR_PAD_LEFT);
}
function info() {
if (!empty($this->error)) {
echo "<p id='error'>{$this->error}</p>";
return;
}
// this all should get refactored instead of duplicating most of the code
// for subscribers and subscriptions
if (empty($this->infos)) return;
if (2 === count($this->infos)) {
// single user
list($x1, $x3) = $this->infos;
$user = empty($this->user2) ? $this->user1 : $this->user2;
echo $this->getSingleChart($user, $x1, $x3);
echo 'See also with: ' . $this->log->getSeeWith($user);
return;
}
list($x1, $x2, $x3, $x4) = $this->infos;
$overlap_followers = array_intersect($x1, $x2);
$overlap_followers_count = count($overlap_followers);
$overlap_friends = array_intersect($x3, $x4);
$overlap_friends_count = count($overlap_friends);
$user1_count = count($x1);
$user1_percent = number_format(100 * $overlap_followers_count / $user1_count, 1);
$user1_count2 = count($x3);
$user1_percent2 = number_format(100 * $overlap_friends_count / $user1_count2, 1);
$user2_count = count($x2);
$user2_percent = number_format(100 * $overlap_followers_count / $user2_count, 1);
$user2_count2 = count($x4);
$user2_percent2 = number_format(100 * $overlap_friends_count / $user2_count2, 1);
$chart_url = $chart_url2 = 'http://chart.apis.google.com/chart?cht=v&chs=250x100&chd=t:';
$chart_url .= "$user1_count,$user2_count,0,$overlap_followers_count";
$chart_url .= "&chdl={$this->user1}%20($user1_count)|{$this->user2}%20($user2_count)";
$chart_url .= '&chdlp=r';
$chart_url .= '&chds=0,' . max($user1_count, $user2_count, $user1_count2, $user2_count2);
$chart_url .= '&chco=' . $this->getColor($this->user1) . ',' . $this->getColor($this->user2);
$chart_url .= '&chtt=Subscribers';
$chart_url2 .= "$user1_count2,$user2_count2,0,$overlap_friends_count";
$chart_url2 .= "&chdl={$this->user1}%20($user1_count2)|{$this->user2}%20($user2_count2)";
$chart_url2 .= '&chdlp=r';
$chart_url2 .= '&chds=0,' . max($user1_count2, $user2_count2, $user1_count2, $user2_count2);
$chart_url2 .= '&chco=' . $this->getColor($this->user1) . ',' . $this->getColor($this->user2);
$chart_url2 .= '&chtt=Subscriptions';
$chart_img = "<img style='width: 250px; height: 100px' src='$chart_url' alt='Subscriber overlap for {$this->user1} and {$this->user2}: $overlap_followers_count' />";
$chart_img2 = "<img style='width: 250px; height: 100px' src='$chart_url2' alt='Subscriptions overlap for {$this->user1} and {$this->user2}: $overlap_friends_count' />";
echo "<p>$chart_img $chart_img2<br />";
$root_name = $this->api->getRootName();
$root = $this->api->getRoot();
echo "The $root_name accounts <a href='$root/{$this->user1}'>@{$this->user1}</a> and <a href='$root/{$this->user2}'>@{$this->user2}</a> have <strong>$overlap_followers_count subscribers in common</strong>, which is $user1_percent% of @{$this->user1}'s $user1_count subscribers and $user2_percent% of @{$this->user2}'s $user2_count subscribers.<br />";
echo "They also have <strong>$overlap_friends_count subscriptions in common</strong>, which is $user1_percent2% of @{$this->user1}'s $user1_count2 subscriptions and $user2_percent2% of @{$this->user2}'s $user2_count2 subscriptions.</p>";
echo '<div>';
echo $this->getSingleChart($this->user1, $x1, $x3);
echo $this->getSingleChart($this->user2, $x2, $x4);
echo '</div>';
$dent = "#Overlap much? @{$this->user1} and @{$this->user2} have $overlap_followers_count subscribers and $overlap_friends_count subscriptions in common: http://statuses.org/tools/overlap?user1={$this->user1}&user2={$this->user2}";
$dent_ue = urlencode($dent);
echo "<a id='spread' href='$root/notice/new?status_textarea=$dent_ue'>Post the news to $root_name</a>.";
}
function showStats() {
$php_lc = count(file('overlap.php'));
$php_date = filemtime('overlap.php');
$update = date('r', $php_date);
echo<<<E_O_T
<div id='stats'>
<h2>Project statistics</h2>
<dl>
<dt>PHP line count:</dt>
<dd>$php_lc</dd>
<dt>Last update:</dt>
<dd>$update</dd>
</dl>
</div>
E_O_T;
}
function showSource($highlight = false) {
if ($highlight) {
echo '<div id="source">';
highlight_file('overlap.php');
echo '</div>';
} else {
header('Content-Type: text/plain; charset=UTF-8');
readfile('overlap.php');
die();
}
}
}
class TwitterApi {
var $api_root;
var $root_name;
var $root;
// var $username;
// var $password;
// some places are still hard-coded for identi.ca
// it's not enough to simple pass a $root argument
// function __construct($username, $password, $root) {
function __construct() {
$this->root = 'http://identi.ca';
$this->root_name = parse_url($root, PHP_URL_HOST);
$this->api_root = "$root/api";
// $this->username = $username;
// $this->password = $password;
}
function getRootName() {
return $this->root_name;
}
function getRoot() {
return $this->root;
}
function guessUsername($user) {
$root_q = quotemeta($this->root);
$p = "|^$root_q/(\w+)|";
if (preg_match($p, $user, $matches)) {
$user = $matches[1];
if (!in_array($user, array(
'tag',
'api',
'settings',
'main',
'doc',
'search',
'group',
'featured',
'favorited',
'rss'))) {
return $user;
}
}
return false;
}
function getFollowersIds($user, $root_name) {
return $this->call_imp('followers/ids', array('screen_name' => $user));
}
function getFriendsIds($user, $root_name) {
return $this->call_imp('friends/ids', array('screen_name' => $user));
}
function call_imp($method, $args) {
/*
$context = stream_context_create(array(
'http' => array(
'method' => 'POST',
'header' => sprintf(
"Authorization: Basic %s\r\n", base64_encode($this->username . ':' . $this->password)) .
"Content-type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query($args),
'timeout' => 5)));
$ret = @file_get_contents("{$this->api_root}$method.json", false, $context);
*/
// $ret = file_get_contents("{$this->api_root}$method.json");
$ret = file_get_contents("http://identi.ca/api/$method/{$args['screen_name']}.json");
if (false === $ret) return false;
return json_decode($ret);
}
function call($user, $followers) {
$options = array('cacheDir' => CACHE_DIR, 'lifeTime' => CACHE_LIFETIME);
$cache = new Cache_Lite_Function($options);
if ($followers) return $cache->call(array($this, 'getFollowersIds'), $user, $this->root_name);
return $cache->call(array($this, 'getFriendsIds'), $user, $this->root_name);
}
}
class OverlapLog {
var $pdo;
function __construct() {
$fn = 'overlap.sqlite';
$dsn = "sqlite:./$fn";
$this->pdo = new PDO($dsn);
if (!file_exists($fn) || (0 === filesize($fn))) {
$query = 'CREATE TABLE queries (user1 not null, user2 not null default "", subscribers, subscriptions, hits not null default 1, date, constraint user_combo unique (user1, user2))';
$this->pdo->query($query);
}
}
function getSeeWith($user) {
$names = array();
foreach ($this->pdo->query("select user1, user2, subscribers from queries where user1 = '$user' or user2 = '$user' order by round(subscribers) desc limit 5", PDO::FETCH_ASSOC) as $row) {
if ($user === $row['user1']) {
if (!($n = $row['user2'])) continue;
$n2 = $row['user1'];
} else {
if (!($n = $row['user1'])) continue;
$n2 = $row['user2'];
}
$names[] = "<a href='http://statuses.org/tools/overlap?user1=$n&user2=$n2'>$n ({$row['subscribers']})</a>";
}
return join($names, ', ');
}
function getLatestComparaisons() {
$query = 'select * from queries where user2 != "" order by date desc limit 5';
return $this->pdo->query($query, PDO::FETCH_NUM);
}
function getLatestProfiles() {
$query = 'select * from queries where user2 = "" order by date desc limit 5';
return $this->pdo->query($query, PDO::FETCH_NUM);
}
function getTopBySubscribers() {
$query = 'select * from queries where user2 != "" order by round(subscribers) desc limit 5';
return $this->pdo->query($query, PDO::FETCH_NUM);
}
function hit($user1, $user2) {
$stmt = $this->pdo->prepare('update queries set hits = hits + 1 where user1 = ? and user2 = ?');
$stmt->execute(array($user1, $user2));
}
function getTopBySubscriptions() {
$query = 'select * from queries where user2 != "" order by round(subscriptions) desc limit 5';
return $this->pdo->query($query, PDO::FETCH_NUM);
}
function querySingle($user1, $subscribers, $subscriptions) {
$date = time();
$stmt = $this->pdo->prepare('insert into queries (user1, subscribers, subscriptions, date) values (?, ?, ?, ?)');
return $stmt->execute(array($user1, $subscribers, $subscriptions, $date));
}
function queryCompare($user1, $user2, $subscribers, $subscriptions) {
$date = time();
$stmt = $this->pdo->prepare('insert into queries (user1, user2, subscribers, subscriptions, date) values (?, ?, ?, ?, ?)');
return $stmt->execute(array($user1, $user2, $subscribers, $subscriptions, $date));
}
}
?>
<html>
<head>
<title>identica overlap</title>
<style>
body {
width: 90%;
margin: 1em auto 0;
text-align: center;
}
form {
text-align: center;
width: 60%;
padding: 1em;
margin: 0 auto;
}
address {
font-style: italic;
margin-top: 2em;
border-top: thin black solid;
text-align: center;
}
h1 {
text-align: center;
}
#tops {
border-left: thin dashed gray;
padding-left: 1em;
float: right;
width: 21em;
text-align: right;
font-size: 85%;
}
#tops p {
padding-top: 0;
margin-top: 0;
}
#tops h2, #tops h3 {
padding-bottom: 0;
margin-bottom: 0;
}
#stats {
text-align: left;
float: left;
margin-right: 2em;
width: 20em;
background: black;
color: white;
padding: 0.5em;
}
#source {
text-align: left;
}
#error {
background: red;
}
#spread {
padding: 0.5em;
width: 5.5em;
margin: 0 auto;
text-align: center;
font-size: 150%;
background: #8f8;
color: #888;
display: block;
clear: both;
border: groove green thick;
}
</style>
</head>
<body>
<h1><a href='http://statuses.org/tools/overlap'>identica overlap</a></h1>
<?php $app->showTops() ?>
<form action='http://statuses.org/tools/overlap'>
<fieldset>
Give me two Laconica or Status.net accounts<br />
<!-- select name='server'>
<option>DOESN'T WORK</option>
<option>identica</option>
<option>twit.tv</option>
<option>bleeper.de</option>
</select -->
<input type='text' name='user1' value='<?php echo $app->user1 ?>' /> <input type='text' name='user2' value='<?php echo $app->user2 ?>' /><br />
<input type='submit' />
</fieldset>
</form>
<?php $app->info() ?>
<?php $app->showStats() ?>
<p>Le code est sous licence <a href='COPYING'>AGPLv3</a>. On peut consulter la <a href='overlap?source=php'>classe PHP</a> (<a href='overlap?source=php&highlight'>highlight</a>).</p>
<address>Blatantly ripped off from <a href='http://meyerweb.com/eric/tools/followerlap/'>Followerlap by Eric Meyer</a> and <a href='http://overlapr.com/'>Overlapr by Dan Benjamin</a>. Thanks to <a href='http://identi.ca/notice/6144744'>mjog</a> for the tip :)<br />
I am <a href='http://rym.waglo.com/'>Robin Millette</a> and <a href='http://identi.ca/millette'>millette on identica</a>.</address>
</body>
</html>
dothidden and rms share 0 subscribers and 0 subscriptions.
dothidden and mattl share 0 subscribers and 0 subscriptions.
evan and lanu share 87 subscribers and 16 subscriptions.
csickendieck and lanu share 124 subscribers and 50 subscriptions.
csickendieck and m1 share 101 subscribers and 66 subscriptions.
fabsh and methoddan share 880 subscribers and 253 subscriptions.
evan and millette share 472 subscribers and 446 subscriptions.
dantheman and fabsh share 471 subscribers and 203 subscriptions.
evan and rkj share 392 subscribers and 181 subscriptions.
evan and exador23 share 383 subscribers and 172 subscriptions.
evan and millette share 472 subscribers and 446 subscriptions.
papag and scobleizer share 219 subscribers and 424 subscriptions.
evan and ronkjeffries share 333 subscribers and 341 subscriptions.
fabsh and methoddan share 880 subscribers and 253 subscriptions.
dantheman and fabsh share 471 subscribers and 203 subscriptions.
Le code est sous licence AGPLv3. On peut consulter la classe PHP (highlight).
Blatantly ripped off from Followerlap by Eric Meyer and Overlapr by Dan Benjamin. Thanks to mjog for the tip :)