Git browser: IRC/

This page presents code associated with the module/unit named above.

Summary of changes
Back to Git index
Licence (AGPLv3)

IRC/Class.phIRCe.php


 *  Portions  (C) 2010-2016 Toby Thain 
 #  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 .
 */

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]+|
)\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: 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, '') !== false){ $matches = array(); if (preg_match('%(.*?)%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 .', 'Portions (C) 2010-2013 Toby Thain .', '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/> $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 "
  • #tuxmachines log for $IRCFULLDATE
  • " \ >> $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 
    # Original Author:
    #   Jeff Waugh 
    # Contributors:
    #   Rick Welykochy 
    #   Alexander Else 
    #
    # 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'\1', text)
    
    def escape(s):
        """Replace ampersands, pointies, control characters.
    
            >>> escape('Hello & ')
            'Hello & <world>'
            >>> escape('Hello & ')
            '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, """\
    
    
    
    \t%(title)s
    \t
    \t
    \t
    
    """ % {
                'VERSION': VERSION,
                'RELEASE': RELEASE,
                'title': escape(title),
                'charset': charset,
            }
    
        def foot(self):
            print >> self.outfile, """
    
    Generated by irclog2html.py %(VERSION)s by Marius Gedminas - find it at mg.pov.lt!
    """ % {'VERSION': VERSION, 'RELEASE': RELEASE}, def servermsg(self, time, what, text): text = escape(text) text = createlinks(text) colour = self.colours.get(what) if colour: text = '%s' % (colour, text) self._servermsg(text) def _servermsg(self, line): print >> self.outfile, '%s
    ' % 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
    ' % (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, ('<%s>' ' %s
    ' % (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, "" def foot(self): print >> self.outfile, "
    " SimpleTextStyle.foot(self) def _servermsg(self, line): print >> self.outfile, ('%s' % line) def _nicktext(self, time, nick, text, htmlcolour): print >> self.outfile, ('' '%s' '%s' % (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, ('' '%s' '%s' % (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 = '
    ' suffix = '
    ' def head(self, title, prev=('', ''), index=('', ''), next=('', ''), charset="UTF-8", searchbox=False): self.prev = prev self.index = index self.next = next print >> self.outfile, """\ %(title)s """ % {'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, 'Tux Machines logo

    %s

    Join us now at the IRC channel.

    ' % escape(title) def link(self, url, title): # Intentionally not escaping title so that &entities; work if url: print >> self.outfile, ('%s' % (escape(urllib.quote(url)), title or escape(url))), elif title: print >> self.outfile, ('%s' % title), def searchbox(self): print >> self.outfile, """ """ 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, '' def foot(self): print >> self.outfile, self.suffix self.navbar(self.prev, self.index, self.next) print >> self.outfile, """

    Generated by irclog2html.py %(VERSION)s by Marius Gedminas - find it at mg.pov.lt!

    """ % {'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, ('

    ' '%s ' '%s

    ' % (time, self.CLASSMAP[what], time, displaytime, text)) else: print >> self.outfile, ('

    %s

    ' % (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, ('

    ' '%s ' '' '<%s>' ' %s

    ' % (time, time, displaytime, htmlcolour, nick, text)) else: print >> self.outfile, ('

    ' '' '<%s>' ' %s

    ' % (htmlcolour, nick, text)) class XHTMLTableStyle(XHTMLStyle): """Table style, produces XHTML that can be styled with CSS""" name = 'xhtmltable' description = __doc__ prefix = '' suffix = '
    ' def servermsg(self, time, what, text, link=''): text = escape(text) text = createlinks(text) if time: displaytime = shorttime(time) print >> self.outfile, ('' '%s' '%s' '' % (time, self.CLASSMAP[what], text, link, time, displaytime)) else: print >> self.outfile, ('' '%s' '' % (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, ('' '%s' '%s' '' '%s' '' % (time, htmlcolour, nick, htmlcolour, text, link, time, displaytime)) else: print >> self.outfile, ('' '%s' '%s' '' % (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 ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
        echo ""  >> $IRCDATEFILE.txt
    
        echo ""  >> $IRCDATEFILE.txt
        echo "

    \"GNOME

    \"GNOME

    #techrights log

    #boycottnovell log

    \"GNOME

    \"GNOME

    #boycottnovell-social log

    #techbytes log

    " >> $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 "

    " >> $IRCDATEFILE.txt echo "Enter the IRC channels now" >> $IRCDATEFILE.txt echo "

    " >> $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

    
    
     *
     *  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 .
     */
    
    /*-----------------------------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 <> $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 <>$t
         Post body
    
    

    HTML5 logs

    HTML5 logs

    #techrights log as HTML5

    #boycottnovell log as HTML5

    HTML5 logs

    HTML5 logs

    #boycottnovell-social log as HTML5

    #techbytes log as HTML5

    text logs

    text logs

    #techrights log as text

    #boycottnovell log as text

    text logs

    text logs

    #boycottnovell-social log as text

    #techbytes log as text

    Enter the IRC channels now

    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

    
     *  Portions  (C) 2010-2013 Toby Thain 
     *  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 .
     */
    
    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 
    # Original Author:
    #   Jeff Waugh 
    # Contributors:
    #   Rick Welykochy 
    #   Alexander Else 
    #
    # 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'\1', text)
    
    def escape(s):
        """Replace ampersands, pointies, control characters.
    
            >>> escape('Hello & ')
            'Hello & <world>'
            >>> escape('Hello & ')
            '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, """\
    
    
    
    \t%(title)s
    \t
    \t
    \t
    
    """ % {
                'VERSION': VERSION,
                'RELEASE': RELEASE,
                'title': escape(title),
                'charset': charset,
            }
    
        def foot(self):
            print >> self.outfile, """
    
    Generated by irclog2html.py %(VERSION)s | ䷉ find the plain text version at this address (HTTP) or in Gemini (how to use Gemini) with a full GemText version.
    """ % {'VERSION': VERSION, 'RELEASE': RELEASE}, def servermsg(self, time, what, text): text = escape(text) text = createlinks(text) colour = self.colours.get(what) if colour: text = '%s' % (colour, text) self._servermsg(text) def _servermsg(self, line): print >> self.outfile, '%s
    ' % 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
    ' % (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, ('<%s>' ' %s
    ' % (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, "" def foot(self): print >> self.outfile, "
    " SimpleTextStyle.foot(self) def _servermsg(self, line): print >> self.outfile, ('%s' % line) def _nicktext(self, time, nick, text, htmlcolour): print >> self.outfile, ('' '%s' '%s' % (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, ('' '%s' '%s' % (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 = '
    ' suffix = '
    ' def head(self, title, prev=('', ''), index=('', ''), next=('', ''), charset="UTF-8", searchbox=False): self.prev = prev self.index = index self.next = next print >> self.outfile, """\ %(title)s """ % {'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, 'Techrights logo

    %s

    (ℹ) Join us now at the IRC channel | ䷉ Find the plain text version at this address (HTTP) or in Gemini (how to use Gemini) with a full GemText version.

    ' % escape(title) def link(self, url, title): # Intentionally not escaping title so that &entities; work if url: print >> self.outfile, ('%s' % (escape(urllib.quote(url)), title or escape(url))), elif title: print >> self.outfile, ('%s' % title), def searchbox(self): print >> self.outfile, """ """ 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, '' def foot(self): print >> self.outfile, self.suffix self.navbar(self.prev, self.index, self.next) print >> self.outfile, """

    Generated by irclog2html.py %(VERSION)s | ䷉ find the plain text version at this address (HTTP) or in Gemini (how to use Gemini) with a full GemText version.

    """ % {'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, ('

    ' '%s ' '%s

    ' % (time, self.CLASSMAP[what], time, displaytime, text)) else: print >> self.outfile, ('

    %s

    ' % (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, ('

    ' '%s ' '' '<%s>' ' %s

    ' % (time, time, displaytime, htmlcolour, nick, text)) else: print >> self.outfile, ('

    ' '' '<%s>' ' %s

    ' % (htmlcolour, nick, text)) class XHTMLTableStyle(XHTMLStyle): """Table style, produces XHTML that can be styled with CSS""" name = 'xhtmltable' description = __doc__ prefix = '' suffix = '
    ' def servermsg(self, time, what, text, link=''): text = escape(text) text = createlinks(text) if time: displaytime = shorttime(time) print >> self.outfile, ('' '%s' '%s' '' % (time, self.CLASSMAP[what], text, link, time, displaytime)) else: print >> self.outfile, ('' '%s' '' % (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, ('' '%s' '%s' '' '%s' '' % (time, htmlcolour, nick, htmlcolour, text, link, time, displaytime)) else: print >> self.outfile, ('' '%s' '%s' '' % (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()
    Back to main index