#!/usr/bin/perl -w use strict; use Irssi; use vars qw($VERSION %IRSSI); $VERSION = "1.0"; %IRSSI = ( authors => 'Julie LaLa', contact => 'ryz@asdf.us', name => 'xdcc.pl', description => 'Run an XDCC file server from irssi.', license => 'Jollo LNT license', url => 'http://asdf.us/xdcc/', changed => 'Tue Jan 21 09:57:55 EST 2015', ); my @files; my @queue; my @channels; my $queue_max = 99; my $bother_delay = 9999; my $irssidir = Irssi::get_irssi_dir(); my $dcc_upload_path = Irssi::settings_get_str('dcc_upload_path'); my $sending = 0; my $disabled = 0; my $timeout = undef; my $current_dcc = undef; my $stats = { files_sent => 0, files => {}, users => {}, }; my $help_local = < ] [-del ] [-list] [-stats] [-help] -add: Add a file to our XDCC server -del: Remove a file from the offerings -list: Display the XDCC list (default) -reset: Reset the file list and the queue -stats: Statistics for this session -enable: Enable/disable the XDCC server -trust: Trust ops from a channel to upload files. -help: Display this help. Examples: /xdcc -add sally.gif Jollo in his native habitat :) /xdcc -add jollo.mp3 Distant cry of the Jollo, 5:43 am /xdcc -del 1 For client commands, type: /ctcp XDCC help Requests are queued, only one file will be sent at a time. Filenames must not contain spaces. EOF my $help_remote = <>%n %_Scriptinfo:%_ Loaded $0 version $1 by $2.' ]); my $messages = { 'queue_is_full' => "[%_XDCC%_] The queue is currently full.", 'queue_is_empty' => "[%_XDCC%_] The queue is currently empty.", 'not_in_queue' => "[%_XDCC%_] Didn't find you in the queue.", 'no_files_offered' => "[%_XDCC%_] Sorry, there's no warez today", 'file_not_found' => "[%_XDCC%_] File not found", 'illegal_index' => "[%_XDCC%_] Bad index for batch request.", 'specify_number' => "[%_XDCC%_] Please specify a number from 1-%d.", 'specify_range' => "[%_XDCC%_] Please specify a range from 1-%d.", 'file_entry' => '[%_XDCC%_] [%d] %s ... %s', 'file_count' => '[%_XDCC%_] %d file%s', 'file_help_get' => '[%_XDCC%_] Type %_/ctcp %nick xdcc get N%_ to request a file', 'in_queue' => '[%_XDCC%_] You are #%d in queue. Requested [%d] %s', 'enqueued_count' => '[%_XDCC%_] Added %d files to queue', 'queue_length' => '[%_XDCC%_] %d request%s in queue', 'sending_file' => '[%_XDCC%_] Sending you [%d] %s ...!', 'file_help_send' => '[%_XDCC%_] Type %_/dcc get %nick%_ to accept the file', 'xdcc_added_file' => '[%_XDCC%_] Added file [%_%d%_] %s', 'xdcc_deleted_file' => '[%_XDCC%_] Deleted file [%_%d%_] %s', 'xdcc_described' => '[%_XDCC%_] Updated description for [%_%d%_] %s', 'xdcc_final_warning' => '[%_XDCC%_] This is your last warning!', 'xdcc_inactive' => '[%_XDCC%_] The DCC transfer has been cancelled for inactivity.', 'xdcc_removed' => '[%_XDCC%_] Your request has been removed.', 'xdcc_file_removed' => '[%_XDCC%_] The file you requested [%d] has been removed.', 'xdcc_autoget_tip' => '[%_XDCC%_] Tip: in irssi, type %_/set dcc_autoget ON%_', 'xdcc_cancelled' => '[%_XDCC%_] File transfer cancelled', 'xdcc_stats' => '[%_XDCC%_] %s ... %_%s%_', 'xdcc_log' => "[%_XDCC%_] %s", 'xdcc_hr' => "[%_XDCC%_] ----", 'xdcc_help' => $help_remote, 'xdcc_help_control' => $help_control, 'xdcc_about' => $help_about, 'xdcc_version' => "[%_XDCC%_] v$VERSION", }; # Public XDCC request API sub ctcp_reply { my ($server, $data, $nick, $address, $target) = @_; my ($ctcp, $cmd, $index, $desc) = split (" ", lc($data), 4); if ($disabled || $ctcp ne "xdcc") { return; } if ($cmd eq "get") { xdcc_enqueue($server, $nick, $index) } elsif ($cmd eq "send") { xdcc_enqueue($server, $nick, $index) } elsif ($cmd eq "batch") { xdcc_batch($server, $nick, $index) } elsif ($cmd eq "info") { xdcc_info_remote($server, $nick, $index) } elsif ($cmd eq "remove") { xdcc_remove($server, $nick, $index) } elsif ($cmd eq "delete") { xdcc_delete($server, $nick, $index) } elsif ($cmd eq "cancel") { xdcc_cancel($server, $nick) } elsif ($cmd eq "queue") { xdcc_queue($server, $nick) } elsif ($cmd eq "list") { xdcc_list($server, $nick) } elsif ($cmd eq "stats") { xdcc_stats_remote($server, $nick) } elsif ($cmd eq "describe") { xdcc_describe($server, $nick, $index, $desc) } elsif ($cmd eq "version") { xdcc_message($server, $nick, 'xdcc_version') } elsif ($cmd eq "help") { xdcc_help($server, $nick) } elsif ($cmd eq "about") { xdcc_message($server, $nick, 'xdcc_about') } else { xdcc_list($server, $nick) } Irssi::signal_stop(); } sub xdcc_message { my ($server, $nick, $msgname, @params) = @_; my (@msgs) = split (/\n/, $messages->{$msgname}); for my $msg (@msgs) { $msg =~ s/%_/\x02/g; $msg =~ s/%-/\x03/g; $msg =~ s/%nick/$server->{nick}/g; $msg = sprintf $msg, @params; $server->send_message( $nick, $msg, 1 ); } } sub xdcc_help { my ($server, $nick) = @_; xdcc_message($server, $nick, 'xdcc_help'); if (xdcc_is_trusted($server, $nick)) { xdcc_message($server, $nick, 'xdcc_help_control'); } } sub xdcc_enqueue { my ($server, $nick, $index, $quiet) = @_; my $id = int $index; $id -= 1; my $request = { server => $server, nick => $nick, id => $id }; if (scalar @files == 0) { xdcc_message( $server, $nick, 'no_files_offered' ); return; } if ($index < 0 || $index > scalar @files) { xdcc_message( $server, $nick, 'file_not_found' ); xdcc_message( $server, $nick, 'specify_range', scalar @files ); return; } if (! $sending && @queue == 0) { xdcc_send($request); return; } elsif (@queue > $queue_max) { xdcc_message( $server, $nick, 'queue_is_full' ); return; } push(@queue, $request); if (! $quiet) { xdcc_queue($server, $nick); } } sub xdcc_batch { my ($server, $nick, $index) = @_; if (scalar @files == 0) { xdcc_message( $server, $nick, 'no_files_offered' ); return; } if ($index !~ /-/) { xdcc_message( $server, $nick, 'illegal_index' ); xdcc_message( $server, $nick, 'specify_range', scalar @files ); return; } my ($from, $to) = split(/-/, $index, 2); $from = int $from; $to = int $to; if ($from >= $to || $from < 1 || $to < 1 || $from > @files || $to > @files) { xdcc_message( $server, $nick, 'illegal_index' ); xdcc_message( $server, $nick, 'specify_range', scalar @files ); return; } for (my $i = $from; $i <= $to; $i++) { xdcc_enqueue($server, $nick, $i, 1); } xdcc_message($server, $nick, 'enqueued_count', $to-$from); xdcc_message($server, $nick, 'xdcc_autoget_tip'); } sub xdcc_remove { my ($server, $nick, $index) = @_; my $id = int $index; $id -= 1; my $removed; for (my $n = @queue; $n >= 0; --$n) { if ($queue[$n]->{nick} eq $nick && ($id == -1 || $queue[$n]->{id} == $id)) { $removed = splice(@queue, $n, 1); } } if ($removed) { xdcc_message( $server, $nick, 'xdcc_removed' ); } else { xdcc_message( $server, $nick, 'not_in_queue' ); } } sub xdcc_cancel { my ($server, $nick) = @_; if ($current_dcc && $current_dcc->{nick} eq $nick) { xdcc_message( $server, $nick, 'xdcc_cancelled' ); $current_dcc->destroy(); } } sub xdcc_delete { my ($server, $nick, $index, $desc) = @_; my $id = int $index; $id -= 1; if (xdcc_is_trusted($server, $nick)) { my $file = xdcc_del($index); if ($file) { xdcc_message( $server, $nick, 'xdcc_deleted_file', $file->{id}+1, $file->{fn} ); } } } sub xdcc_describe { my ($server, $nick, $index, $desc) = @_; my $id = int $index; $id -= 1; if (xdcc_is_trusted($server, $nick)) { my $file = $files[$id]; $file->{desc} = $desc; xdcc_message( $server, $nick, 'xdcc_described', $file->{id}+1, $file->{'fn'} ); } } sub xdcc_info_remote { my ($server, $nick, $index) = @_; my $info = xdcc_get_info($index); if (! $info) { return; } xdcc_message( $server, $nick, 'xdcc_stats', ' #', $info->{id} ); xdcc_message( $server, $nick, 'xdcc_stats', 'name', $info->{name} ); xdcc_message( $server, $nick, 'xdcc_stats', 'nick', $info->{nick} ); xdcc_message( $server, $nick, 'xdcc_stats', 'date', $info->{date} ); xdcc_message( $server, $nick, 'xdcc_stats', 'size', $info->{size} ); xdcc_message( $server, $nick, 'xdcc_stats', 'desc', $info->{desc} ); } sub xdcc_info { my ($index) = @_; my $info = xdcc_get_info($index); if (! $info) { return; } Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', ' #', $info->{id} ); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', 'name', $info->{name} ); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', 'nick', $info->{nick} ); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', 'date', $info->{date} ); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', 'size', $info->{size} ); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', 'desc', $info->{desc} ); } sub xdcc_get_info { my ($index) = @_; my $id = int $index; return if ($id < 1 || $id > scalar @files); $id -= 1; my $file = $files[$id]; my @stats = stat($file->{path}); my $size = int $stats[7]; my $date = $stats[9]; my ($m,$h,$d,$n,$y) = (localtime $date)[1..5]; my @months = qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec]; my $ymd = sprintf "%d-%s-%d %d:%02d", $d, $months[$n], 1900+$y, $h, $m; my $bytes; if ($size < 1024) { $bytes = $size + " b." } elsif ($size < 1024*1024) { $bytes = int($size/1024) + " kb." } elsif ($size < 1024*1024*1024) { $bytes = sprintf("%0.1d",int((10*$size)/(1024*1024))) + " kb." } return { id => $id+1, name => $file->{fn}, nick => $file->{nick}, date => $ymd, size => $bytes, desc => $file->{desc}, } } sub xdcc_list { my ($server, $nick) = @_; if (scalar @files == 0) { xdcc_message( $server, $nick, 'no_files_offered' ); return; } my ($msg, $file); for (my $n = 0; $n < @files ; ++$n) { xdcc_message( $server, $nick, 'file_entry', $n+1, $files[$n]->{fn}, $files[$n]->{desc} ); } # xdcc_message( $server, $nick, 'file_count', scalar @files, scalar @files == 1 ? "" : "s" ); xdcc_message( $server, $nick, 'file_help_get'); } sub xdcc_queue { my ($server, $nick) = @_; if (scalar @queue == 0) { xdcc_message( $server, $nick, 'queue_is_empty' ); return } my $msg; for (my $n = 0; $n < @queue; ++$n) { if ($queue[$n]->{nick} eq $nick) { xdcc_message( $server, $nick, 'in_queue', $n+1, $queue[$n]->{id}+1, $files[$queue[$n]->{id}]->{fn} ) # break } } xdcc_message( $server, $nick, 'queue_length', scalar @queue, scalar @queue == 1 ? "" : "s" ) } sub xdcc_stats_remote { my ($server, $nick) = @_; xdcc_message( $server, $nick, 'xdcc_stats', "xdcc.pl version", $VERSION); if (xdcc_is_trusted($server, $nick)) { my @channel_names = map { $_->{'name'} } @channels; xdcc_message( $server, $nick, 'xdcc_stats', 'trusted channels', join(' ', @channel_names)); } xdcc_message( $server, $nick, 'xdcc_stats', "files sent", $stats->{files_sent}); xdcc_message( $server, $nick, 'xdcc_hr'); xdcc_message( $server, $nick, 'xdcc_log', 'top files'); map { xdcc_message( $server, $nick, 'xdcc_stats', $_->[0], $_->[1]) } sort { $b->[1] <=> $a->[1] } map { [$_, $stats->{files}->{$_}] } keys %{ $stats->{files} }; xdcc_message( $server, $nick, 'xdcc_hr'); xdcc_message( $server, $nick, 'xdcc_log', 'top users'); map { xdcc_message( $server, $nick, 'xdcc_stats', $_->[0], $_->[1]) } sort { $b->[1] <=> $a->[1] } map { [$_, $stats->{users}->{$_}] } keys %{ $stats->{users} }; xdcc_message( $server, $nick, 'xdcc_hr'); } sub xdcc_advance { if ($timeout) { Irssi::timeout_remove($timeout) } undef $timeout; undef $current_dcc; if (@queue == 0) { return; } my $request = shift @queue; xdcc_send($request); } sub xdcc_send { my ($request) = @_; my $server = $request->{server}; my $nick = $request->{nick}; my $id = $request->{id}; my $file = $files[$id]; my $path = $file->{path}; my $fn = $file->{fn}; xdcc_message( $server, $nick, 'sending_file', $id+1, $fn ); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_sending_file', $id, $nick, $fn); $server->command("/DCC send $nick $path"); $sending = 1; $stats->{files_sent}++; $stats->{users}->{$nick} ||= 0; $stats->{users}->{$nick}++; $stats->{files}->{$fn} ||= 0; $stats->{files}->{$fn}++; } # XDCC command control sub xdcc_report { if (scalar @files == 0) { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_no_files'); } else { for (my $n = 0; $n < @files ; ++$n) { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_print_file', $n+1, $files[$n]->{fn}, $files[$n]->{desc}); } } if (scalar @queue == 0) { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_queue_empty'); } else { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_hr'); for (my $n = 0; $n < @files ; ++$n) { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_print_queue', $n+1, $queue[$n]->{nick}, $queue[$n]->{id}, $files[$queue[$n]->{id}-1]->{fn}); } Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_hr'); } } sub xdcc_stats { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', "xdcc.pl version", $VERSION); my @channel_names = map { $_->{'name'} } @channels; if (scalar @channel_names) { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', 'trusted channels', join(' ', @channel_names)); } else { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', 'trusted channels', 'none'); } Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', "files sent", $stats->{files_sent}); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_hr'); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'top files'); map { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', $_->[0], $_->[1]) } sort { $b->[1] <=> $a->[1] } map { [$_, $stats->{files}->{$_}] } keys %{ $stats->{files} }; Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_hr'); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'top users'); map { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_stats', $_->[0], $_->[1]) } sort { $b->[1] <=> $a->[1] } map { [$_, $stats->{users}->{$_}] } keys %{ $stats->{users} }; Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_hr'); } sub xdcc_add { my ($path, $desc, $nick) = @_; if ($path !~ /^[\/~]/) { $path = $dcc_upload_path . "/" . $path; } if ($path =~ /^[~]/) { $path =~ s/^~//; $path = $ENV{"HOME"} . $path; } if (! -e $path) { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_file_not_found'); return 0; } my $fn = $path; $fn =~ s|^.*\/||; my $id = scalar @files; my $file = { id => $id, fn => $fn, path => $path, desc => $desc, nick => $nick, }; push(@files, $file); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_added_file', $id+1, $fn); return $file; } sub xdcc_del { my ($id) = @_; $id = (int $id) - 1; if ($id < 0 || $id > scalar @files) { xdcc_log( 'No file with index $id' ); return 0; } my $file = $files[$id]; my $req; splice(@files, $id, 1); for (my $n = $#queue; $n >= 0; --$n) { if ($queue[$n]->{id} == $id) { $req = $queue[$n]; splice(@queue, $n, 1); xdcc_message( $req->{server}, $req->{nick}, 'xdcc_file_removed', $n ); } elsif ($queue[$n]->{id} > $id) { --$queue[$n]->{id}; } } Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_removed_file', $id+1, $file->{fn}); return $file; } sub xdcc_reset { @files = (); @queue = (); if ($current_dcc) { $current_dcc->destroy() } $sending = 0; Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_reset'); } sub xdcc_log { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', $_[0]); } # Trust ops from certain channels sub xdcc_trust { my ($channel_name) = @_; my $channel = Irssi::channel_find($channel_name); if ($channel) { for my $c (@channels) { if ($c->{'name'} eq $channel->{'name'}) { xdcc_log("Already trusting ops on $channel_name"); return; } } xdcc_log("Trusting ops on $channel_name"); # Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_autoget_tip'); push(@channels, $channel); } else { xdcc_log("Couldn't find channel $channel_name (join it?)"); } } sub xdcc_distrust { my ($channel_name) = @_; for (my $i = $#channels; $i >= 0; $i--) { if ($channels[$i]->{name} == $channel_name) { my $channel = $channels[$i]; splice(@channels, $i, 1); xdcc_log("Stopped trusting $channel_name"); return } } xdcc_log("Couldn't find channel $channel_name"); } sub xdcc_is_trusted { my ($server, $nick) = @_; my $user; for my $channel (@channels) { if ($channel->{server}->{'name'} ne $server->{'name'}) { next; } $user = $channel->nick_find($nick); if ($user && $user->{op}) { return 1; } } return 0; } sub xdcc { my ($data, $server) = @_; my ($cmd, $fn, $desc) = split (" ", $data, 3); $cmd = lc($cmd); $cmd =~ s/^-//; if ($cmd eq "add") { xdcc_add($fn, $desc, $server->{nick}) } elsif ($cmd eq "del") { xdcc_del($fn) } elsif ($cmd eq "info") { xdcc_info($fn) } elsif ($cmd eq "list") { xdcc_report() } elsif ($cmd eq "reset") { xdcc_reset() } elsif ($cmd eq "stats") { xdcc_stats() } elsif ($cmd eq "enable") { $disabled = 0 } elsif ($cmd eq "disable") { $disabled = 1 } elsif ($cmd eq "trust") { xdcc_trust($fn) } elsif ($cmd eq "distrust") { xdcc_distrust($fn) } elsif ($cmd eq "help") { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_help', $help_local) } elsif ($cmd eq "version") { xdcc_version() } else { xdcc_report() } } # DCC management sub dcc_created { my ($dcc) = @_; # Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'dcc created'); if (lc $dcc->{'type'} eq "send") { if ($timeout) { Irssi::timeout_remove($timeout) } $timeout = Irssi::timeout_add_once($bother_delay, \&xdcc_bother, { dcc => $dcc, times => 1 }); # xdcc_log("sending file.."); $current_dcc = $dcc; } elsif (lc $dcc->{'type'} eq "get" && scalar @channels) { if (xdcc_is_trusted($dcc->{'server'}, $dcc->{'nick'})) { # all is well... } else { $dcc->destroy(); } } } sub xdcc_bother { my ($data) = @_; my $dcc = $data->{dcc}; my $times = $data->{times}; if ($times == 3) { xdcc_message($dcc->{server}, $dcc->{nick}, 'xdcc_final_warning'); } if ($times <= 3) { xdcc_message($dcc->{server}, $dcc->{nick}, 'file_help_send'); $data->{times}++; $timeout = Irssi::timeout_add_once($bother_delay, \&xdcc_bother, $data); } else { Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'Send to ' . $dcc->{nick} . ' timed out.'); xdcc_message($dcc->{server}, $dcc->{nick}, 'xdcc_inactive'); xdcc_message($dcc->{server}, $dcc->{nick}, 'xdcc_autoget_tip'); $dcc->destroy(); return } } sub dcc_destroyed { # Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'dcc destroyed'); my ($dcc) = @_; if (lc $dcc->{'type'} eq "send") { $sending = 0; xdcc_advance(); } elsif (lc $dcc->{'type'} eq "get" && xdcc_is_trusted($dcc->{'server'}, $dcc->{'nick'})) { my $file = xdcc_add($dcc->{'arg'}, "uploaded by $dcc->{'nick'}", $dcc->{'nick'}); if ($file) { xdcc_message($dcc->{server}, $dcc->{nick}, 'xdcc_added_file', $file->{'id'}+1, $file->{'fn'}); } } } sub dcc_connected { # Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'dcc connected'); my ($dcc) = @_; if (lc $dcc->{'type'} eq "send" && $timeout) { Irssi::timeout_remove($timeout); undef $timeout; } } sub dcc_rejecting { # Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'dcc rejecting'); } sub dcc_closed { # Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'xdcc_log', 'dcc closed'); } # listen for xdcc end/cancel/close Irssi::signal_add('dcc created', 'dcc_created'); Irssi::signal_add('dcc destroyed', 'dcc_destroyed'); Irssi::signal_add('dcc connected', 'dcc_connected'); Irssi::signal_add('dcc rejecting', 'dcc_rejecting'); Irssi::signal_add('dcc closed', 'dcc_closed'); Irssi::signal_add('default ctcp msg', 'ctcp_reply'); Irssi::command_bind('xdcc', 'xdcc'); Irssi::command_set_options('xdcc','add del list stats enable disable reset trust distrust help version'); Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'loaded', $IRSSI{name}, $VERSION, $IRSSI{authors});