Git browser: IRC/
This page presents code associated with the module/unit named above.
IRC/Class.phIRCe.php
<?php
/* phIRCe - PHP IRC bot
* Copyright (C) 2009-2013 Tony Manco <trmanco@gmx.com>
* Portions (C) 2010-2016 Toby Thain <toby@telegraphics.com.au>
# Portions (C) 2022 bnchs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class phirce {
private $socket, $conn, $line, $cmd, $version, $allowed = array(),
$badwords = array(), $modules = array(),
$message, $tmppath, $useragent, $logpath, $log, $arcpath,
$archive, $modpath, $slash, $windows,
$twitter_ckey, $twitter_csecret, $twitter_token, $twitter_tsecret, // from config.php
$twitter_bearer_token, $lastfrom, $lastparam;
public $nick, $ident, $pass, $realname, $host, $port, $chan, $twitter_api,
$response_colour, $cmd_highlight, $cmd_begin;
/**
* Don't process a URL more than once if it has been seen in the
* same (arbitrary) screenful of lines.
*/
const HISTORY_SCREENFUL = 24;
/**
* http://www.mirc.com/colors.html, http://www.irssi.org/documentation/formats
*/
const MIRC_COLOUR_RE = '\002|(\003\d\d?(,\d\d?)?)|\017|\026|\037';
const RESET = "\033[0m";
const DIM = "\033[2m";
const BLACK = "\033[30m";
const RED = "\033[31m";
const GREEN = "\033[32m";
const YELLOW = "\033[33m";
const BLUE = "\033[34m";
const MAGENTA = "\033[35m";
const CYAN = "\033[36m";
const WHITE = "\033[37m";
const REDBG = "\033[41m";
const GREENBG = "\033[42m";
const YELLOWBG = "\033[43m";
const MAGENTABG = "\033[45m";
const CYANBG = "\033[46m";
// Set this to the domain of your preferred Invidious instance
// const INVIDIOUS_INSTANCE = "yewtu.be";
// LIST: Invidious instances, A random instance will be picked from this list
// FILTERED: Tor and Clownflare'd instances
public $invidious_instances = [
public $invidious_instances = [
"invidious.projectsegfau.lt",
"yewtu.be",
"invidious.lunar.icu",
"invidious.tiekoetter.com",
"invidious.baczek.me",
"vid.priv.au",
"iv.ggtyler.dev",
"not-ytb.blocus.ch",
"inv.zzls.xyz",
"onion.tube",
"iv.melmac.space",
"invidious.privacydev.net",
"invidious.slipfox.xyz",
"vid.puffyan.us",
"inv.makerlab.tech",
"inv.in.projectsegfau.lt",
"yt.oelrichsgarcia.de"
];
public function __construct($nick, $pass, $ident, $realname, $host, $port,
$chan, $tmppath, $useragent, $logpath, $log,
$arcpath, $archive, $modpath) {
$this->nick = $nick;
$this->pass = $pass;
$this->ident = $ident;
$this->realname = $realname;
$this->host = $host;
$this->port = $port;
$this->chan = $chan;
$this->line_count = 0;
$this->url_seen = array();
$this->lastcommand = $this->lastfrom = $this->lastparam = '';
die_unless(is_dir($tmppath) || mkdir($tmppath, 0755, true), // use 0750 if don't trust other users
"Cannot create tmp directory $tmppath\r\n");
$this->tmppath = $tmppath;
$this->useragent = $useragent;
die_unless(is_dir($logpath) || mkdir($logpath, 0755, true),
"Cannot create log directory $logpath\r\n");
$this->logpath = $logpath;
$this->log = $log;
die_unless(is_dir($arcpath) || mkdir($arcpath, 0755, true),
"Cannot create archive directory $arcpath\r\n");
$this->arcpath = $arcpath;
$this->archive = $archive;
$this->modpath = $modpath;
$this->twitter_api = 'https://api.twitter.com';
$this->windows = strcasecmp(substr(PHP_OS, 0, 3), 'Win') == 0;
$this->version = 'phIRCe v0.77';
$this->response_colour = array(
0 => phirce::YELLOW,
2 => phirce::GREEN,
3 => phirce::CYAN,
4 => phirce::RED,
5 => phirce::RED,
);
$this->cmd_highlight = array(
'NOTICE' => phirce::YELLOW,
'PRIVMSG' => phirce::MAGENTA
);
$this->cmd_begin = array(
'JOIN' => phirce::CYANBG.phirce::BLACK,
'QUIT' => phirce::REDBG.phirce::BLACK,
'PING' => phirce::GREENBG.phirce::BLACK,
'NOTICE' => phirce::YELLOWBG.phirce::BLACK,
'PRIVMSG' => phirce::MAGENTABG.phirce::BLACK
);
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
die_unless($this->socket, "socket_create() failed: reason: "
. socket_strerror(socket_last_error()) . "\r\n");
echo "Socket successfully created.\r\n";
echo "Trying to connect to '$this->host' on port '$this->port'...";
$this->conn = socket_connect($this->socket, $this->host, $this->port);
die_unless($this->conn, "socket_connect() failed.\nReason: ($this->conn) "
. socket_strerror(socket_last_error($this->socket)) . "\r\n");
echo "Successfully connected to $this->host.\r\n";
}
private function SendCommand($cmd) {
$this->cmd = $cmd;
$bytes = socket_write($this->socket, $cmd, strlen($cmd));
if ($bytes){
if (strpos($cmd, 'PASS') !== 0 && stripos($cmd, 'NickServ') === false)
echo ("Message sent with success -> $cmd\r\n");
return true;
}
else{
echo "socket_write() failed: reason: " . socket_strerror(socket_last_error()) . "\r\n";
return false;
}
}
private function ircSafe($str) {
// Anything longer than 450 here makes Freenode/Weechat lose UTF-8 encoding - weird
return mb_strcut(str_replace("\r\n", '', $str), 0, 450, 'UTF-8');
}
private function notice($text) {
return $this->SendCommand("NOTICE {$this->chan} :" . $this->ircSafe($text) . "\r\n");
}
private function message($text) {
return $this->SendCommand("PRIVMSG {$this->chan} :" . $this->ircSafe($text) . "\r\n");
}
private function recentLine($line) {
return $line >= $this->line_count-phirce::HISTORY_SCREENFUL;
}
public function stripMircColours($text) {
return preg_replace('/'.phirce::MIRC_COLOUR_RE.'/', '', $text);
}
/**
* @return false if connection was unexpectedly terminated; true if deliberately terminated
*/
public function loop() {
if($this->pass) {
$this->SendCommand("PASS $this->pass\r\n");
}
$this->SendCommand("NICK $this->nick\r\n");
$this->SendCommand("USER $this->ident 0 * :$this->realname\r\n");
$this->SendCommand("JOIN $this->chan\r\n");
$joined = $kicked = false;
while ($this->line = socket_read($this->socket, 1024, PHP_NORMAL_READ)){
// Auto loading of modules
foreach ($this->modules as $module) {
$mod = "{$this->modpath}/$module.php";
if (file_exists($mod)) {
// Load it if file exists
//$this->SendCommand("Module $module sucessfully loaded!\r\n");
include $mod;
}
else {
$key = array_search($module, $this->modules);
unset ($this->modules[$key]);
$this->notice("Failed to load module $module. Reason: Not found!");
$this->notice("Module $module has been removed until restarted!");
}
}
if ($this->log) {
$this->LogRaw($this->line);
}
$msg = $this->MsgSplit($this->line);
if(!$msg) {
continue;
}
$this->LogStdout($msg);
$text = rtrim($this->stripMircColours(implode(' ', array_slice($msg['param'], 1))));
if ($msg['command'] == 'PING'){
// if sending PONG fails, we should probably disconnect and reconnect
if(!$this->SendCommand("PONG :{$msg['param'][0]}\r\n")) {
$this->quit();
return false;
}
if(!$joined) {
$this->SendCommand("JOIN $this->chan\r\n");
}
}
else if($msg['command'] == 'JOIN' && $msg['from'] == $this->nick) {
$joined = true;
if($kicked) {
$kicked = false;
$this->notice('How dare you kick me!');
}
}
else if($msg['command'] == 'QUIT' && $msg['from'] == $this->nick) {
return false;
}
else if ($msg['command'] == 'PRIVMSG'){
++$this->line_count;
if(($this->line_count % 100) == 0) {
// every so often, purge URL history
$this->url_seen = array_filter($this->url_seen, array($this, 'recentLine'));
}
if ($this->log)
$this->LogChannel($msg['from'], $text);
if ($this->IsCMD($text)){
if (in_array($this->FilterCloak($this->line), $this->allowed)
|| !in_array($this->FilterCloak($this->line), $this->allowed)){
$this->ExecCMDNonAdm($msg['chan'], $text);
}
if (in_array($this->FilterCloak($this->line), $this->allowed)){
if (strstr($this->line,"!quit $this->nick")){
$this->SendCommand("QUIT :Exiting! phIRCe - ".
"I told you so! .::. http://bitbucket.org/trmanco/phirce/\r\n");
sleep(1);
break;
}
// Command from channel
elseif ($msg['chan']){
$this->ExecCMD($msg['chan'], $text, $msg['from']);
}
// Command from query
elseif ($msg['quser']){
$this->QExecCMD($this->chan, $text, $msg['quser']);
}
}
else{
//$this->SendCommand("NOTICE $msg[from] :You are not allowed to control me!\r\n");
}
} elseif ($msg['chan'] == $this->chan && $this->ProcessURLs($text, $this->archive, true)){
;
}
foreach ($this->badwords as $badword){
if (strstr($text, $badword)){
$this->SendCommand("KICK ".$this->chan." ".$msg['from']." :Watch your language!\r\n");
}
}
}
elseif ($msg['command'] == 'KICK' && $msg['chan'] == $this->chan && $msg['param'][1] == $this->nick){
$kicked = true;
$joined = false;
$this->SendCommand("JOIN $this->chan\r\n");
}
$this->line = '';
}
$this->quit();
return true;
}
private function DoVersion($to) {
$this->SendCommand("NOTICE $to :{$this->version}\r\n");
}
private function DisplayModLoaded($user) {
if (!empty($this->modules)) {
foreach ($this->modules as $module) {
$this->SendCommand("PRIVMSG $user :Module \x02$module\x02 is loaded!\r\n");
}
}
else {
$this->SendCommand("PRIVMSG $user :No modules are loaded!\r\n");
}
}
private function UnloadMod($user, $mod) {
// @var $user is needed to send message status messages back to user
// @var $mod is the name of the module
$key = array_search($mod, $this->modules);
if ($key === FALSE) {
$this->SendCommand("PRIVMSG $user :Unable to unload \x02$mod\x02. Module not loaded!\r\n");
}
else {
unset ($this->modules[$key]);
$this->SendCommand("PRIVMSG $user :Module \x02$mod\x02 has been unloaded!\r\n");
if (empty($this->modules))
$this->SendCommand("PRIVMSG $user :You have no more modules in memory!\r\n");
}
}
private function LoadMod($user, $mod) {
// @var $user is needed to send message status messages back to user
// @var $mod is the name of the module
$load = $mod;
$mod = "$this->modpath/$load.php";
if (in_array($load, $this->modules)){
$this->SendCommand("PRIVMSG $user :Module \x02$load\x02 is already loaded!\r\n");
}
else {
if (file_exists($mod)) {
include $mod;
$this->modules[] = $load;
$this->SendCommand("PRIVMSG $user :Module \x02$load\x02 has been loaded!\r\n");
}
else {
$this->SendCommand("PRIVMSG $user :Failed to load module \x02$load\x02. Reason: Not found!\r\n");
}
}
}
/**
* Parse IRC message (RFC 2812).
*
* @param string $line
* @return array if a non-empty message, or null if empty message
*/
private function MsgSplit($line){
$NOSPCRLFCL = '[^\000\015\012 :]';
$MIDDLE = $NOSPCRLFCL.'[^\000\015\012 ]*';
$TRAILING = '[^\000\015\012]*';
// Regexp is for extraction, not validation, so is very permissive
// http://tools.ietf.org/html/rfc2812#section-2.3.1
if(!preg_match("/^(:(.+?) # server|nick
( (!(.+?))? # optional user
@(.+?) # host
)? # optional user|host
[ ]
)? # optional prefix
([A-Z]+|(\d+)) # command or response
($TRAILING) # params
/ix",
$line,
$m))
{
return null;
}
list ($whole, $prefix, $serverOrNick, $bangUserAtHost, $bangUser,
$user, $host, $command, $response, $params) = $m;
if(preg_match_all("/ ($MIDDLE)| :($TRAILING)/", $params, $mm) !== false) {
// $mm[1] collects all the 'middle' parameters, if present, while
// $mm[2] collects the 'trailing' parameter, if present.
// Either of these arrays can contain empty values, meaning no match;
// these are removed by array_filter()
$param = array_merge(array_filter($mm[1]), array_filter($mm[2]));
} else {
$param = null;
}
$chan = !empty($param[0]) && $param[0][0] == '#' ? $param[0] : null;
return array(
'command' => $command, // set for both commands and responses
'response' => $response, // set only for responses
'from' => $serverOrNick,
'chan' => $chan,
'quser' => $chan ? null : $serverOrNick,
'param' => $param,
'raw' => $line
);
}
private function IsCMD($cmd) {
return $cmd && $cmd[0] == '!' && strstr($cmd, ' ');
}
private function ExecCMD($chan,$cmd,$from) {
$keys = preg_split('/\s+/', $cmd);
$command = trim($keys[0]);
$user = trim($keys[1]);
switch ($command) {
case '!kick':
$this->SendCommand("KICK $chan $user :Don't take this personally!\r\n");
break;
case '!op':
$this->SendCommand("MODE $chan +o $user\r\n");
break;
case '!deop':
$this->SendCommand("MODE $chan -o $user\r\n");
break;
case '!voice':
$this->SendCommand("MODE $chan +v $user\r\n");
break;
case '!devoice':
$this->SendCommand("MODE $chan -v $user\r\n");
break;
case '!ban':
$banop = trim($keys[2]);
switch ($banop){
case 1:
$this->SendCommand("MODE $chan +b $user!*@*\r\n");
break;
default:
$this->SendCommand("PRIVMSG $chan :Not a valid ban option!\r\n");
break;
}
break;
case '!unban':
$banop = trim($keys[2]);
switch ($banop){
case 1:
$this->SendCommand("MODE $chan -b $user!*@*\r\n");
break;
default:
$this->SendCommand("PRIVMSG $chan :Not a valid unban option!\r\n");
break;
}
break;
case '!kickban':
$banop = trim($keys[2]);
switch ($banop){
case 1:
$this->SendCommand("MODE $chan +b $user!*@*\r\n");
$this->SendCommand("KICK $chan $user :Don't take this personally!\r\n");
break;
default:
$this->SendCommand("PRIVMSG $chan :Not a valid kickban option!\r\n");
break;
}
break;
case '!help':
$this->DisplayHelp($from);
break;
case '!license':
$this->DisplayLicense($from);
break;
case '!memory':
$this->SendCommand("PRIVMSG $chan :\x02Memory in use:\x02 "
.$this->MemoryUsage()." MB | \x02Peak usage:\x02 "
.$this->PeakMemoryUsage()." MB \r\n");
break;
default:
//$this->SendCommand("PRIVMSG $chan :Not a valid command!\r\n");
break;
}
}
private function QExecCMD($chan,$cmd,$quser) {
$keys = preg_split('/\s+/', $cmd);
$command = trim($keys[0]);
$user = trim($keys[1]);
//For !say command
foreach ($keys as $key) {
$this->message .= $key.' ';
}
$this->message = trim($this->message, '!say');
switch ($command) {
case '!kick':
$this->SendCommand("KICK $chan $user :Don't take this personally!\r\n");
break;
case '!op':
$this->SendCommand("MODE $chan +o $user\r\n");
break;
case '!deop':
$this->SendCommand("MODE $chan -o $user\r\n");
break;
case '!voice':
$this->SendCommand("MODE $chan +v $user\r\n");
break;
case '!devoice':
$this->SendCommand("MODE $chan -v $user\r\n");
break;
case '!ban':
$banop = trim($keys[2]);
switch ($banop){
case 1:
$this->SendCommand("MODE $chan +b $user!*@*\r\n");
break;
default:
$this->SendCommand("PRIVMSG $quser :Not a valid ban option!\r\n");
break;
}
break;
case '!unban':
$banop = trim($keys[2]);
switch ($banop){
case 1:
$this->SendCommand("MODE $chan -b $user!*@*\r\n");
break;
default:
$this->SendCommand("PRIVMSG $quser :Not a valid unban option!\r\n");
break;
}
break;
case '!kickban':
$banop = trim($keys[2]);
switch ($banop){
case 1:
$this->SendCommand("MODE $chan +b $user!*@*\r\n");
$this->SendCommand("KICK $chan $user :Don't take this personally!\r\n");
break;
default:
$this->SendCommand("PRIVMSG $quser :Not a valid kickban option!\r\n");
break;
}
break;
case '!help':
$this->DisplayHelp($quser);
break;
case '!say':
$this->SendCommand("PRIVMSG $chan :".trim($this->message)."\r\n");
$this->message = '';
break;
case '!license':
$this->DisplayLicense($quser);
break;
case '!clearlog':
$r = $this->ClearLog();
if ($r)
$this->SendCommand("PRIVMSG $quser :Log has been cleared!\r\n");
else
$this->SendCommand("PRIVMSG $quser :There was a problem clearing the log!\r\n");
break;
case '!clear-rawlog':
$r = $this->ClearRawLog();
if ($r)
$this->SendCommand("PRIVMSG $quser :Raw log has been cleared!\r\n");
else
$this->SendCommand("PRIVMSG $quser :There was a problem clearing the raw log!\r\n");
break;
case '!version':
$this->DisplayVersion($quser);
break;
case '!mods':
$this->DisplayModLoaded($quser);
break;
case '!loadmod':
// @param $user is module name in this case
$this->LoadMod($quser, $user);
break;
case '!unloadmod':
// @param $user is module name in this case
$this->UnloadMod($quser, $user);
break;
default:
$this->SendCommand("PRIVMSG $quser :Not a valid command!\r\n");
break;
}
}
private function ExecCMDNonAdm($chan,$cmd) {
$keys = preg_split('/\s+/', $cmd);
$command = trim($keys[0]);
switch ($command) {
case '!google':
$this->GoogSearch(implode(' ', array_slice($keys, 1)), $chan);
break;
default:
//$this->SendCommand("PRIVMSG $chan :Not a valid command!\r\n");
break;
}
}
private function GoogSearch($query,$chan) {
$url = 'http://ajax.googleapis.com/ajax/services/search/web'
.'?v=1.0&hl=en&rsz=small&q='.urlencode($query);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_REFERER, 'http://bitbucket.org/trmanco/phirce/');
$body = curl_exec($ch);
$searchresult = '';
if(!curl_errno($ch)){
$json = json_decode($body);
if($json && $json->responseData->results){
foreach ($json->responseData->results as $n => $searchresult) {
if($searchresult->GsearchResultClass == 'GwebSearch')
$this->SendCommand("PRIVMSG $chan :\x02[".($n + 1)."]\x02 - "
.html_entity_decode($searchresult->titleNoFormatting,
ENT_QUOTES,
'UTF-8')
." | $searchresult->url\r\n");
}
}
else{
$this->SendCommand("PRIVMSG $chan :\x02No Google results\x02\r\n");
}
}
else{
$this->SendCommand("PRIVMSG $chan :\x02Could not get Google results\x02 - ".curl_error($ch)."\r\n");
}
curl_close($ch);
}
public function Nickserv($pass) {
$this->SendCommand("/msg NickServ identify $pass\r\n");
}
public function allowed($allowed = array()) {
$this->allowed = $allowed;
}
public function badwords($badwords = array()) {
$this->badwords = $badwords;
}
public function modules($modules = array()) {
$this->modules = $modules;
}
private function MemoryUsage() {
// Returns memory usage in MB
$usage = (memory_get_usage() / 1024);
return round($usage / 1024, 3);
}
private function PeakMemoryUsage() {
// Returns memory usage in MB
$peak = (memory_get_peak_usage() / 1024);
return round($peak / 1024, 3);
}
private function CleanFilename($str){
// alternative set based on shell metachars, and path separators:
// Windows prohibits: / : < > |
// '/[\s\/\\\\:&|()!{}\\[\\]<>~`$*\'"?#]/'
// original $banstrings=array("."," ","|",",",":","-","\"","'","/");
// Don't allow timestamps on Windows, they fail.
if ($this->windows)
return preg_replace('/[-.\s|,:"\'\/\\\\]/', '_', $str);
else
return '['.date('d.m.y@G:i:s').']-'.preg_replace('/[-.\s|,:"\'\/\\\\]/', '_', $str);
}
private function XPathFromUrl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_MAXREDIRS, 8); // don't hang the bot on redirect loop
curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST , 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$doc = curl_exec($ch);
$ctype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if($doc){
$domDoc = strpos($ctype, 'xml') ? DOMDocument::loadXML($doc) : DOMDocument::loadHTML($doc);
return array ($doc, new DOMXPath($domDoc));
}
return array (null, null);
}
public function TwitterSetCredentials($ckey, $csecret, $token, $tsecret) {
$this->twitter_ckey = $ckey;
$this->twitter_csecret = $csecret;
$this->twitter_token = $token;
$this->twitter_tsecret = $tsecret;
}
// Called from bot.php startup code
// Used to validate the credentials at startup.
public function TwitterAuth() {
$cred = base64_encode(urlencode($this->twitter_ckey).':'
.urlencode($this->twitter_csecret));
$c = curl_init($this->twitter_api.'/oauth2/token');
if($c) {
if(curl_setopt($c, CURLOPT_POST, true)
//&& curl_setopt($c, CURLOPT_VERBOSE, true)
&& curl_setopt($c, CURLOPT_RETURNTRANSFER, true)
&& curl_setopt($c, CURLOPT_HTTPHEADER, array("Authorization: Basic $cred"))
&& curl_setopt($c, CURLOPT_POSTFIELDS, 'grant_type=client_credentials'))
{
$result = curl_exec($c);
$status = curl_getinfo($c, CURLINFO_HTTP_CODE);
if($result && $status >= 200 && $status < 300) {
$obj = json_decode($result);
if($obj && empty($obj->errors)) {
$this->twitter_bearer_token = $obj->access_token;
} else if(!empty($obj->errors)) {
foreach($obj->errors as $err) {
echo "$err->message\r\n";
}
} else {
echo "Bad JSON response from Twitter\r\n";
}
} else {
echo $status, '; ', curl_error($c), "\r\n";
}
}
curl_close($c);
}
return (bool)$this->twitter_bearer_token;
}
// Make an application-auth (bearer token) request. Note that this doesn't work
// for many endpoints (but it is not clearly documented which ones). :|
private function TwitterAppAuthRequest($endpoint, $params) {
$response = false;
$c = curl_init($this->twitter_api.'/1.1/'.$endpoint.'?'.http_build_query($params));
if($c) {
if(curl_setopt($c, CURLOPT_RETURNTRANSFER, true)
&& curl_setopt($c, CURLOPT_HTTPHEADER, array("Authorization: Bearer {$this->twitter_bearer_token}")))
{
$response = json_decode(curl_exec($c));
}
curl_close($c);
}
return $response;
}
// Requires the OAuth extension, which can be installed with PECL.
// (This is not currently used, as application-auth suffices to fetch
// tweet content.)
private function TwitterOAuthRequest($endpoint, $params) {
$oauth = new OAuth($this->twitter_ckey, $this->twitter_csecret,
OAUTH_SIG_METHOD_HMACSHA1,
OAUTH_AUTH_TYPE_AUTHORIZATION);
$oauth->setToken($this->twitter_token, $this->twitter_tsecret);
$url = $this->twitter_api.'/1.1/'.$endpoint.'?'.http_build_query($params);
$oauth->fetch($url);
$resp = $oauth->getLastResponse();
echo "$resp\n";
$json = json_decode($resp);
if(!$json) {
throw new Exception("Bad JSON: $resp");
}
return $json;
}
private function TwitterStatusesShow($id) {
return $this->TwitterAppAuthRequest('statuses/show.json', array('id' => $id));
}
public function stripNewlinesAndEntities($text) {
return html_entity_decode(preg_replace('/[\r\n]+/', ' ', $text), ENT_QUOTES, 'UTF-8');
}
private function GetTweet($url, $status_id){
try {
$json = $this->TwitterStatusesShow($status_id);
} catch(Exception $e) {
return array(array('error' => $e->getMessage()." ( $url )"), array());
}
if(!empty($json->retweeted_status)) {
$extra = array(" (Re-tweeted by {$json->user->screen_name})");
$text = $this->stripNewlinesAndEntities($json->retweeted_status->text);
$screen_name = $json->retweeted_status->user->screen_name;
$doc = "{$json->retweeted_status->user->screen_name} ({$json->retweeted_status->user->name})\n"
. "URL: $url\n"
. "Date: {$json->retweeted_status->created_at}\n\n"
. "Re-tweet by: {$json->user->screen_name}\n"
. "Original ID: {$json->retweeted_status->id_str}\n"
. "$text\n";
} else if(!empty($json->text)) {
$extra = array();
$text = $this->stripNewlinesAndEntities($json->text);
$screen_name = $json->user->screen_name;
$doc = "{$json->user->screen_name} ({$json->user->name})\n"
. "URL: $url\n"
. "Date: {$json->created_at}\n\n"
. "$text\n";
} else if(!empty($json->errors)) {
return array(array('error' => $json->errors[0]->message), array());
} else {
return array(array('error' => 'Unexpected response'), array());
}
if ($this->archive)
file_put_contents($this->arcpath.'/'.$this->CleanFilename(strstr($url, 'twitter')), $doc);
return array(array('text' => $text, 'screen_name' => $screen_name), $extra);
}
private function GetDent($url){
$result = array();
list ($doc, $xpath) = $this->XPathFromUrl($url);
if($xpath){
$titleElement = $xpath->query('/html/head/title');
$textElement = $xpath->query('/html/head/meta[@property="og:description"]');
if ($titleElement->length && $textElement->length){
$result['text'] = $textElement->item(0)->getAttribute('content');
$result['screen_name'] = $fname = $titleElement->item(0)->textContent;
}
else{
$result['error'] = 'Not a dent?';
$fname = strstr($url, 'identi'); // default filename
}
if ($this->archive)
file_put_contents($this->arcpath.'/'.$this->CleanFilename($fname), $doc);
}
else{
$result['error'] = 'Could not fetch identi.ca page';
}
return $result;
}
private function GetDiasporaPost($url){
$extra = array();
$resp = file_get_contents($url.'.json');
if($resp && ($json = json_decode($resp))){
$result = array('text' => $json->text,
'screen_name' => $json->author->diaspora_id
. ($json->post_type == 'StatusMessage' ? '' : ' reshared'));
$doc = "{$json->author->diaspora_id} ({$json->author->name})\n"
. "{$json->post_type}\n"
. "URL: $url\n"
. "Date: {$json->created_at}\n\n"
. "{$json->text}\n";
foreach($json->photos as $photo) {
$str = " Photo by {$photo->author->diaspora_id}: {$photo->sizes->medium}";
$extra[] = $str;
$doc .= "\n$str";
}
$fname = $result['screen_name'];
}else{
$doc = $url;
$result = array('error' => 'Not a Diaspora post?');
$fname = strstr($url, 'diaspora'); // default filename
}
if ($this->archive)
file_put_contents($this->arcpath.'/'.$this->CleanFilename($fname), $doc);
return array($result, $extra);
}
// Get YouTube video info with Invidious API
private function GetYoutubeVideo($videoid) {
$result = array();
$inv_key = $this->invidious_instances[array_rand($this->invidious_instances)];
$result["screen_name"] = "Invidious";
$result["text"] = "https://" . $inv_key. "/watch?v=" . $videoid;
// Only get the title and author of the video
// TODO: you know what
//$resp = file_get_contents('http://' . phirce::INVIDIOUS_INSTANCE . '/api/v1/videos/' . $videoid . '?fields=title,author');
//if ($resp && ($json = json_decode($resp))){
// $result['text'] = $json->title;
// $result['screen_name'] = $json->author;
//}else{
// $result['error'] = "An error has occured: INSTANCE: " . phirce::INVIDIOUS_INSTANCE . " VIDEO_ID: " . $videoid . " RESP: " . $resp;
//}
$result["text"] = "https://" . $inv_key . "/watch?v=" . $videoid;
return $result;
}
private function ProcessURL($url, $archive, $shorten, $depth = 0) {
if(isset($this->url_seen[$url]) && $this->url_seen[$url] > $this->line_count-phirce::HISTORY_SCREENFUL) {
echo "Ignoring $url which was processed ", $this->line_count - $this->url_seen[$url], " lines ago\r\n";
return null; // Processed too recently. Ignore.
}
$this->url_seen[$url] = $this->line_count;
// Special case for Twitter
if(preg_match('%^https?://twitter\.com/.+/status(es)?/(\d+)%', $url, $tweet)){
return $this->GetTweet($url, $tweet[2]);
}
// Special case for identi.ca
else if(preg_match('%^https?://identi\.ca/notice/%', $url, $dent)){
return array($this->GetDent($url), array());
}
// Special case for Youtube
else if(preg_match('%^https?://www\.youtube\.com/watch\?v=([\w-]{11})%', $url, $youtubeid)){
return array($this->GetYoutubeVideo($youtubeid[1]), array());
}
// Special case for Youtube
else if(preg_match('%^https?://youtube\.com/watch\?v=([\w-]{11})%', $url, $youtubeid)){
return array($this->GetYoutubeVideo($youtubeid[1]), array());
}
// Special case for joindiaspora.com
// joindiaspora.com changes content-type to text/html
// based on user agent, even if the content is actually xml.
// https://joindiaspora.com/posts/1309074
else if(preg_match('%^https?://(www\.)?joindiaspora\.com/posts/\d+%', $url, $urlMatch)){
return $this->GetDiasporaPost($urlMatch[0]);
}
// All other URLs
else{
// Fixup new Twitter URL so it fetches a specific resource
if(preg_match('%^(https?://twitter\.com/)#!/(.*)%', $url, $m))
$url = $m[1].$m[2];
return $this->ParseURL($url, $archive, $shorten, $depth);
}
}
private function ProcessURLs($line, $archive = false, $look_inside = false, $prefix = '') {
// If message begins with '@' then skip URL processing. This is
// useful for other bots that might have already expanded some
// content (like a Twitter stream bot).
if($line === '' || $line[0] == '@') {
return 0;
}
preg_match_all(
'%\b
(https?:// # body of URL is:
([^()]+? # a string without parens
|(\(.*?\)) # a string wrapped in balanced (...)
)+? # any number of the above
)
[.,;:?!\'"ββ]* # excluding trailing punctuation (possibly repeated)
([)\]>[:cntrl:][:space:]]|$) # must end at control char, whitespace, end of input
%ix', $line, $matches);
foreach ($matches[1] as $url){
$result = $this->ProcessURL($url, $archive, strlen($url) > 40);
if(!$result) {
continue;
}
list ($data, $extra) = $result;
$msg = "\x02No message found\x02";
if(empty($data['error'])) {
if(isset($data['title'])) {
// landing_url is the URL after all redirections
$landing_host = parse_url($data['landing_url'], PHP_URL_HOST);
$base_host = preg_replace('/(^www\.)|(\.com$)/i', '', $landing_host);
if(stripos($data['title'], $base_host) === false) {
$hint = "\x0306$landing_host\x03 | ";
} else {
$hint = '';
}
$msg = "\x0306$prefix\x03$hint\x02{$data['title']}\x02"
. (empty($data['short']) ? '' : " \x0306[ {$data['short']} ]\x03");
}
else if($data['screen_name']){
$msg = "@\x02{$data['screen_name']}\x02: {$data['text']}";
}
}
else{
$msg = "\x02{$data['error']}\x02";
}
$joined = preg_replace('%\s*([\r\n]+|<br/?>)\s*%i', ' ', $msg);
$this->notice(mb_strimwidth($joined, 0, 440, ' ...', 'UTF-8'));
foreach($extra as $line) {
$this->notice($line);
}
// Fetch URLs in tweet/dent. Linked documents will not be archived.
if($look_inside && !empty($data['text']))
$this->ProcessURLs($data['text'], false, false, '-> ');
}
return count($matches[1]);
}
/**
* Use ur1.ca to shorten a URL.
*/
public function shortenUrl($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://ur1.ca/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'longurl='.urlencode($url));
$response = curl_exec($ch);
$result = null;
if($response && preg_match('/Your ur1 is: <a href="(.*?)"/', $response, $matches)) {
$result = $matches[1];
}
curl_close($ch);
return $result;
}
private static function matchLocation($h) {
return preg_match('/^Location:\s*(.*)/i', $h, $m) ? $m[1] : null;
}
private function ParseURL($url, $archive = false, $shorten = true, $depth = 0) {
$data = array();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
// t.co issues a META HTTP-EQUIV REFRESH and javascript redirect,
// but no HTTP redirect, if it thinks client is a browser.
// Don't spoof useragent for this site.
if(!preg_match('%^https?://t.co/%i', $url))
curl_setopt($ch, CURLOPT_USERAGENT, $this->useragent);
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = explode("\r\n\r\n", curl_exec($ch), 2);
$ctype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($response && in_array($status, array(300, 301, 302, 303, 307))) {
if($depth == 20) {
$data['error'] = "Aborting after $depth redirects";
} else {
// Look for Location header
$loc = array_filter(array_map(
'phirce::matchLocation',
preg_split('/\r\n/', $response[0])
));
$targetUrl = array_shift($loc);
if($targetUrl) {
if(preg_match('%^https?://%', $targetUrl)) {
// normal case: absolute URL
echo "$status redirect to: $targetUrl\n";
return $this->ProcessURL($targetUrl, $archive, $shorten, $depth + 1);
} else if($targetUrl[0] == '/') {
// relative URL
$u = parse_url($url);
$absUrl = $u['scheme'] . '://' . $u['host']
. (empty($u['port']) ? '' : $u['port'])
. $targetUrl;
echo "$status redirect to: $targetUrl ==> Corrected to absolute URL $absUrl\n";
return $this->ProcessURL($absUrl, $archive, $shorten, $depth + 1);
} else {
$data['error'] = "$status redirect with weird Location: $targetUrl";
}
} else {
$data['error'] = "$status redirect without Location header";
}
}
}
else if($status >= 200 && $status < 300 && count($response) > 1){
$data['landing_url'] = $url;
if($shorten) {
$data['short'] = $this->shortenUrl($url);
}
$doc = $response[1];
$ctypeparts = split(';', $ctype);
if (!strcasecmp(trim($ctypeparts[0]), 'text/html') || stripos($doc, '<html>') !== false){
$matches = array();
if (preg_match('%<title.*?>(.*?)</title\s*>%si', $doc, $matches)){
$data['title'] = trim(html_entity_decode(
strip_tags(preg_replace('/[\r\n]+/', ' ', $matches[1])),
ENT_QUOTES,
'UTF-8'));
if ($archive){
// Save to archive
file_put_contents($this->arcpath.'/'
.$this->CleanFilename(substr($data['title'], 0, 120))
.'.html',
$doc);
}
clearstatcache();
}else{
$data['title'] = 'NO TITLE';
}
}
else if($ctype){
$matches = array();
if ($archive && preg_match('/:\/\/(.*?)([^\/]*?)(\.\w+)?([?#].*)?$/', $url, $matches)){
// Save to archive
// Take filename from url. If no base name, use whole hostname and path
list ($skip, $urlpath, $filename, $ext) = $matches;
file_put_contents($this->arcpath.'/'
.$this->CleanFilename($filename ? $filename : $urlpath)
.($ext ? $ext : ''), // add extension if present
$doc);
}
return null;
}
else{
$data['error'] = 'Missing content type. Ignoring.';
}
$data['pagesize'] = round(strlen($doc) / 1024 , 2).' KB';
}
else{
$data['error'] = curl_error($ch)." ( status $status @ $url )";
}
curl_close($ch);
return array($data, array());
}
private function DisplayHelp($user) {
$helpers = array (0 => "---START---",
1 => "\x02[Help]\x02 - List of available commands:",
2 => "\x02[Kick]\x02 - Kick a user from the channel (!kick phIRCe)",
3 => "\x02[Op]\x02 - OPs a user from the channel (!op phIRCe)",
4 => "\x02[Deop]\x02 - DEOPs a user from the channel (!deop phIRCe)",
5 => "\x02[Ban]\x02 - Bans a user from the channel (!ban phIRCe 1)",
6 => "\x02[Unban]\x02 -Unbans a user from the channel (!unban phIRCe 1)",
7 => "\x02[Voice]\x02 - Give VOICE to a user from the channel (!voice phIRCe)",
8 => "\x02[Devoice]\x02 - Give DEVOICE to a user from the channel (!devoice phIRCe)",
9 => "\x02[Kickban]\x02 - Kickbans a user from the channel (!kickban phIRCe 1)",
10 => "\x02[Quit]\x02 - Disconnects the bot in a clean way (!quit phIRCe)",
11 => "\x02[Say]\x02 - Says something to the channel (!say #channel Hello)",
12 => "\x02[License]\x02 - Shows a small licensing notice (!license phIRCe)",
13 => "\x02[Clearlog]\x02 - Clears the normal logfile (!clearlog phIRCe)",
14 => "\x02[Clear-rawlog]\x02 - Clears the raw logfile (!clear-rawlog phIRCe)",
15 => "\x02[Version]\x02 - Shows the version of the bot (!version phIRCe)",
16 => "\x02[Memory]\x02 - Shows current memory used by emalloc() (!memory phIRCe)",
17 => "\x02[Mods]\x02 - Lists loaded modules (!mods phIRCe)",
18 => "\x02[Loadmod]\x02 - Loads a module into memory (!loadmod {module_name})",
19 => "\x02[Unloadmod]\x02 - Unloads a module from memory (!unloadmod {module_name})",
20 => "---END---");
foreach ($helpers as $help) {
$this->SendCommand("PRIVMSG $user :$help\r\n");
// Don't flood the servers
sleep(1);
}
}
private function DisplayLicense($user) {
$licensemsg = array('phIRCe - Copyright (C) 2009-2013 Tony Manco <trmanco@gmx.com>.',
'Portions (C) 2010-2013 Toby Thain <toby@telegraphics.com.au>.',
'This program comes with ABSOLUTELY NO WARRANTY.',
'This is free software, and you are welcome to redistribute',
'it under certain conditions.');
foreach ($licensemsg as $line) {
$this->SendCommand("PRIVMSG $user :$line\r\n");
}
}
private function FilterCloak($line) {
$pieces = explode(' ', $line);
$cloak = trim(strstr($pieces[0], '~'), '~');
return $cloak;
}
private function LogRaw($line) {
$handle = fopen("{$this->logpath}/raw.log", 'a+');
fwrite($handle, $line . "\r\n");
fclose($handle);
}
private function LogStdout($msg) {
if($msg['response']) {
$resp = (int)$msg['response']/100;
$highlight = isset($this->response_colour[$resp])
? $this->response_colour[$resp] : '';
$cmdbegin = '';
} else {
$highlight = isset($this->cmd_highlight[$msg['command']])
? $this->cmd_highlight[$msg['command']] : '';
$cmdbegin = isset($this->cmd_begin[$msg['command']])
? $this->cmd_begin[$msg['command']] : '';
}
$suppress = $msg['command'] == $this->lastcommand
&& $msg['from'] == $this->lastfrom
&& $msg['param'][0] == $this->lastparam;
// log command, sender, recipient (first param) and message (second param)
printf("$highlight$cmdbegin%8s".phirce::RESET
." %24s $highlight%24s".phirce::RESET
." %2s $highlight%s".phirce::RESET."\r\n",
$msg['command'],
$suppress ? '' : $msg['from'],
$suppress ? '' : $msg['param'][0],
count($msg['param']) > 2 ? '1' : '|',
isset($msg['param'][1]) ? $this->stripMircColours($msg['param'][1]) : '');
// log remaining parameters one per line for clarity
foreach(array_slice($msg['param'], 2) as $i => $p) {
printf("%8s %24s %24s %2d $highlight%s".phirce::RESET."\r\n",
'', '', '', $i+2, $this->stripMircColours($p));
}
$this->lastcommand = $msg['command'];
$this->lastfrom = $msg['from'];
$this->lastparam = $msg['param'][0];
}
private function LogChannel($from,$message) {
$handle = fopen("{$this->logpath}/log.log", 'a+');
$line = date(DATE_RFC822).':'."[$from] $message\r\n";
fwrite($handle, $line);
fclose($handle);
}
private function ClearLog(){
$handle = fopen("{$this->logpath}/log.log", 'w');
$r = ftruncate($handle, 0);
fclose($handle);
return $r;
}
private function ClearRawLog(){
$handle = fopen("{$this->logpath}/raw.log", 'w');
$r = ftruncate($handle, 0);
fclose($handle);
return $r;
}
private function DisplayVersion($user){
$this->SendCommand("PRIVMSG $user :$this->version\r\n");
}
public function quit() {
if($this->socket) {
socket_shutdown($this->socket, 1);
usleep(500);
socket_shutdown($this->socket, 0);
sleep(1);
socket_close($this->socket);
}
$this->socket = null;
}
}
IRC/irc-deduplicate.pl
#!/usr/bin/perl
# 2022-05-12
# parse HTML version of IRC logs
# remove seqential duplicate lines
use utf8;
use Getopt::Std;
use File::Glob ':bsd_glob';
use HTML::TreeBuilder::XPath;
use open qw/:std :utf8/;
use English;
use warnings;
use strict;
our %opt;
our %tags;
# work-arounds for 'wide character' error from wrong UTF8
binmode(STDIN, ":encoding(utf8)");
binmode(STDOUT, ":encoding(utf8)");
getopts('hv', \%opt);
&usage if ($opt{'h'});
my @filenames;
while (my $file = shift) {
my @files = bsd_glob($file);
foreach my $f (@files) {
push(@filenames, $f);
}
}
&usage if($#filenames < 0);
while (my $infile = shift(@filenames)) {
next if ($infile=~/~$/);
my $result = &deduplicate($infile);
}
exit(0);
sub usage {
print qq(Deduplicate the HTML table in IRC Logs.\n);
print qq(Finds adjacent, duplicate entries and deletes the extras.\n);
$0 =~ s/^.*\///;
print qq($0: file\n);
exit(1);
}
sub deduplicate {
my ($file)= (@_);
my @content = ();
open (my $in, '<:utf8', $file)
or die("Could not open '$file' for reading: $!\n");
while (my $line = <$in>) {
push(@content, $line);
}
close($in);
my $xhtml = HTML::TreeBuilder::XPath->new_from_content(@content);
$xhtml->implicit_tags(1);
$xhtml->no_space_compacting(1);
my %row = ();
for my $tr ($xhtml->findnodes('//table[@class="irclog"]//tr')) {
my $key = $tr->as_text;
if ($row{$key}) {
$tr->delete();
%row = ();
} else {
$row{$key}++;
}
}
print $xhtml->as_HTML("<"," ",{});
$xhtml->delete;
return (1);
}
IRC/irc-techrights-log-tail.sh
# see Git/IRC for versioning
# /root/bin/log-tail.sh
# runs from cron
set -e
irclog=/var/www/techrights.org/htdocs/irc/log
gemlog=/home/gemini/techrights.org/chat/index.gmi
echo 'Full logs (past days) in http://techrights.org/category/irc-logs/ and latest below.' \
> $irclog
echo 'βββββββββββββββββββββββββββββββββββββββββββ β' \
>> $irclog
tail -n1170 /home/irc-bots/phirce-techrights/logs/log.log \
| cut -b1-3,15- \
| sed -e s/'+0000:\['/' β γ'/ \
-e s/\]/'γ \tβ'/ \
-e 's/techrights-news/πππ TR NEWS/' \
-e 's/techrights-ipfs-bot/πππ IPFS/' \
-e 's/</\</g' \
>> $irclog
echo ' ββββββββββββββββββββββββββββββββββββββββββββ' \
>> $irclog
echo -n ' β² Last updated ' \
>> $irclog
date \
>> $irclog
tail -n1170 /home/irc-bots/phirce-techrights/logs/log.log \
| cut -b1-3,15- \
| /home/irc-bots/bin/irc-techrights-log-to-gemtext.pl \
> $gemlog
exit 0
IRC/yesterday-irc-tuxmachines.sh
!#!/bin/sh
sizetop=1000000
sizediff=1000000
tail -n${sizetop} .xchat2/xchatlogs/FreeNode-#tuxmachines.log \
| head -n${sizediff} \
> ~/LAPTOP-FreeNode-#tuxmachines.log
for i in {1..370}
do
IRCAGE=$i
IRCDATE=$(date --date="$IRCAGE days ago" +"%b %d")
#IRCDATE=$(date --date yesterday +"%b %d")
echo Processing $IRCDATE
IRCFULLDATE=$(date --date="$IRCAGE days ago" "+%A, %B %d, %Y")
#IRCFULLDATE=$(date --date yesterday "+%A, %B %d, %Y")
echo Full date: $IRCFULLDATE
IRCDATESLUG=$(date --date="$IRCAGE days ago" +"%d%m%y")
#IRCDATESLUG=$(date --date yesterday +"%d%m%y")
echo Slug: $IRCDATESLUG
IRCDATEFILE=tux
grep "^$IRCDATE" ~/LAPTOP-FreeNode-#tuxmachines.log \
> irc-log-tuxmachines.daily
python ./Main/Programs/irclog2html-tuxmachines.py \
irc-log-tuxmachines.daily \
--title="IRC: #tuxmachines @ Techrights IRC Network: $IRCFULLDATE"
cp irc-log-tuxmachines.daily.html irc-log-tuxmachines-$IRCDATESLUG.html
echo "" >> $IRCDATEFILE.txt
echo "<li><a href=\"http://techrights.org/irc-archives/irc-log-tuxmachines-$IRCDATESLUG.html\" title=\"Read the log\"><code>#tuxmachines</code> log for <b>$IRCFULLDATE</b></a></li>" \
>> $IRCDATEFILE.txt
done
sleep 10
exit 0
IRC/irclog2html-tuxmachines.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Convert IRC logs to HTML.
Usage: irclog2html.py filename
irclog2html will write out a colourised irc log, appending a .html
extension to the output file.
This is a Python port (+ improvements) of irclog2html.pl Version 2.1, which
was written by Jeff Waugh and is available at www.perkypants.org
"""
# Copyright (c) 2005, Marius Gedminas
# Copyright (c) 2000, Jeffrey W. Waugh
# Python port:
# Marius Gedminas <marius@pov.lt>
# Original Author:
# Jeff Waugh <jdub@perkypants.org>
# Contributors:
# Rick Welykochy <rick@praxis.com.au>
# Alexander Else <aelse@uu.net>
#
# Released under the terms of the GNU GPL
# http://www.gnu.org/copyleft/gpl.html
# Differences from the Perl version:
# There are no hardcoded nick colour preferences for jdub, cantanker and
# chuckd
#
# Colours are preserver accross nick changes (irclog2html.pl tries to do
# that, but its regexes are buggy)
#
# irclog2html.pl interprets --colour-server #rrggbb as -s #rrggbb,
# irclog2html.py does not have this bug
#
# irclog2html.py understands ISO 8601 timestamps (YYYY-MM-DDTHH:MM:SS)
#
# New options: --title, --{prev,index,next}-{url,title}
#
# New styles: xhtml, xhtmltable
#
# New default style: xhtmltable
#
import os
import re
import sys
import urllib
import optparse
VERSION = "2.6"
RELEASE = "2007-10-30"
# $Id$
#
# Log parsing
#
class Enum(object):
"""Enumerated value."""
def __init__(self, value):
self.value = value
def __repr__(self):
return self.value
class LogParser(object):
"""Parse an IRC log file.
When iterated, yields the following events:
time, COMMENT, (nick, text)
time, ACTION, text
time, JOIN, text
time, PART, text,
time, NICKCHANGE, (text, oldnick, newnick)
time, SERVER, text
"""
COMMENT = Enum('COMMENT')
ACTION = Enum('ACTION')
JOIN = Enum('JOIN')
PART = Enum('PART')
NICKCHANGE = Enum('NICKCHANGE')
SERVER = Enum('SERVER')
OTHER = Enum('OTHER')
TIME_REGEXP = re.compile(
r'^\[?(' # Optional [
r'(?:\d{4}-\d{2}-\d{2}T|\d{2}-\w{3}-\d{4} |\w{3} \d{2} )?' # Optional date
r'\d\d:\d\d(:\d\d)?' # Mandatory HH:MM, optional :SS
r')\]? +') # Optional ], mandatory space
NICK_REGEXP = re.compile(r'^<(.*?)>\s')
JOIN_REGEXP = re.compile(r'^(?:\*\*\*|-->)\s.*joined')
PART_REGEXP = re.compile(r'^(?:\*\*\*|<--)\s.*(quit|left)')
SERVMSG_REGEXP = re.compile(r'^(?:\*\*\*|---)\s')
NICK_CHANGE_REGEXP = re.compile(
r'^(?:\*\*\*|---)\s+(.*?) (?:are|is) now known as (.*)')
def __init__(self, infile):
self.infile = infile
def __iter__(self):
for line in self.infile:
line = line.rstrip('\r\n')
if not line:
continue
m = self.TIME_REGEXP.match(line)
if m:
time = m.group(1)
line = line[len(m.group(0)):]
else:
time = None
m = self.NICK_REGEXP.match(line)
if m:
nick = m.group(1)
text = line[len(m.group(0)):]
yield time, self.COMMENT, (nick, text)
elif line.startswith('* ') or line.startswith('*\t'):
yield time, self.ACTION, line
elif self.JOIN_REGEXP.match(line):
yield time, self.JOIN, line
elif self.PART_REGEXP.match(line):
yield time, self.PART, line
else:
m = self.NICK_CHANGE_REGEXP.match(line)
if m:
oldnick = m.group(1)
newnick = m.group(2)
yield time, self.NICKCHANGE, (line, oldnick, newnick)
elif self.SERVMSG_REGEXP.match(line):
yield time, self.SERVER, line
else:
yield time, self.OTHER, line
def shorttime(time):
"""Strip date and seconds from time.
>>> shorttime('12:45:17')
'12:45'
>>> shorttime('12:45')
'12:45'
>>> shorttime('2005-02-04T12:45')
'12:45'
"""
if 'T' in time:
time = time.split('T')[-1]
if time.count(':') > 1:
time = ':'.join(time.split(':')[:2])
return time
#
# Colouring stuff
#
class ColourChooser:
"""Choose distinguishable colours."""
def __init__(self, rgbmin=240, rgbmax=125, rgb=None, a=0.95, b=0.5):
"""Define a range of colours available for choosing.
`rgbmin` and `rgbmax` define the outmost range of colour depth (note
that it is allowed to have rgbmin > rgbmax).
`rgb`, if specified, is a list of (r,g,b) values where each component
is between 0 and 1.0.
If `rgb` is not specified, then it is constructed as
[(a,b,b), (b,a,b), (b,b,a), (a,a,b), (a,b,a), (b,a,a)]
You can tune `a` and `b` for the starting and ending concentrations of
RGB.
"""
assert 0 <= rgbmin < 256
assert 0 <= rgbmax < 256
self.rgbmin = rgbmin
self.rgbmax = rgbmax
if not rgb:
assert 0 <= a <= 1.0
assert 0 <= b <= 1.0
rgb = [(a,b,b), (b,a,b), (b,b,a), (a,a,b), (a,b,a), (b,a,a)]
else:
for r, g, b in rgb:
assert 0 <= r <= 1.0
assert 0 <= g <= 1.0
assert 0 <= b <= 1.0
self.rgb = rgb
def choose(self, i, n):
"""Choose a colour.
`n` specifies how many different colours you want in total.
`i` identifies a particular colour in a set of `n` distinguishable
colours.
Returns a string '#rrggbb'.
"""
if n == 0:
n = 1
r, g, b = self.rgb[i % len(self.rgb)]
m = self.rgbmin + (self.rgbmax - self.rgbmin) * float(n - i) / n
r, g, b = map(int, (r * m, g * m, b * m))
assert 0 <= r < 256
assert 0 <= g < 256
assert 0 <= b < 256
return '#%02x%02x%02x' % (r, g, b)
class NickColourizer:
"""Choose distinguishable colours for nicknames."""
def __init__(self, maxnicks=30, colour_chooser=None, default_colours=None):
"""Create a colour chooser for nicknames.
If you know how many different nicks there might be, specify that
numer as `maxnicks`. If you don't know, don't worry.
If you really want to, you can specify a colour chooser. Default is
ColourChooser().
If you want, you can specify default colours for certain nicknames
(`default_colours` is a mapping of nicknames to HTML colours, that is
'#rrggbb' strings).
"""
if colour_chooser is None:
colour_chooser = ColourChooser()
self.colour_chooser = colour_chooser
self.nickcount = 0
self.maxnicks = maxnicks
self.nick_colour = {}
if default_colours:
self.nick_colour.update(default_colours)
def __getitem__(self, nick):
colour = self.nick_colour.get(nick)
if not colour:
self.nickcount += 1
if self.nickcount >= self.maxnicks:
self.maxnicks *= 2
colour = self.colour_chooser.choose(self.nickcount, self.maxnicks)
self.nick_colour[nick] = colour
return colour
def change(self, oldnick, newnick):
if oldnick in self.nick_colour:
self.nick_colour[newnick] = self.nick_colour.pop(oldnick)
#
# HTML
#
URL_REGEXP = re.compile(r'((http|https|ftp|gopher|news)://[^ \'")>]*)')
def createlinks(text):
"""Replace possible URLs with links."""
return URL_REGEXP.sub(r'<a href="\1">\1</a>', text)
def escape(s):
"""Replace ampersands, pointies, control characters.
>>> escape('Hello & <world>')
'Hello & <world>'
>>> escape('Hello & <world>')
'Hello & <world>'
Control characters (ASCII 0 to 31) are stripped away
>>> escape(''.join([chr(x) for x in range(32)]))
''
"""
s = s.replace('&', '&').replace('<', '<').replace('>', '>')
return ''.join([c for c in s if ord(c) > 0x1F])
#
# Output styles
#
class AbstractStyle(object):
"""A style defines the way output is formatted.
This is not a real class, rather it is an description of how style
classes should be written.
"""
name = "stylename"
description = "Single-line description"
def __init__(self, outfile, colours=None):
"""Create a text formatter for writing to outfile.
`colours` may have the following attributes:
part
join
server
nickchange
action
"""
self.outfile = outfile
self.colours = colours or {}
def head(self, title, prev=('', ''), index=('', ''), next=('', ''),
searchbox=False):
"""Generate the header.
`prev`, `index` and `next` are tuples (title, url) that comprise
the navigation bar.
"""
def foot(self):
"""Generate the footer."""
def servermsg(self, time, what, line):
"""Output a generic server message.
`time` is a string.
`line` is not escaped.
`what` is one of LogParser event constants (e.g. LogParser.JOIN).
"""
def nicktext(self, time, nick, text, htmlcolour):
"""Output a comment uttered by someone.
`time` is a string.
`nick` and `text` are not escaped.
`htmlcolour` is a string ('#rrggbb').
"""
class SimpleTextStyle(AbstractStyle):
"""Text style with little use of colour"""
name = "simplett"
description = __doc__
def head(self, title, prev=None, index=None, next=None,
charset="iso-8859-1", searchbox=False):
print >> self.outfile, """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
\t<title>%(title)s</title>
\t<meta name="generator" content="irclog2html.py %(VERSION)s by Marius Gedminas">
\t<meta name="version" content="%(VERSION)s - %(RELEASE)s">
\t<meta http-equiv="Content-Type" content="text/html; charset=%(charset)s">
</head>
<body text="#000000" bgcolor="#ffffff"><tt>""" % {
'VERSION': VERSION,
'RELEASE': RELEASE,
'title': escape(title),
'charset': charset,
}
def foot(self):
print >> self.outfile, """
<br>Generated by irclog2html.py %(VERSION)s by <a href="mailto:marius@pov.lt">Marius Gedminas</a>
- find it at <a href="http://mg.pov.lt/irclog2html/">mg.pov.lt</a>!
</tt></body></html>""" % {'VERSION': VERSION, 'RELEASE': RELEASE},
def servermsg(self, time, what, text):
text = escape(text)
text = createlinks(text)
colour = self.colours.get(what)
if colour:
text = '<font color="%s">%s</font>' % (colour, text)
self._servermsg(text)
def _servermsg(self, line):
print >> self.outfile, '%s<br>' % line
def nicktext(self, time, nick, text, htmlcolour):
nick = escape(nick)
text = escape(text)
text = createlinks(text)
text = text.replace(' ', ' ')
self._nicktext(time, nick, text, htmlcolour)
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, '<%s> %s<br>' % (nick, text)
class TextStyle(SimpleTextStyle):
"""Text style using colours for each nick"""
name = "tt"
description = __doc__
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, ('<font color="%s"><%s></font>'
' <font color="#000000">%s</font><br>'
% (htmlcolour, nick, text))
class SimpleTableStyle(SimpleTextStyle):
"""Table style, without heavy use of colour"""
name = "simpletable"
def head(self, title, prev=None, index=None, next=None,
charset="iso-8859-1", searchbox=False):
SimpleTextStyle.head(self, title, prev, index, next, charset, searchbox)
print >> self.outfile, "<table cellspacing=3 cellpadding=2 border=0>"
def foot(self):
print >> self.outfile, "</table>"
SimpleTextStyle.foot(self)
def _servermsg(self, line):
print >> self.outfile, ('<tr><td colspan=2><tt>%s</tt></td></tr>'
% line)
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, ('<tr bgcolor="#eeeeee"><th><font color="%s">'
'<tt>%s</tt></font></th>'
'<td width="100%%"><tt>%s</tt></td></tr>'
% (htmlcolour, nick, text))
class TableStyle(SimpleTableStyle):
"""Default style, using a table with bold colours"""
name = "table"
description = __doc__
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, ('<tr><th bgcolor="%s"><font color="#ffffff">'
'<tt>%s</tt></font></th>'
'<td width="100%%" bgcolor="#eeeeee"><tt><font'
' color="%s">%s</font></tt></td></tr>'
% (htmlcolour, nick, htmlcolour, text))
class XHTMLStyle(AbstractStyle):
"""Text style, produces XHTML that can be styled with CSS"""
name = 'xhtml'
description = __doc__
CLASSMAP = {
LogParser.ACTION: 'action',
LogParser.JOIN: 'join',
LogParser.PART: 'part',
LogParser.NICKCHANGE: 'nickchange',
LogParser.SERVER: 'servermsg',
LogParser.OTHER: 'other',
}
prefix = '<div class="irclog">'
suffix = '</div>'
def head(self, title, prev=('', ''), index=('', ''), next=('', ''),
charset="UTF-8", searchbox=False):
self.prev = prev
self.index = index
self.next = next
print >> self.outfile, """\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
<title>%(title)s</title>
<link rel="stylesheet" href="/wp-content/uploads/2009/05/irclog.css" />
<meta name="generator" content="irclog2html.py %(VERSION)s by Marius Gedminas" />
<meta name="version" content="%(VERSION)s - %(RELEASE)s" />
</head>
<body>""" % {'VERSION': VERSION, 'RELEASE': RELEASE,
'title': escape(title), 'charset': charset}
self.heading(title)
if searchbox:
self.searchbox()
self.navbar(prev, index, next)
print >> self.outfile, self.prefix
def heading(self, title):
print >> self.outfile, '<a href="http://www.tuxmachines.org/" title="Home page"><img src="http://www.tuxmachines.org/files/tuxmachines-logo.png" alt="Tux Machines logo" border="0" /></a><h1>%s</h1><p>Join us now at <a href="http://www.tuxmachines.org/node/121258">the IRC channel</a>.</p>' % escape(title)
def link(self, url, title):
# Intentionally not escaping title so that &entities; work
if url:
print >> self.outfile, ('<a href="%s">%s</a>'
% (escape(urllib.quote(url)),
title or escape(url))),
elif title:
print >> self.outfile, ('<span class="disabled">%s</span>'
% title),
def searchbox(self):
print >> self.outfile, """
<div class="searchbox">
<form action="search" method="get">
<input type="text" name="q" id="searchtext" />
<input type="submit" value="Search" id="searchbutton" />
</form>
</div>
"""
def navbar(self, prev, index, next):
prev_title, prev_url = prev
index_title, index_url = index
next_title, next_url = next
if not (prev_title or index_title or next_title or
prev_url or index_url or next_url):
return
print >> self.outfile, '<div class="navigation">',
self.link(prev_url, prev_title)
self.link(index_url, index_title)
self.link(next_url, next_title)
print >> self.outfile, '</div>'
def foot(self):
print >> self.outfile, self.suffix
self.navbar(self.prev, self.index, self.next)
print >> self.outfile, """
<div class="generatedby">
<p>Generated by irclog2html.py %(VERSION)s by <a href="mailto:marius@pov.lt">Marius Gedminas</a>
- find it at <a href="http://mg.pov.lt/irclog2html/">mg.pov.lt</a>!</p>
</div>
</body>
</html>""" % {'VERSION': VERSION, 'RELEASE': RELEASE}
def servermsg(self, time, what, text):
"""Output a generic server message.
`time` is a string.
`line` is not escaped.
`what` is one of LogParser event constants (e.g. LogParser.JOIN).
"""
text = escape(text)
text = createlinks(text)
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<p id="t%s" class="%s">'
'<a href="#t%s" class="time">%s</a> '
'%s</p>'
% (time, self.CLASSMAP[what], time,
displaytime, text))
else:
print >> self.outfile, ('<p class="%s">%s</p>'
% (self.CLASSMAP[what], text))
def nicktext(self, time, nick, text, htmlcolour):
"""Output a comment uttered by someone.
`time` is a string.
`nick` and `text` are not escaped.
`htmlcolour` is a string ('#rrggbb').
"""
nick = escape(nick)
text = escape(text)
text = createlinks(text)
text = text.replace(' ', ' ')
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<p id="t%s" class="comment">'
'<a href="#t%s" class="time">%s</a> '
'<span class="nick" style="color: %s">'
'<%s></span>'
' <span class="text">%s</span></p>'
% (time, time, displaytime, htmlcolour, nick,
text))
else:
print >> self.outfile, ('<p class="comment">'
'<span class="nick" style="color: %s">'
'<%s></span>'
' <span class="text">%s</span></p>'
% (htmlcolour, nick, text))
class XHTMLTableStyle(XHTMLStyle):
"""Table style, produces XHTML that can be styled with CSS"""
name = 'xhtmltable'
description = __doc__
prefix = '<table class="irclog">'
suffix = '</table>'
def servermsg(self, time, what, text, link=''):
text = escape(text)
text = createlinks(text)
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<tr id="t%s">'
'<td class="%s" colspan="2">%s</td>'
'<td><a href="%s#t%s" class="time">%s</a></td>'
'</tr>'
% (time, self.CLASSMAP[what], text,
link, time, displaytime))
else:
print >> self.outfile, ('<tr>'
'<td class="%s" colspan="3">%s</td>'
'</tr>'
% (self.CLASSMAP[what], text))
def nicktext(self, time, nick, text, htmlcolour, link=''):
nick = escape(nick)
text = escape(text)
text = createlinks(text)
text = text.replace(' ', ' ')
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<tr id="t%s">'
'<th class="nick" style="background: %s">%s</th>'
'<td class="text" style="color: %s">%s</td>'
'<td class="time">'
'<a href="%s#t%s" class="time">%s</a></td>'
'</tr>'
% (time, htmlcolour, nick, htmlcolour, text,
link, time, displaytime))
else:
print >> self.outfile, ('<tr>'
'<th class="nick" style="background: %s">%s</th>'
'<td class="text" colspan="2" style="color: %s">%s</td>'
'</tr>'
% (htmlcolour, nick, htmlcolour, text))
#
# Main
#
# All styles
STYLES = [
SimpleTextStyle,
TextStyle,
SimpleTableStyle,
TableStyle,
XHTMLStyle,
XHTMLTableStyle,
]
# Customizable colours
COLOURS = [
("part", "#000099", LogParser.PART),
("join", "#009900", LogParser.JOIN),
("server", "#009900", LogParser.SERVER),
("nickchange", "#009900", LogParser.NICKCHANGE),
("action", "#CC00CC", LogParser.ACTION),
]
def main(argv=sys.argv):
progname = os.path.basename(argv[0])
parser = optparse.OptionParser("usage: %prog [options] filename",
prog=progname,
description="Colourises and converts IRC"
" logs to HTML format for easy"
" web reading.")
parser.add_option('-s', '--style', dest="style", default="xhtmltable",
help="format log according to specific style"
" (default: xhtmltable); try -s help for a list of"
" available styles")
parser.add_option('-t', '--title', dest="title", default=None,
help="title of the page (default: same as file name)")
parser.add_option('--prev-title', dest="prev_title", default='',
help="title of the previous page (default: none)")
parser.add_option('--prev-url', dest="prev_url", default='',
help="URL of the previous page (default: none)")
parser.add_option('--index-title', dest="index_title", default='',
help="title of the index page (default: none)")
parser.add_option('--index-url', dest="index_url", default='',
help="URL of the index page (default: none)")
parser.add_option('--next-title', dest="next_title", default='',
help="title of the next page (default: none)")
parser.add_option('--next-url', dest="next_url", default='',
help="URL of the next page (default: none)")
parser.add_option('-S', '--searchbox', action="store_true", dest="searchbox",
default=False,
help="include a search box")
for name, default, what in COLOURS:
parser.add_option('--color-%s' % name, '--colour-%s' % name,
dest="colour_%s" % name, default=default,
help="select %s colour (default: %s)"
% (name, default))
options, args = parser.parse_args(argv[1:])
if options.style == "help":
print "The following styles are available for use with irclog2html.py:"
for style in STYLES:
print
print " %s" % style.name
print " %s" % style.description
print
return
for style in STYLES:
if style.name == options.style:
break
else:
parser.error("unknown style: %s" % style)
colours = {}
for name, default, what in COLOURS:
colours[what] = getattr(options, 'colour_%s' % name)
if not args:
parser.error("required parameter missing")
title = options.title
prev = (options.prev_title, options.prev_url)
index = (options.index_title, options.index_url)
next = (options.next_title, options.next_url)
for filename in args:
try:
infile = open(filename)
except EnvironmentError, e:
sys.exit("%s: cannot open %s for reading: %s"
% (progname, filename, e))
outfilename = filename + ".html"
try:
outfile = open(outfilename, "w")
except EnvironmentError, e:
infile.close()
sys.exit("%s: cannot open %s for writing: %s"
% (progname, outfilename, e))
try:
parser = LogParser(infile)
formatter = style(outfile, colours)
convert_irc_log(parser, formatter, title or filename,
prev, index, next, searchbox=options.searchbox)
finally:
outfile.close()
infile.close()
def convert_irc_log(parser, formatter, title, prev, index, next,
searchbox=False):
"""Convert IRC log to HTML or some other format."""
nick_colour = NickColourizer()
formatter.head(title, prev, index, next, searchbox=searchbox)
for time, what, info in parser:
if what == LogParser.COMMENT:
nick, text = info
htmlcolour = nick_colour[nick]
formatter.nicktext(time, nick, text, htmlcolour)
else:
if what == LogParser.NICKCHANGE:
text, oldnick, newnick = info
nick_colour.change(oldnick, newnick)
else:
text = info
formatter.servermsg(time, what, text)
formatter.foot()
if __name__ == '__main__':
main()
IRC/irc-techrights-log-to-gemtext.pl
#!/usr/bin/perl
# 2021-12-04
# see Git/IRC/ for versioning
use utf8;
use warnings;
use strict;
# binmode(STDIN, ":encoding(utf8)");
# binmode(STDOUT, ":encoding(utf8)");
use open qw(:std :utf8);
my @records = ();
my $oldday = 0;
my $oldhour = 0;
my $max = 0;
while (my $rec = <> ) {
$rec =~ s/\s+$//;
$rec =~ s/\s+\+0000:\[([\S]+)\]+\s*/\t$1\t/;
my @line = split(/\t/, $rec);
my @datetime = split(/[, ]+/,$line[0]);
# spell the day out fully
my %d = (
'Mon' => 'Monday',
'Tue' => 'Tuesday',
'Wed' => 'Wednesday',
'Thu' => 'Thursday',
'Fri' => 'Friday',
'Sat' => 'Saturday',
'Sun' => 'Sunday',
);
$datetime[0] = $d{$datetime[0]};
if ($oldday ne $datetime[0]) {
$oldday = $datetime[0];
push(@records,"\n## ".$datetime[0]);
}
# print the day only at the first post of the day
$datetime[0] = "";
# space between hours
my ($hour) = ($datetime[1] =~ m/^([0-9]{2})/);
if ($hour != $oldhour) {
$oldhour=$hour;
push(@records,"");
}
$line[0] = join(" ", @datetime);
if($line[1] eq "techrights-news") {
$line[1] = "TR News";
if($line[2] =~ m/Yesterday's bulletin is now ready/) {
my ($gemini) = ($line[2] =~ m{(gemini\S+)});
my ($www) = ($line[2] =~ m{(http\S+)});
$line[2] = "Yesterday's bulletin is now ready "
. $gemini . " " . $www
. "\n=> $gemini";
}
} elsif($line[1] eq "techrights-ipfs-bot") {
$line[1] = "TR IPFS";
$line[2] =~ s{^\D+([\.0-9]+)\D+([\.0-9]+)\D+([\.0-9]+)\D+([\.0-9]+).*}
{$1 minutes, downstream average $2 k/sec,
upstream average $3 k/sec, average swarm size $4.}gx;
}
my $l = length($line[1]);
if($l>$max) {
$max=$l;
}
push(@records, join("\t", @line));
my @links =();
if (length($line[2])) {
while ($line[2] =~ m{(https?:/\S+)}gc ||
$line[2] =~ m{(gemini:/\S+)}gc ||
$line[2] =~ m{(gopher:/\S+)}gc ) {
my $link = $1;
push(@links,$link);
}
}
foreach my $link (@links) {
if($link =~ m{techrights\.org/}) {
push(@records, "=> $link $link\n");
} else {
push(@records, "=> $link βΊ $link\n");
}
}
}
print "# Full IRC logs for the last few days.\n";
print "This Gemini page updates once every 5-10 minutes.\n";
print "=> / back to Techrights (Main Index)\n";
for my $rec (@records) {
my @line = split(/\t/, $rec);
if(exists($line[1])) {
$line[1] = sprintf("%*s;", $max, $line[1]);
}
print join(" ", @line),"\n";
}
my (undef,$minute,$hour,$day,$month,$year,undef,undef,undef) = gmtime(time);
$month += 1;
$year += 1900;
$day = sprintf("%02d", $day);
$month = sprintf("%02d", $month);
$minute = sprintf("%02d", $minute);
$hour = sprintf("%02d", $hour);
print "\n### Last updated $year-$month-$day at $hour:$minute UTC\n";
print "=> /index.gmi Techrights\n";
exit(0);
IRC/xhtml-log-to-text.pl
#!/usr/bin/perl
# 2020-11-20
# read IRC logs from an XHTML document's table(s) and convert to plain text
use Getopt::Std;
use File::Glob ':bsd_glob';
use HTML::TreeBuilder::XPath;
use warnings;
use strict;
our %opt;
getopts('f:gh', \%opt);
&usage if ($opt{'h'});
my $output = $opt{'f'} if ($opt{'f'});
# get full list of all individual files, also expanding the globs
my @filenames;
while (my $file = shift) {
my @files = bsd_glob($file);
push(@filenames, @files);
}
if (-p STDIN) {
# also read from stdin if a pipe is active
unshift(@filenames, '/dev/stdin')
} elsif ($#filenames < 0 && ! -t STDIN) {
# if there are not file names and TTY is false
# then there is probably input via a direcect and stdin is needed
unshift(@filenames, '/dev/stdin')
}
# show usage and quit, if no data sources are given
&usage unless($#filenames >= 0);
my $out;
if($output) {
if (-e $output) {
print STDERR qq("$output" already exists!\n);
exit(1);
}
open($out, ">", $output)
or die("Could not open '$output' for writing: $!\n");
}
while (my $infile = shift(@filenames)) {
my @result = &process_text($infile);
if ($output) {
print $out @result;
} else {
print @result;
}
}
if ($output) {
close($out)
or die("Could not close '$output' : $!\n");
}
exit(0);
sub usage {
print qq(Read IRC logs from HTML tables and convert them to text.\n);
print qq(Output goes to STDOUT unless the -f option designates a file for writing.\n);
print qq(Output is in Gemtext with the -g option.\n);
$0 =~ s/^.*\///;
print qq($0: file [file...]\n);
exit(1);
}
sub process_text {
my ($file) = (@_);
my @result = ();
my $xhtml = HTML::TreeBuilder::XPath->new;
$xhtml->implicit_tags(1);
$xhtml->parse_file($file)
or die("Could not parse '$file' : $!\n");
my $title = $xhtml->findnodes('//h1[1]');
if($opt{'g'}) {
push (@result, qq(# $title\n));
push (@result, qq(=> / back to Techrights (Main Index)\n));
} else {
push (@result, qq(ββ $title ββ\n));
}
for my $table ($xhtml->findnodes('//table[@class="irclog"]')) {
my $old_day = "";
my $old_hour = "";
my $r=0;
my %month = (
'Jan' => 'January',
'Feb' => 'February',
'Mar' => 'March',
'Apr' => 'April',
'May' => 'May',
'Jun' => 'June',
'Jul' => 'July',
'Aug' => 'August',
'Sep' => 'September',
'Oct' => 'October',
'Nov' => 'November',
'Dec' => 'December',
);
for my $row ($table->findnodes('./tr')) {
my $nick = $row->findvalue('./th[@class="nick"]');
my $stamp = $row->attr('id');
$stamp =~ s/^t//;
next unless ($stamp);
my $text = $row->findvalue('./td[@class="text"]');
if( ! $text) {
$text = $row->findvalue('./td[@class="other"]');
if($text =~ s/^-(TechrightsBot\S+)\s\|?//) {
$nick = 'TR Bot';
} elsif($text =~ s/^-(altlink\S+)\s\|?//) {
$nick = 'Alternative link';
}
}
if($nick eq 'techrights-news') {
$nick = 'TR News';
$text =~ s/^.*(Yesterday)/$1/;
$text =~ s/^.*β NEWS β /News: /;
$text =~ s/π
·πππ
Ώ:/ /;
$text =~ s/\s+\| π
Άπ
΄π
Όπ
Έπ
½π
Έ /; /;
} elsif($nick eq 'techrights-ipfs-bot') {
$nick = 'IPFS';
# too brittle
if(my ($m, $d, $u, $s) = ($text =~ m/.*downstream
\D*([\.0-9]+)
\D*([\.0-9]+)
\D*([\.0-9]+)
\D*([\.0-9]+)
/x)) { $text = "IPFS downstream $m minutes average $d k/sec., " .
"IPFS upstream $u average k/sec., " .
"average swarm size $s";
}
}
if (! $text) {
$text = $row->findvalue('./td[@class="action"]');
}
if (! $text) {
$text = $row->findvalue('./td[@class="other"]');
}
$text =~ s/\x{00A0}/ /gm;
my ($day, $time) = ($stamp =~ m/^(.*)\s+(\d{2}:\d{2}:\d{2})$/);
my ($hour, $minute, $second) = ($time =~ m/^(\d{2}):(\d{2}):(\d{2})$/);
if ($old_hour ne $hour) {
if($opt{'g'}) {
my $h;
if ($hour == 0) {
$h = 'beginning of new day';
} elsif($hour < 12) {
$h = $hour % 12 . ' AM';
} elsif($hour == 12) {
$h ='noon';
} else {
$h = $hour % 12 . ' PM';
}
my ($m, $d) = ($day=~m/^(\w{3})\s+0?([0-9]+)$/);
$m = exists($month{$m}) ? $month{$m} : $m;
push(@result, qq(\n## $h, $m $d\n));
} else {
push(@result, qq(β $day\n));
}
} elsif ($old_day ne $day) {
if($opt{'g'}) {
my ($m, $d) = ($day=~m/^(\w{3})\s+0?([0-9]+)$/);
push(@result, qq(\n## $m $d\n));
} else {
push(@result, qq(\nβ $day\n));
}
}
$old_hour = $hour;
$old_day = $day;
if($opt{'g'}) {
# simplify timestamps for gemtext
$nick = $nick ? $nick.';' : '';
push(@result, qq($hour:$minute\t$nick $text\n));
# make gemtext links from url-like strings
my @links =();
while ($text =~ m{(https?://\S+)}gc ) {
my $link = $1;
push(@links,$link);
}
while ($text =~ m{(gemini://\S+)}gc ) {
my $link = $1;
push(@links,$link);
}
foreach my $link (@links) {
if($link =~ m{techrights\.org/}) {
push(@result, "=> $link $link\n");
} else {
push(@result, "=> $link βΊ $link\n");
}
}
} else {
push(@result, qq([$hour:$minute]\t$nick\t$text\n));
}
}
}
if($opt{'g'}) {
push (@result, qq(\n# $title\n\n));
push (@result, qq(=> / back to Techrights (Main Index)\n));
}
$xhtml->delete;
return(@result);
}
IRC/yesterday-irc.log.sh
#!/bin/bash
# some thresholds
sizetop=10000
sizediff=10000
tail -n${sizetop} .xchat2/xchatlogs/FreeNode-#techrights.log \
| head -n${sizediff} > ~/LAPTOP-FreeNode-#techrights.log
tail -n${sizetop} .xchat2/xchatlogs/FreeNode-#boycottnovell.log \
| head -n${sizediff} > ~/LAPTOP-FreeNode-#boycottnovell.log
tail -n${sizetop} .xchat2/xchatlogs/FreeNode-#boycottnovell-social.log \
| head -n${sizediff} > ~/LAPTOP-FreeNode-#boycottnovell-social.log
tail -n${sizetop} .xchat2/xchatlogs/FreeNode-#techbytes.log \
| head -n${sizediff} > ~/LAPTOP-FreeNode-#techbytes.log
tail -n${sizetop} .xchat2/xchatlogs/FreeNode-#techpol.log \
| head -n${sizediff} > ~/LAPTOP-FreeNode-#techpol.log
# kate ~/LAPTOP-FreeNode-#techrights.log ~/LAPTOP-FreeNode-#boycottnovell.log ~/LAPTOP-FreeNode-#boycottnovell-social.log ~/LAPTOP-FreeNode-#techbytes.log
# sleep 600 # time to merge in missing logs (if any)
# e.g. MERGED IN: Self-hosted IRC logs below
for i in {1..1}
do
IRCAGE=$i
IRCDATE=$(date --date="$IRCAGE days ago" +"%b %d")
#IRCDATE=$(date --date yesterday +"%b %d")
echo Processing $IRCDATE
IRCFULLDATE=$(date --date="$IRCAGE days ago" "+%A, %B %d, %Y")
#IRCFULLDATE=$(date --date yesterday "+%A, %B %d, %Y")
echo Full date: $IRCFULLDATE
IRCDATESLUG=$(date --date="$IRCAGE days ago" +"%d%m%y")
#IRCDATESLUG=$(date --date yesterday +"%d%m%y")
echo Slug: $IRCDATESLUG
IRCDATEFILE=$(date --date="$IRCAGE days ago" +"%Y-%m-%d")
# IRCDATEFILE=bulk # for BULK runs with one combined output file change 'echo "" > $IRCDATEFILE.txt' to 'echo "" >> $IRCDATEFILE.txt'
grep "^$IRCDATE" ~/LAPTOP-FreeNode-#techrights.log > irc-log-techrights.daily
grep "^$IRCDATE" ~/LAPTOP-FreeNode-#boycottnovell.log > irc-log-boycottnovell.daily
grep "^$IRCDATE" ~/LAPTOP-FreeNode-#boycottnovell-social.log > interim-irc-log-boycottnovell-social.daily
grep "^$IRCDATE" ~/LAPTOP-FreeNode-#techbytes.log > irc-log-techbytes.daily
grep "^$IRCDATE" ~/LAPTOP-FreeNode-#techpol.log > irc-log-techpol.daily
cat interim-irc-log-boycottnovell-social.daily irc-log-techpol.daily > irc-log-boycottnovell-social.daily
# merge the two channel logs
# sleep 600 # time to merge in missing logs (if any)
python ./Main/Programs/irclog2html.py irc-log-techrights.daily \
--title="IRC: #techrights @ Techrights IRC Network: $IRCFULLDATE"
python ./Main/Programs/irclog2html.py irc-log-boycottnovell.daily \
--title="IRC: #boycottnovell @ Techrights IRC Network: $IRCFULLDATE"
python ./Main/Programs/irclog2html.py irc-log-boycottnovell-social.daily \
--title="IRC: #boycottnovell-social and #techpol @ Techrights IRC Network: $IRCFULLDATE"
python ./Main/Programs/irclog2html.py irc-log-techbytes.daily \
--title="IRC: #techbytes @ Techrights IRC Network: $IRCFULLDATE"
# cp irc-log-techrights.daily.html irc-log-techrights-$IRCDATESLUG.html
sed "s/TEXTVERSIONSLUG/irc-log-techrights-${IRCDATESLUG}.txt/" irc-log-techrights.daily.html \
| sed "s/TEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/tr_text_version\/irc-log-techrights-${IRCDATESLUG}.txt/" \
| sed "s/GEMTEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/irc-gmi\/irc-log-techrights-${IRCDATESLUG}.gmi/" \
> irc-log-techrights-$IRCDATESLUG.html
# cp irc-log-boycottnovell.daily.html irc-log-$IRCDATESLUG.html
sed "s/TEXTVERSIONSLUG/irc-log-${IRCDATESLUG}.txt/" irc-log-boycottnovell.daily.html \
| sed "s/TEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/tr_text_version\/irc-log-${IRCDATESLUG}.txt/" \
| sed "s/GEMTEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/irc-gmi\/irc-log-${IRCDATESLUG}.gmi/" \
> irc-log-$IRCDATESLUG.html
# cp irc-log-boycottnovell-social.daily.html irc-log-social-$IRCDATESLUG.html
sed "s/TEXTVERSIONSLUG/irc-log-social-${IRCDATESLUG}.txt/" irc-log-boycottnovell-social.daily.html \
| sed "s/TEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/tr_text_version\/irc-log-social-${IRCDATESLUG}.txt/" \
| sed "s/GEMTEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/irc-gmi\/irc-log-social-${IRCDATESLUG}.gmi/" \
> irc-log-social-$IRCDATESLUG.html
# cp irc-log-techbytes.daily.html irc-log-techbytes-$IRCDATESLUG.html
sed "s/TEXTVERSIONSLUG/irc-log-techbytes-${IRCDATESLUG}.txt/" irc-log-techbytes.daily.html \
| sed "s/TEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/tr_text_version\/irc-log-techbytes-${IRCDATESLUG}.txt/" \
| sed "s/GEMTEXTVERSIONGEMINI/gemini:\/\/gemini.techrights.org\/irc-gmi\/irc-log-techbytes-${IRCDATESLUG}.gmi/" \
> irc-log-techbytes-$IRCDATESLUG.html
echo "" > $IRCDATEFILE.txt
echo " Post title:" >> $IRCDATEFILE.txt
echo "IRC Proceedings: $IRCFULLDATE" >> $IRCDATEFILE.txt
echo "" >> $IRCDATEFILE.txt
echo " Post excerpt (to add on the right), don't forget to pick IRC log category:" >> $IRCDATEFILE.txt
echo "IRC logs for $IRCFULLDATE" >> $IRCDATEFILE.txt
echo "" >> $IRCDATEFILE.txt
echo " Post slug (put below title):" >> $IRCDATEFILE.txt
echo "irc-log-$IRCDATESLUG" >> $IRCDATEFILE.txt
echo "" >> $IRCDATEFILE.txt
echo " Post body" >> $IRCDATEFILE.txt
echo "" >> $IRCDATEFILE.txt
echo "<table align=\"center\">" >> $IRCDATEFILE.txt
echo "<tbody>" >> $IRCDATEFILE.txt
echo "<tr>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-techrights-$IRCDATESLUG.html\" title=\"Read the log\"><img src=\"/wp-content/uploads/2008/03/116px-Gartoon-Gedit-icon.png\" border=\"0\" hspace=\"20\" vspace=\"4\" alt=\"GNOME Gedit\" /></a></p></td>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-$IRCDATESLUG.html\" title=\"Read the log\"><img src=\"/wp-content/uploads/2008/03/116px-Gartoon-Gedit-icon.png\" border=\"0\" hspace=\"20\" vspace=\"4\" alt=\"GNOME Gedit\" /></a></p></td>" >> $IRCDATEFILE.txt
echo "</tr>" >> $IRCDATEFILE.txt
echo "<tr>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-techrights-$IRCDATESLUG.html\" title=\"Read the log\">#techrights log</a></p></td>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-$IRCDATESLUG.html\" title=\"Read the log\">#boycottnovell log</a></p></td>" >> $IRCDATEFILE.txt
echo "</tr>" >> $IRCDATEFILE.txt
echo "<tr>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-social-$IRCDATESLUG.html\" title=\"Read the log\"><img src=\"/wp-content/uploads/2008/03/116px-Gartoon-Gedit-icon.png\" border=\"0\" hspace=\"20\" vspace=\"4\" alt=\"GNOME Gedit\" /></a></p></td>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-techbytes-$IRCDATESLUG.html\" title=\"Read the log\"><img src=\"/wp-content/uploads/2008/03/116px-Gartoon-Gedit-icon.png\" border=\"0\" hspace=\"20\" vspace=\"4\" alt=\"GNOME Gedit\" /></a></p></td>" >> $IRCDATEFILE.txt
echo "</tr>" >> $IRCDATEFILE.txt
echo "<tr>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-social-$IRCDATESLUG.html\" title=\"Read the log\">#boycottnovell-social log</a></p></td>" >> $IRCDATEFILE.txt
echo "<td><p align=\"center\"><a href=\"/irc-archives/irc-log-techbytes-$IRCDATESLUG.html\" title=\"Read the log\">#techbytes log</a></p></td>" >> $IRCDATEFILE.txt
echo "</tr>" >> $IRCDATEFILE.txt
echo "</tbody>" >> $IRCDATEFILE.txt
echo "</table>" >> $IRCDATEFILE.txt
# perl xhmtl-log-to-text.pl irc-log-$IRCDATESLUG.html > irc-log-$IRCDATESLUG.txt
# perl xhmtl-log-to-text.pl irc-log-social-$IRCDATESLUG.html > irc-log-social-$IRCDATESLUG.txt
# iconv -c -f utf-8 -t ascii irc-log-techrights-$IRCDATESLUG.html | perl xhtml-log-to-text.pl > irc-log-techrights-$IRCDATESLUG.txt
# perl xhmtl-log-to-text.pl irc-log-techbytes-$IRCDATESLUG.html > irc-log-techbytes-$IRCDATESLUG.txt
# Now generate txt and GemText
for f in ./irc-log-*$(date -d "-$IRCAGE day" +"%d%m%y").html;
do
echo Processing $f;
t=${f%%.html}.txt;
g=${f%%.html}.gmi;
iconv -c -f utf-8 -t ascii $f | perl ./xhtml-log-to-text.pl \
> $t;
iconv -c -f utf-8 -t ascii $f | perl ./xhtml-log-to-text.pl -g \
> $g;
echo $t ............................... DONE;
done
done
echo "<p align=\"center\">" >> $IRCDATEFILE.txt
echo "<font size=\"5\"><a href=\"/irc-channel/\" title=\"IRC Channel\">Enter the IRC channels now</a></font>" >> $IRCDATEFILE.txt
echo "</p>" >> $IRCDATEFILE.txt
# put higher (NESTED) the lines below for multiple dates processed, or MANUALLY send the text file only once at the end
# sleep 600 # UNCOMMENT TO EDIT files for 10 minutes at most - time to redact, making missing bits in the logs for privacy reasons
echo '========= Sending IRC to Techrights ========= '
echo '========= Sending IRC to Raspi ========= '
echo ' == Text and HTML'
# alert in IRC
echo "π
Έππ
² boycottnovell irc β Yesterday's #boycottnovell IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#boycottnovell-social/in
sleep 60
echo "π
Έππ
² techbytes irc β Yesterday's #techbytes IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-techbytes-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-techbytes-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-techbytes-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-techbytes-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#boycottnovell-social/in
sleep 60
echo "π
Έππ
² techpol + social irc β Yesterday's #boycottnovell-social and #techpol IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-social-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-social-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-social-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-social-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#boycottnovell-social/in
sleep 60
echo "π
Έππ
² techrights irc β Yesterday's #techrights IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-techrights-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-techrights-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-techrights-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-techrights-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#boycottnovell-social/in
sleep 180
echo "π
Έππ
² techpol + social irc β Yesterday's #boycottnovell-social and #techpol IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-social-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-social-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-social-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-social-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#techrights/in
sleep 60
echo "π
Έππ
² techrights irc β Yesterday's #techrights IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-techrights-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-techrights-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-techrights-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-techrights-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#techrights/in
sleep 60
echo "π
Έππ
² boycottnovell irc β Yesterday's #boycottnovell IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#techrights/in
sleep 60
echo "π
Έππ
² techbytes irc β Yesterday's #techbytes IRC logs ready. HTML: http://techrights.org/irc-archives/irc-log-techbytes-$IRCDATESLUG.html TEXT: http://techrights.org/irc-archives/irc-log-techbytes-$IRCDATESLUG.txt GEMINI GemText: gemini://gemini.techrights.org/irc-gmi/irc-log-techbytes-$IRCDATESLUG.gmi GEMINI Plain Text: gemini://gemini.techrights.org/tr_text_version/irc-log-techbytes-$IRCDATESLUG.txt" > ~/Desktop/Text_Workspace/images/ii-1.8/techrights3/irc.techrights.org/#techrights/in
echo "Generating text bulletins (it can take a minute), next batch at 5AM"
./text-upload.sh
echo "Check resultant text"
sleep 20
IRC/config.php
<?php
/* phIRCe - PHP IRC bot
* Copyright (C) 2009 Tony Manco <trmanco@gmx.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*-----------------------------START CONFIG--------------------------------*/
// Be slightly paranoid during testing. If you don't wish to run phirce
// as a separate user, comment out the following 4 lines.
# $uinfo = posix_getpwuid(posix_getuid());
## die_unless($uinfo['name'] == 'phirce',
## Please run as 'phirce' user: sudo -u phirce php -q {$argv[0]}\r\n"
## or disable this check in config.php");
set_time_limit(0);
error_reporting(E_ALL);
//List of authorized cloaks/hostnames to command the bot
$allowed_users=array("trmanco@unaffiliated/trmanco");
//List of bad words
$bad_words=array("fuck",
"asshole",
"bitch",
"fag",
"shit",
"moron",
"assfuck",
"assfucker",
"dumbfuck",
"faggot ",
"fucker",
"jackass",
"motherfucker",
"shitface",
"pussy");
// Insert the names of the modules you want to use
$modules=array("HelloWorld");
$pass = "";
$host = "irc.techrights.org";
$port = 6667;
$nick = "TechrightsBot-tr"; // change to something unique.
$ident = "TR";
$chan = "#techrights";
// To enable features based on the Twitter API v1.1, obtain application
// credentials from Twitter at https://dev.twitter.com/apps
// DO NOT GIVE THESE TO ANYONE OR ALLOW TO BECOME PUBLIC.
$twitter_ckey = '[redacted]';
$twitter_csecret = '[redacted]';
// The following two fields are only required for OAuth usage of Twitter API.
// (Not currently used.)
$twitter_token = '[redacted]';
$twitter_tsecret = '[redacted]';
$realname = "Techrights";
$tmppath = "/root/phirce-techrights/tmp"; // Put tmp directory here
$logpath = "/root/phirce-techrights/logs"; // Put logs directory here
$arcpath = "/root/phirce-techrights/archive"; // Put page archive directory here
$modpath = "/root/phirce-techrights/modules"; // Put modules directory here
//Set user agent so that all sites can be fetched
//Set user agent so that all sites can be fetched
$useragent= "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100105 Firefox/3.6";
$log = 1;
$archive = 1;
/*-------------------------------END CONFIG--------------------------------*/
IRC/.directory-listing-ok
IRC/irc-table.sh
#!/bin/sh
# 2020-02-17
# print out estimated IRC log links
# updated 2021-01-29
PATH=/bin:/usr/bin:/usr/local/bin
if test -z "${DISPLAY}"; then
export DISPLAY=:0.0
fi
d=$(date -d yesterday +"%F")
title=$(date -d $d +"%A, %B %d, %Y")
slug=$(date -d $d +"irc-log-%d%m%y")
log=$(date -d $d +"%d%m%y.html")
logt=$(date -d $d +"%d%m%y.txt")
t=$(mktemp) || exit 1
# trap any kind of exit and remove the temp file on the way out
trap "echo 'Cleaning up'; rm -f -- '$t'" EXIT TERM INT
# must use absolute paths because emtpy strings cause test to return TRUE
if test -x /usr/bin/gedit; then
editor=/usr/bin/gedit
elif test -x /usr/bin/mousepad; then
editor=/usr/bin/mousepad
fi
echo > $t
# command="test -f /home/boycottn/public_html/irc-archives/irc-log-techrights-$(date -d '1 days ago' +'%d%m%y').html"
echo "Checking remote server for IRC logs."
if ! ssh -i ~/.ssh/tr-irc-checker tr; then
url="http://techrights.org/irc-archives/irc-log-techrights-$(date -d $d +'%d%m%y').html";
if ! wget -q -O /dev/null "$url"; then
day=$(date -u +'%F %T %Z')
echo "No IRC file for $d yet as of $day.\n"
read -p 'Continue? (y/n) ' continue
continue=$(echo $continue | tr -d ' ')
continue=$(echo $continue | tr 'A-Z' 'a-z')
if test "x$continue" != 'xyes' \
&& test "x$continue" != 'xy'; then
exit 1
fi
echo "No IRC file for $d yet as of $day.\n" >> $t
fi
fi
cat <<EOSLUG >> $t
Post title:
IRC Proceedings: $title
Post excerpt (to add on the right), don't forget to pick IRC log category:
IRC logs for $title
Post slug (put below title):
$slug
EOSLUG
cat <<EOTABLE >>$t
Post body
<table align="center">
<tbody>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-techrights-$log" title="Read the log"><img src="http://techrights.org/images/irc/hypertext.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="HTML5 logs" /></a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-$log" title="Read the log"><img src="http://techrights.org/images/irc/hypertext.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="HTML5 logs" /></a></p></td>
</tr>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-techrights-$log" title="Read the log">#techrights log as HTML5</a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-$log" title="Read the log">#boycottnovell log as HTML5</a></p></td>
</tr>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-social-$log" title="Read the log"><img src="http://techrights.org/images/irc/hypertext.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="HTML5 logs" /></a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-techbytes-$log" title="Read the log"><img src="http://techrights.org/images/irc/hypertext.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="HTML5 logs" /></a></p></td>
</tr>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-social-$log" title="Read the log">#boycottnovell-social log as HTML5</a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-techbytes-$log" title="Read the log">#techbytes log as HTML5</a></p></td>
</tr>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-techrights-$logt" title="Read the log"><img src="http://techrights.org/images/irc/text.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="text logs" /></a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-$logt" title="Read the log"><img src="http://techrights.org/images/irc/text.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="text logs" /></a></p></td>
</tr>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-techrights-$logt" title="Read the log">#techrights log as text</a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-$logt" title="Read the log">#boycottnovell log as text</a></p></td>
</tr>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-social-$logt" title="Read the log"><img src="http://techrights.org/images/irc/text.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="text logs" /></a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-techbytes-$logt" title="Read the log"><img src="http://techrights.org/images/irc/text.png" border="0" hspace="20" vspace="4" width="96" height="136" alt="text logs" /></a></p></td>
</tr>
<tr>
<td><p align="center"><a href="/irc-archives/irc-log-social-$logt" title="Read the log">#boycottnovell-social log as text</a></p></td>
<td><p align="center"><a href="/irc-archives/irc-log-techbytes-$logt" title="Read the log">#techbytes log as text</a></p></td>
</tr>
</tbody>
</table>
<p align="center">
<font size="5"><a href="/irc-channel/" title="IRC Channel">Enter the IRC channels now</a></font>
</p>
EOTABLE
# get IPFS site material
ipfsfile=$(date -d $d +"%y%m%d.html");
if ! ssh -i ~/.ssh/techrights-th-links-automated.ed25519 -l links \
-o addkeystoagent=yes -o identitiesonly=yes th \
"cat /home/links/ipfs/$ipfsfile" >>$t
then
echo "Could not fetch IPFS log summary"
exit 1
fi
$editor "$t"
test -f "$t" && rm -f -- "$t"
trap - EXIT
exit 0
# 2020-11-09 added IPFS
# 2020-11-21 added text-only, new icons with HTTP full-path
# 2021-01-22 fail if IPFS logs are not accessible
IRC/bot.php
<?php
/* phIRCe - PHP IRC bot
* Copyright (C) 2009-2013 Tony Manco <trmanco@gmx.com>
* Portions (C) 2010-2013 Toby Thain <toby@telegraphics.com.au>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (PHP_SAPI != 'cli') {
die("Please run from CLI!\r\n");
}
function die_unless($success, $error_str){
if(!$success){
fprintf(STDERR, $error_str);
die(1);
}
}
require 'config.php';
require_once('Class.phIRCe.php');
echo "Connecting and joining...\r\n";
$ircbot = new phirce($nick, $pass, $ident, $realname, $host, $port, $chan,
$tmppath, $useragent, $logpath, $log, $arcpath, $archive, $modpath);
if($twitter_ckey && $twitter_csecret) {
echo "Authenticating with Twitter... ";
$ircbot->TwitterSetCredentials($twitter_ckey, $twitter_csecret, $twitter_token, $twitter_tsecret);
die_unless($ircbot->TwitterAuth(),
'Failed; please check Twitter credentials (or blank them to proceed)\r\n');
echo "OK\r\n";
}
echo "Identifing to NickSERV...\r\n";
$ircbot->Nickserv($pass);
echo "Loading allowed user list...\r\n";
$ircbot->allowed($allowed_users);
echo "Loading bad word list...\r\n";
$ircbot->badwords($bad_words);
echo "Loading modules...\r\n";
$ircbot->modules($modules);
echo "Working...\r\n";
$ircbot->loop();
echo "Disconnecting... \r\n";
$ircbot->quit();
echo "Disconnected!\r\n";
IRC/irclog2html.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Convert IRC logs to HTML.
Usage: irclog2html.py filename
irclog2html will write out a colourised irc log, appending a .html
extension to the output file.
This is a Python port (+ improvements) of irclog2html.pl Version 2.1, which
was written by Jeff Waugh and is available at www.perkypants.org
"""
# Copyright (c) 2005, Marius Gedminas
# Copyright (c) 2000, Jeffrey W. Waugh
# Python port:
# Marius Gedminas <marius@pov.lt>
# Original Author:
# Jeff Waugh <jdub@perkypants.org>
# Contributors:
# Rick Welykochy <rick@praxis.com.au>
# Alexander Else <aelse@uu.net>
#
# Released under the terms of the GNU GPL
# http://www.gnu.org/copyleft/gpl.html
# Differences from the Perl version:
# There are no hardcoded nick colour preferences for jdub, cantanker and
# chuckd
#
# Colours are preserver accross nick changes (irclog2html.pl tries to do
# that, but its regexes are buggy)
#
# irclog2html.pl interprets --colour-server #rrggbb as -s #rrggbb,
# irclog2html.py does not have this bug
#
# irclog2html.py understands ISO 8601 timestamps (YYYY-MM-DDTHH:MM:SS)
#
# New options: --title, --{prev,index,next}-{url,title}
#
# New styles: xhtml, xhtmltable
#
# New default style: xhtmltable
#
import os
import re
import sys
import urllib
import optparse
VERSION = "2.6"
RELEASE = "2007-10-30"
# $Id$
#
# Log parsing
#
class Enum(object):
"""Enumerated value."""
def __init__(self, value):
self.value = value
def __repr__(self):
return self.value
class LogParser(object):
"""Parse an IRC log file.
When iterated, yields the following events:
time, COMMENT, (nick, text)
time, ACTION, text
time, JOIN, text
time, PART, text,
time, NICKCHANGE, (text, oldnick, newnick)
time, SERVER, text
"""
COMMENT = Enum('COMMENT')
ACTION = Enum('ACTION')
JOIN = Enum('JOIN')
PART = Enum('PART')
NICKCHANGE = Enum('NICKCHANGE')
SERVER = Enum('SERVER')
OTHER = Enum('OTHER')
TIME_REGEXP = re.compile(
r'^\[?(' # Optional [
r'(?:\d{4}-\d{2}-\d{2}T|\d{2}-\w{3}-\d{4} |\w{3} \d{2} )?' # Optional date
r'\d\d:\d\d(:\d\d)?' # Mandatory HH:MM, optional :SS
r')\]? +') # Optional ], mandatory space
NICK_REGEXP = re.compile(r'^<(.*?)>\s')
JOIN_REGEXP = re.compile(r'^(?:\*\*\*|-->)\s.*joined')
PART_REGEXP = re.compile(r'^(?:\*\*\*|<--)\s.*(quit|left)')
SERVMSG_REGEXP = re.compile(r'^(?:\*\*\*|---)\s')
NICK_CHANGE_REGEXP = re.compile(
r'^(?:\*\*\*|---)\s+(.*?) (?:are|is) now known as (.*)')
def __init__(self, infile):
self.infile = infile
def __iter__(self):
for line in self.infile:
line = line.rstrip('\r\n')
if not line:
continue
m = self.TIME_REGEXP.match(line)
if m:
time = m.group(1)
line = line[len(m.group(0)):]
else:
time = None
m = self.NICK_REGEXP.match(line)
if m:
nick = m.group(1)
text = line[len(m.group(0)):]
yield time, self.COMMENT, (nick, text)
elif line.startswith('* ') or line.startswith('*\t'):
yield time, self.ACTION, line
elif self.JOIN_REGEXP.match(line):
yield time, self.JOIN, line
elif self.PART_REGEXP.match(line):
yield time, self.PART, line
else:
m = self.NICK_CHANGE_REGEXP.match(line)
if m:
oldnick = m.group(1)
newnick = m.group(2)
yield time, self.NICKCHANGE, (line, oldnick, newnick)
elif self.SERVMSG_REGEXP.match(line):
yield time, self.SERVER, line
else:
yield time, self.OTHER, line
def shorttime(time):
"""Strip date and seconds from time.
>>> shorttime('12:45:17')
'12:45'
>>> shorttime('12:45')
'12:45'
>>> shorttime('2005-02-04T12:45')
'12:45'
"""
if 'T' in time:
time = time.split('T')[-1]
if time.count(':') > 1:
time = ':'.join(time.split(':')[:2])
return time
#
# Colouring stuff
#
class ColourChooser:
"""Choose distinguishable colours."""
def __init__(self, rgbmin=240, rgbmax=125, rgb=None, a=0.95, b=0.5):
"""Define a range of colours available for choosing.
`rgbmin` and `rgbmax` define the outmost range of colour depth (note
that it is allowed to have rgbmin > rgbmax).
`rgb`, if specified, is a list of (r,g,b) values where each component
is between 0 and 1.0.
If `rgb` is not specified, then it is constructed as
[(a,b,b), (b,a,b), (b,b,a), (a,a,b), (a,b,a), (b,a,a)]
You can tune `a` and `b` for the starting and ending concentrations of
RGB.
"""
assert 0 <= rgbmin < 256
assert 0 <= rgbmax < 256
self.rgbmin = rgbmin
self.rgbmax = rgbmax
if not rgb:
assert 0 <= a <= 1.0
assert 0 <= b <= 1.0
rgb = [(a,b,b), (b,a,b), (b,b,a), (a,a,b), (a,b,a), (b,a,a)]
else:
for r, g, b in rgb:
assert 0 <= r <= 1.0
assert 0 <= g <= 1.0
assert 0 <= b <= 1.0
self.rgb = rgb
def choose(self, i, n):
"""Choose a colour.
`n` specifies how many different colours you want in total.
`i` identifies a particular colour in a set of `n` distinguishable
colours.
Returns a string '#rrggbb'.
"""
if n == 0:
n = 1
r, g, b = self.rgb[i % len(self.rgb)]
m = self.rgbmin + (self.rgbmax - self.rgbmin) * float(n - i) / n
r, g, b = map(int, (r * m, g * m, b * m))
assert 0 <= r < 256
assert 0 <= g < 256
assert 0 <= b < 256
return '#%02x%02x%02x' % (r, g, b)
class NickColourizer:
"""Choose distinguishable colours for nicknames."""
def __init__(self, maxnicks=30, colour_chooser=None, default_colours=None):
"""Create a colour chooser for nicknames.
If you know how many different nicks there might be, specify that
numer as `maxnicks`. If you don't know, don't worry.
If you really want to, you can specify a colour chooser. Default is
ColourChooser().
If you want, you can specify default colours for certain nicknames
(`default_colours` is a mapping of nicknames to HTML colours, that is
'#rrggbb' strings).
"""
if colour_chooser is None:
colour_chooser = ColourChooser()
self.colour_chooser = colour_chooser
self.nickcount = 0
self.maxnicks = maxnicks
self.nick_colour = {}
if default_colours:
self.nick_colour.update(default_colours)
def __getitem__(self, nick):
colour = self.nick_colour.get(nick)
if not colour:
self.nickcount += 1
if self.nickcount >= self.maxnicks:
self.maxnicks *= 2
colour = self.colour_chooser.choose(self.nickcount, self.maxnicks)
self.nick_colour[nick] = colour
return colour
def change(self, oldnick, newnick):
if oldnick in self.nick_colour:
self.nick_colour[newnick] = self.nick_colour.pop(oldnick)
#
# HTML
#
URL_REGEXP = re.compile(r'((http|https|ftp|gopher|news)://[^ \'")>]*)')
def createlinks(text):
"""Replace possible URLs with links."""
return URL_REGEXP.sub(r'<a href="\1">\1</a>', text)
def escape(s):
"""Replace ampersands, pointies, control characters.
>>> escape('Hello & <world>')
'Hello & <world>'
>>> escape('Hello & <world>')
'Hello & <world>'
Control characters (ASCII 0 to 31) are stripped away
>>> escape(''.join([chr(x) for x in range(32)]))
''
"""
s = s.replace('&', '&').replace('<', '<').replace('>', '>')
return ''.join([c for c in s if ord(c) > 0x1F])
#
# Output styles
#
class AbstractStyle(object):
"""A style defines the way output is formatted.
This is not a real class, rather it is an description of how style
classes should be written.
"""
name = "stylename"
description = "Single-line description"
def __init__(self, outfile, colours=None):
"""Create a text formatter for writing to outfile.
`colours` may have the following attributes:
part
join
server
nickchange
action
"""
self.outfile = outfile
self.colours = colours or {}
def head(self, title, prev=('', ''), index=('', ''), next=('', ''),
searchbox=False):
"""Generate the header.
`prev`, `index` and `next` are tuples (title, url) that comprise
the navigation bar.
"""
def foot(self):
"""Generate the footer."""
def servermsg(self, time, what, line):
"""Output a generic server message.
`time` is a string.
`line` is not escaped.
`what` is one of LogParser event constants (e.g. LogParser.JOIN).
"""
def nicktext(self, time, nick, text, htmlcolour):
"""Output a comment uttered by someone.
`time` is a string.
`nick` and `text` are not escaped.
`htmlcolour` is a string ('#rrggbb').
"""
class SimpleTextStyle(AbstractStyle):
"""Text style with little use of colour"""
name = "simplett"
description = __doc__
def head(self, title, prev=None, index=None, next=None,
charset="iso-8859-1", searchbox=False):
print >> self.outfile, """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
\t<title>%(title)s</title>
\t<meta name="generator" content="irclog2html.py %(VERSION)s by Marius Gedminas">
\t<meta name="version" content="%(VERSION)s - %(RELEASE)s">
\t<meta http-equiv="Content-Type" content="text/html; charset=%(charset)s">
</head>
<body text="#000000" bgcolor="#ffffff"><tt>""" % {
'VERSION': VERSION,
'RELEASE': RELEASE,
'title': escape(title),
'charset': charset,
}
def foot(self):
print >> self.outfile, """
<br>Generated by irclog2html.py %(VERSION)s | δ· find the plain text version at <a href="TEXTVERSIONSLUG" title="Text version of IRC log">this address</a> (HTTP) or <a href="TEXTVERSIONGEMINI" title="Gemini version of IRC log">in Gemini</a> (<a href="http://techrights.org/gemini" title="Techrights on Gemini">how to use Gemini</a>) with <a href="GEMTEXTVERSIONGEMINI" title="GemText version of IRC log">a full GemText version</a>.
</tt></body></html>""" % {'VERSION': VERSION, 'RELEASE': RELEASE},
def servermsg(self, time, what, text):
text = escape(text)
text = createlinks(text)
colour = self.colours.get(what)
if colour:
text = '<font color="%s">%s</font>' % (colour, text)
self._servermsg(text)
def _servermsg(self, line):
print >> self.outfile, '%s<br>' % line
def nicktext(self, time, nick, text, htmlcolour):
nick = escape(nick)
text = escape(text)
text = createlinks(text)
text = text.replace(' ', ' ')
self._nicktext(time, nick, text, htmlcolour)
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, '<%s> %s<br>' % (nick, text)
class TextStyle(SimpleTextStyle):
"""Text style using colours for each nick"""
name = "tt"
description = __doc__
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, ('<font color="%s"><%s></font>'
' <font color="#000000">%s</font><br>'
% (htmlcolour, nick, text))
class SimpleTableStyle(SimpleTextStyle):
"""Table style, without heavy use of colour"""
name = "simpletable"
def head(self, title, prev=None, index=None, next=None,
charset="iso-8859-1", searchbox=False):
SimpleTextStyle.head(self, title, prev, index, next, charset, searchbox)
print >> self.outfile, "<table cellspacing=3 cellpadding=2 border=0>"
def foot(self):
print >> self.outfile, "</table>"
SimpleTextStyle.foot(self)
def _servermsg(self, line):
print >> self.outfile, ('<tr><td colspan=2><tt>%s</tt></td></tr>'
% line)
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, ('<tr bgcolor="#eeeeee"><th><font color="%s">'
'<tt>%s</tt></font></th>'
'<td width="100%%"><tt>%s</tt></td></tr>'
% (htmlcolour, nick, text))
class TableStyle(SimpleTableStyle):
"""Default style, using a table with bold colours"""
name = "table"
description = __doc__
def _nicktext(self, time, nick, text, htmlcolour):
print >> self.outfile, ('<tr><th bgcolor="%s"><font color="#ffffff">'
'<tt>%s</tt></font></th>'
'<td width="100%%" bgcolor="#eeeeee"><tt><font'
' color="%s">%s</font></tt></td></tr>'
% (htmlcolour, nick, htmlcolour, text))
class XHTMLStyle(AbstractStyle):
"""Text style, produces XHTML that can be styled with CSS"""
name = 'xhtml'
description = __doc__
CLASSMAP = {
LogParser.ACTION: 'action',
LogParser.JOIN: 'join',
LogParser.PART: 'part',
LogParser.NICKCHANGE: 'nickchange',
LogParser.SERVER: 'servermsg',
LogParser.OTHER: 'other',
}
prefix = '<div class="irclog">'
suffix = '</div>'
def head(self, title, prev=('', ''), index=('', ''), next=('', ''),
charset="UTF-8", searchbox=False):
self.prev = prev
self.index = index
self.next = next
print >> self.outfile, """\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
<title>%(title)s</title>
<link rel="stylesheet" href="http://techrights.org/wp-content/uploads/2009/05/irclog.css" />
<meta name="generator" content="irclog2html.py %(VERSION)s by Marius Gedminas" />
<meta name="version" content="%(VERSION)s - %(RELEASE)s" />
</head>
<body>""" % {'VERSION': VERSION, 'RELEASE': RELEASE,
'title': escape(title), 'charset': charset}
self.heading(title)
if searchbox:
self.searchbox()
self.navbar(prev, index, next)
print >> self.outfile, self.prefix
def heading(self, title):
print >> self.outfile, '<a href="/" title="Home page"><img src="http://techrights.org/images/techrights-logo.png" alt="Techrights logo" border="0" /></a><h1>%s</h1><p align="center">(βΉ) Join us now at <a href="http://techrights.org/irc-channel/">the IRC channel</a> | δ· Find the <b>plain text</b> version at <a href="TEXTVERSIONSLUG" title="Text version of IRC log">this address</a> (HTTP) or <a href="TEXTVERSIONGEMINI" title="Gemini version of IRC log">in Gemini</a> (<a href="http://techrights.org/gemini" title="Techrights on Gemini">how to use Gemini</a>) with <a href="GEMTEXTVERSIONGEMINI" title="GemText version of IRC log">a full GemText version</a>.</p>' % escape(title)
def link(self, url, title):
# Intentionally not escaping title so that &entities; work
if url:
print >> self.outfile, ('<a href="%s">%s</a>'
% (escape(urllib.quote(url)),
title or escape(url))),
elif title:
print >> self.outfile, ('<span class="disabled">%s</span>'
% title),
def searchbox(self):
print >> self.outfile, """
<div class="searchbox">
<form action="search" method="get">
<input type="text" name="q" id="searchtext" />
<input type="submit" value="Search" id="searchbutton" />
</form>
</div>
"""
def navbar(self, prev, index, next):
prev_title, prev_url = prev
index_title, index_url = index
next_title, next_url = next
if not (prev_title or index_title or next_title or
prev_url or index_url or next_url):
return
print >> self.outfile, '<div class="navigation">',
self.link(prev_url, prev_title)
self.link(index_url, index_title)
self.link(next_url, next_title)
print >> self.outfile, '</div>'
def foot(self):
print >> self.outfile, self.suffix
self.navbar(self.prev, self.index, self.next)
print >> self.outfile, """
<div class="generatedby">
<p>Generated by <code>irclog2html.py</code> %(VERSION)s | δ· find the plain text version at <a href="TEXTVERSIONSLUG" title="Text version of IRC log">this address</a> (HTTP) or <a href="TEXTVERSIONGEMINI" title="Gemini version of IRC log">in Gemini</a> (<a href="http://techrights.org/gemini" title="Techrights on Gemini">how to use Gemini</a>) with <a href="GEMTEXTVERSIONGEMINI" title="GemText version of IRC log">a full GemText version</a>.</p>
</div>
</body>
</html>""" % {'VERSION': VERSION, 'RELEASE': RELEASE}
def servermsg(self, time, what, text):
"""Output a generic server message.
`time` is a string.
`line` is not escaped.
`what` is one of LogParser event constants (e.g. LogParser.JOIN).
"""
text = escape(text)
text = createlinks(text)
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<p id="t%s" class="%s">'
'<a href="#t%s" class="time">%s</a> '
'%s</p>'
% (time, self.CLASSMAP[what], time,
displaytime, text))
else:
print >> self.outfile, ('<p class="%s">%s</p>'
% (self.CLASSMAP[what], text))
def nicktext(self, time, nick, text, htmlcolour):
"""Output a comment uttered by someone.
`time` is a string.
`nick` and `text` are not escaped.
`htmlcolour` is a string ('#rrggbb').
"""
nick = escape(nick)
text = escape(text)
text = createlinks(text)
text = text.replace(' ', ' ')
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<p id="t%s" class="comment">'
'<a href="#t%s" class="time">%s</a> '
'<span class="nick" style="color: %s">'
'<%s></span>'
' <span class="text">%s</span></p>'
% (time, time, displaytime, htmlcolour, nick,
text))
else:
print >> self.outfile, ('<p class="comment">'
'<span class="nick" style="color: %s">'
'<%s></span>'
' <span class="text">%s</span></p>'
% (htmlcolour, nick, text))
class XHTMLTableStyle(XHTMLStyle):
"""Table style, produces XHTML that can be styled with CSS"""
name = 'xhtmltable'
description = __doc__
prefix = '<table class="irclog">'
suffix = '</table>'
def servermsg(self, time, what, text, link=''):
text = escape(text)
text = createlinks(text)
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<tr id="t%s">'
'<td class="%s" colspan="2">%s</td>'
'<td><a href="%s#t%s" class="time">%s</a></td>'
'</tr>'
% (time, self.CLASSMAP[what], text,
link, time, displaytime))
else:
print >> self.outfile, ('<tr>'
'<td class="%s" colspan="3">%s</td>'
'</tr>'
% (self.CLASSMAP[what], text))
def nicktext(self, time, nick, text, htmlcolour, link=''):
nick = escape(nick)
text = escape(text)
text = createlinks(text)
text = text.replace(' ', ' ')
if time:
displaytime = shorttime(time)
print >> self.outfile, ('<tr id="t%s">'
'<th class="nick" style="background: %s">%s</th>'
'<td class="text" style="color: %s">%s</td>'
'<td class="time">'
'<a href="%s#t%s" class="time">%s</a></td>'
'</tr>'
% (time, htmlcolour, nick, htmlcolour, text,
link, time, displaytime))
else:
print >> self.outfile, ('<tr>'
'<th class="nick" style="background: %s">%s</th>'
'<td class="text" colspan="2" style="color: %s">%s</td>'
'</tr>'
% (htmlcolour, nick, htmlcolour, text))
#
# Main
#
# All styles
STYLES = [
SimpleTextStyle,
TextStyle,
SimpleTableStyle,
TableStyle,
XHTMLStyle,
XHTMLTableStyle,
]
# Customizable colours
COLOURS = [
("part", "#000099", LogParser.PART),
("join", "#009900", LogParser.JOIN),
("server", "#009900", LogParser.SERVER),
("nickchange", "#009900", LogParser.NICKCHANGE),
("action", "#CC00CC", LogParser.ACTION),
]
def main(argv=sys.argv):
progname = os.path.basename(argv[0])
parser = optparse.OptionParser("usage: %prog [options] filename",
prog=progname,
description="Colourises and converts IRC"
" logs to HTML format for easy"
" web reading.")
parser.add_option('-s', '--style', dest="style", default="xhtmltable",
help="format log according to specific style"
" (default: xhtmltable); try -s help for a list of"
" available styles")
parser.add_option('-t', '--title', dest="title", default=None,
help="title of the page (default: same as file name)")
parser.add_option('--prev-title', dest="prev_title", default='',
help="title of the previous page (default: none)")
parser.add_option('--prev-url', dest="prev_url", default='',
help="URL of the previous page (default: none)")
parser.add_option('--index-title', dest="index_title", default='',
help="title of the index page (default: none)")
parser.add_option('--index-url', dest="index_url", default='',
help="URL of the index page (default: none)")
parser.add_option('--next-title', dest="next_title", default='',
help="title of the next page (default: none)")
parser.add_option('--next-url', dest="next_url", default='',
help="URL of the next page (default: none)")
parser.add_option('-S', '--searchbox', action="store_true", dest="searchbox",
default=False,
help="include a search box")
for name, default, what in COLOURS:
parser.add_option('--color-%s' % name, '--colour-%s' % name,
dest="colour_%s" % name, default=default,
help="select %s colour (default: %s)"
% (name, default))
options, args = parser.parse_args(argv[1:])
if options.style == "help":
print "The following styles are available for use with irclog2html.py:"
for style in STYLES:
print
print " %s" % style.name
print " %s" % style.description
print
return
for style in STYLES:
if style.name == options.style:
break
else:
parser.error("unknown style: %s" % style)
colours = {}
for name, default, what in COLOURS:
colours[what] = getattr(options, 'colour_%s' % name)
if not args:
parser.error("required parameter missing")
title = options.title
prev = (options.prev_title, options.prev_url)
index = (options.index_title, options.index_url)
next = (options.next_title, options.next_url)
for filename in args:
try:
infile = open(filename)
except EnvironmentError, e:
sys.exit("%s: cannot open %s for reading: %s"
% (progname, filename, e))
outfilename = filename + ".html"
try:
outfile = open(outfilename, "w")
except EnvironmentError, e:
infile.close()
sys.exit("%s: cannot open %s for writing: %s"
% (progname, outfilename, e))
try:
parser = LogParser(infile)
formatter = style(outfile, colours)
convert_irc_log(parser, formatter, title or filename,
prev, index, next, searchbox=options.searchbox)
finally:
outfile.close()
infile.close()
def convert_irc_log(parser, formatter, title, prev, index, next,
searchbox=False):
"""Convert IRC log to HTML or some other format."""
nick_colour = NickColourizer()
formatter.head(title, prev, index, next, searchbox=searchbox)
for time, what, info in parser:
if what == LogParser.COMMENT:
nick, text = info
htmlcolour = nick_colour[nick]
formatter.nicktext(time, nick, text, htmlcolour)
else:
if what == LogParser.NICKCHANGE:
text, oldnick, newnick = info
nick_colour.change(oldnick, newnick)
else:
text = info
formatter.servermsg(time, what, text)
formatter.foot()
if __name__ == '__main__':
main()