<?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($usertrue)))
                
$this->error "Error reading $user's subscribers.";
            elseif (
false === ($x3 $this->api->call($userfalse)))
                
$this->error "Error reading $user's subscriptions.";
            else {
                
$this->infos = array($x1$x3);
                
//update the db
                
$this->log->querySingle($usercount($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->user1true)))
            
$this->error "Error reading {$this->user1}'s subscribers.";
        elseif (
false === ($x2 $this->api->call($this->user2true)))
            
$this->error "Error reading {$this->user2}'s subscribers.";
        elseif (
false === ($x3 $this->api->call($this->user1false)))
            
$this->error "Error reading {$this->user1}'s subscriptions.";
        elseif (
false === ($x4 $this->api->call($this->user2false)))
            
$this->error "Error reading {$this->user2}'s subscriptions.";
        else {
            
$this->infos = array($x1$x2$x3$x4);
            
$this->log->queryCompare($this->user1$this->user2count(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]}&amp;user2={$o[1]}'>{$o[0]} and {$o[1]}</a> share {$o[2]}&nbsp;subscribers and {$o[3]}&nbsp;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($usertrue);
        
$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&amp;user=$user&amp;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($color30);
        return 
str_pad($color6'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 (
=== 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_count1);
        
$user1_count2 count($x3);
        
$user1_percent2 number_format(100 $overlap_friends_count $user1_count21);

        
$user2_count count($x2);
        
$user2_percent number_format(100 $overlap_followers_count $user2_count1);
        
$user2_count2 count($x4);
        
$user2_percent2 number_format(100 $overlap_friends_count $user2_count21);

        
$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($rootPHP_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) || (=== 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&amp;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($queryPDO::FETCH_NUM);
    }

    function 
getLatestProfiles() {
        
$query 'select * from queries where user2 = "" order by date desc limit 5';
        return 
$this->pdo->query($queryPDO::FETCH_NUM);

    }

    function 
getTopBySubscribers() {
        
$query 'select * from queries where user2 != "" order by round(subscribers) desc limit 5';
        return 
$this->pdo->query($queryPDO::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($queryPDO::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&amp;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>
identica overlap

identica overlap

Tops

Latest comparaisons

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.

Top subscriber overlaps

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.

Top subscription overlaps

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.

Give me two Laconica or Status.net accounts

Project statistics

PHP line count:
586
Last update:
Thu, 14 Jan 2010 14:45:59 -0500

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.