# /PROMCHANS <no parameters>
#
# List channels that are visited by the users from the active channel
#
# PURPOSE:
# 1. discover interesting channels,
# e.g., partner and sub-channels, channels visited by people like you,
# overlooked channels on your topics
# 2. discover groups/milieus in the active channel,
# e.g., developers, political parties, linux geeks;
# @TODO why? identify the usual suspects?
#
# EXAMPLE OUTPUT:
# Prominent channels in here:
# 56% 30 #ccc 20% 13 #chaosradio 7% 6 #haskell #c 5 #java #erlang
# 4% 4 #26c3 3 #ak-vorrat 1% 2 #berlin #flirt #gentoo #openoffice #scribus
#
# screenshot:
# http://img37.imageshack.us/img37/5782/screenshot2009122900034.png
#
# ISSUES:
# - performs very slowly: 36 users->1 minute, 96->4, 300->11
# - bombing an IRC server with hundreds of WHOIS requests isn't very ethical
# anyway: #freenode staff said, those can be answered with local
# information (non-remote whois) and should be fairly cheap
# - starting a new /promchans while another one is running -> UB?
# - doing simple /whois while /promchans is running -> UB?
#
# ASSUMES:
# - there is a vast number of channels (24K on freenode)
# - users (collaboratively) explore these channels over time
# (55K-60K users on freenode)
# - users usually don't stay in waste channels
# -> every user's channel list (WHOIS) is a sort of recommendation:
# "these channels are worth my time and attention"
# - if several users visit the same channel, perhaps it's worth the visit
# -> resilient "recommendations" emerge from aggregated channel lists:
# the more the better
# - you're in a channel together with people who probably share your
# interests or mindset
# - they joined channels that might interest you too
# -> their aggregated channel lists somehow fit to your interests
# - the user composition in your channel is the bias here
# => serendipity
# - diversity in the results: #language -> tools, libs, projects,
# other languages, aspects (security, math, electronics),
# culture (#26c3, #startups)
# - neither the number of visitors nor the ratio is sufficient info:
# "75 users join #example is pretty much, even the majority here" (no, just 6%)
# "6% users join #example is moderate - maybe 8 users..." (no, 75!)
# -> we need both values: "75 or 6% of all users join #example"
# - ...
# - a present WHOIS service
# - a significant number of users not hiding their channel list
#
# REALITY:
# - the most interesting channels were listed in the mid or lower range
# of counted visitors; top range offers already known channels or
# partner/sub-chans
#
# OPERATION BREAKDOWN:
# 1. get nicks in active channel
# 2. WHOIS every nick for his channel list
# 3. sum up number of visitors of each channel
# 4. sort channels by their number of visitors in descending order
# 5. output channels
#
# TODO:
# - rule out duplicate nicknames, e.g., nickname and nickname_
# - improved error handling
# - output estimated runtime as "at 20:43" instead of "in 5 minutes"
# - estimate runtime on the basis of the actual performance
# if formular doesn't work for other
# - dark grey falsely appears bold
# - output number of users in every prominent channel
# - output to status window instead of the active channel? (convenience)
# - display percent /and/ number of visitors?
# - add [#channel] or [#channels ...] parameter
# - consider user's chat time?
#
# Perl v5.8.8
use strict;
use vars qw($VERSION %IRSSI);
use Irssi;
use Irssi qw(command_bind signal_add signal_remove);
$VERSION = '0.9';
%IRSSI = (authors => '',
contact => '',
url => '',
name => 'Promchans',
description => 'List channels that are visited by the users from the active channel',
created => '2009-12-10',
license => 'Public Domain',);
# globals (smells are OK with small irssi scripts)
{
our %num_visitors; # per channel name (key)
our $my_channel; # active Channel-object
our $num_users; # in active channel
our $num_awaited_users = 0; # burn down
}
# @param Channel object
# @return bool true if error
#
sub print_unsatisfied_preconditions
{
my ($channel) = @_;
if (! $channel
|| ref($channel) ne 'Irssi::Irc::Channel'
|| $channel->{'type'} ne 'CHANNEL')
{
print MSGS 'Call /PROMCHANS in a channel';
return 1;
}
if (! $channel->{'names_got'})
{
print MSGS 'Wait until Irssi received all names, then retry';
return 1;
}
return 0;
}
# @param Channel object
# @param int number of users
# @return int
#
sub print_runtime_estimate
{
my ($channel, $num_users) = @_;
# runtime is O(n)
# spreadsheet 'promchans.ods' computes slope and intercept from samples;
#
my $minutes = int(0.5 + (0.04 * $num_users) + 0.03); # rounded
$channel->print($minutes > 1 ? "Need $minutes minutes..." : "Wait...",
MSGLEVEL_MSGS);
}
# @param Channel object
# @return void
#
sub print_summary
{
my ($channel) = @_;
our %num_visitors;
our $num_users;
$channel->print('Prominent channels in here:', MSGLEVEL_MSGS);
my $summary = '';
my $prev_num = 0;
my $prev_share = 0;
# Sort by value in descending order
foreach my $channel_name
(sort { $num_visitors{$b} <=> $num_visitors{$a} } keys %num_visitors)
{
my $num = $num_visitors{$channel_name};
my $share = int(0.5 + (($num / $num_users) * 100)); # rounded
last if $share < 1;
$summary .= " %9$share%%%9" if $share != $prev_share;
$summary .= " $num " if $num != $prev_num;
$summary .= $share > 1 ? '%c' : '%K';
$summary .= "%c$channel_name%n ";
$prev_num = $num;
$prev_share = $share;
}
$summary = '%9none%9' if !$summary;
$channel->print($summary, MSGLEVEL_MSGS);
}
# @return void
#
sub script_finished
{
print_summary(our $my_channel);
# restore original state
signal_remove('redir user_channels_found', 'user_channels_found');
signal_remove('redir done_with_user', 'done_with_user');
}
# @ingroup irssi_signal
# @param Server object
# @param string channel names
# @return void
#
sub user_channels_found
{
my ($server, $args) = @_;
our %num_visitors;
our $my_channel;
$_ = $args;
my @channel_names = /\#[\#a-zA-Z0-9_-]*/g;
my @relevant_channel_names = grep { $_ ne $my_channel->{name} && $_ ne '#' } @channel_names;
foreach my $channel_name (@relevant_channel_names)
{
$num_visitors{$channel_name}++;
}
}
# @ingroup irssi_signal
# @param Server object
# @return void
#
sub done_with_user
{
our $num_awaited_users--;
script_finished() if $num_awaited_users == 0;
}
# @ingroup irssi_command
# @return void
#
sub prominent_channels_sought
{
my ($args, $server, $witem) = @_;
our %num_visitors = ();
our $my_channel = Irssi::active_win->{active};
return if print_unsatisfied_preconditions($my_channel);
my @usernames = map { $_->{nick} } $my_channel->nicks();
our $num_users = @usernames;
our $num_awaited_users = $num_users;
print_runtime_estimate($my_channel, $num_awaited_users);
signal_add('redir user_channels_found', 'user_channels_found');
signal_add('redir done_with_user', 'done_with_user');
foreach my $username (@usernames)
{
$server->redirect_event('whois', 1, $username, 0, undef, {
# 319 Channels
'event 319' => 'redir user_channels_found',
# 318 End of WHOIS
'event 318' => 'redir done_with_user',
# 401 No such nickname
'event 401' => 'redir done_with_user',
# NOP
'' => 'event empty',});
$server->send_raw("WHOIS :$username");
}
}
command_bind('promchans', 'prominent_channels_sought');