* * 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 . */ /* ==== 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 "

{$o[0]} and {$o[1]} share {$o[2]} subscribers and {$o[3]} subscriptions.

"; } function showTops() { echo '
'; echo '

Tops

'; echo '

Latest comparaisons

'; foreach ($this->log->getLatestComparaisons() as $o) { echo $this->getTopLine($o); } /* echo '

Latest profiles

'; foreach ($this->log->getLatestProfiles() as $o) { // $o[5] = date('r', $o[5]); // echo '

' . join($o, ', ') . '

'; echo $this->getTopLine($o); } */ echo '

Top subscriber overlaps

'; foreach ($this->log->getTopBySubscribers() as $o) { echo $this->getTopLine($o); } echo '

Top subscription overlaps

'; foreach ($this->log->getTopBySubscriptions() as $o) { echo $this->getTopLine($o); } echo '
'; } // 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 "
Subscribers vs subscriptions overlap for $user: $overlap_count
Chech out $user on denticator
"; } 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 "

{$this->error}

"; 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 = "Subscriber overlap for {$this->user1} and {$this->user2}: $overlap_followers_count"; $chart_img2 = "Subscriptions overlap for {$this->user1} and {$this->user2}: $overlap_friends_count"; echo "

$chart_img $chart_img2
"; $root_name = $this->api->getRootName(); $root = $this->api->getRoot(); echo "The $root_name accounts @{$this->user1} and @{$this->user2} have $overlap_followers_count subscribers in common, which is $user1_percent% of @{$this->user1}'s $user1_count subscribers and $user2_percent% of @{$this->user2}'s $user2_count subscribers.
"; echo "They also have $overlap_friends_count subscriptions in common, which is $user1_percent2% of @{$this->user1}'s $user1_count2 subscriptions and $user2_percent2% of @{$this->user2}'s $user2_count2 subscriptions.

"; echo '
'; echo $this->getSingleChart($this->user1, $x1, $x3); echo $this->getSingleChart($this->user2, $x2, $x4); echo '
'; $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 "Post the news to $root_name."; } function showStats() { $php_lc = count(file('overlap.php')); $php_date = filemtime('overlap.php'); $update = date('r', $php_date); echo<<

Project statistics

PHP line count:
$php_lc
Last update:
$update
E_O_T; } function showSource($highlight = false) { if ($highlight) { echo '
'; highlight_file('overlap.php'); echo '
'; } 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[] = "$n ({$row['subscribers']})"; } 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)); } } ?> identica overlap

identica overlap

showTops() ?>
Give me two Laconica or Status.net accounts

info() ?> showStats() ?>

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 :)
I am Robin Millette and millette on identica.