commit 1bafc1a69ebc10d876ceb397e7b7884c7d2d1587 Author: mrkubax10 Date: Tue Sep 5 10:04:47 2023 +0200 Initial commit diff --git a/configuration.pm b/configuration.pm new file mode 100644 index 0000000..e05e432 --- /dev/null +++ b/configuration.pm @@ -0,0 +1,11 @@ +# This file is a configuration for irclogger_web. Adjust it for your needs. + +package configuration; + +our $database = "irclogger.db"; +our $logFolder = "logs"; +our $botNick = "irclogger__"; +our $botPassword = "none"; +our $botUsername = "irclogger__"; +our $botHostname = "hostname"; +our $botName = "Full name of bot"; diff --git a/database_settings.sql b/database_settings.sql new file mode 100644 index 0000000..2cc52b8 --- /dev/null +++ b/database_settings.sql @@ -0,0 +1,23 @@ +create table channels(id int primary key not null, + server_id int not null, -- foreign key in servers table + name text not null, + public int not null, + accessor int -- foreign key in accessors table +); + +create table users(id int primary key not null, + name text not null, + password text not null, + accessor int -- foreign key in accessors table +); + +create table servers(id int primary key not null, + name text not null, + host text not null, + port int not null +); + +create table accessors(id int primary key not null, + channel_id int not null, -- foreign key in channels table + user_id int not null -- foreign key in users table +); diff --git a/logger.pl b/logger.pl new file mode 100644 index 0000000..af7ce67 --- /dev/null +++ b/logger.pl @@ -0,0 +1,306 @@ +# irclogger +# Copyright (C) 2023 mrkubax10 + +# 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 . + +use IO::Socket; +use List::Util; +use Time::Piece; +use File::Path; +use DBI; +use threads; + +use lib "."; +use configuration; + +use feature qw(switch); +use strict; +use warnings; + +sub connectToServer { + my $aServer = $_[0]; + my $aPort = $_[1]; + my $aServerName = $_[2]; + + my $socket = new IO::Socket::INET(PeerAddr=>$aServer, PeerPort=>$aPort, Proto=>"tcp"); + $socket->send(sprintf("PASS %s\r\n", $configuration::botPassword)); + $socket->send(sprintf("NICK %s\r\n", $configuration::botNick)); + $socket->send(sprintf("USER %s %s %s :%s\r\n", $configuration::botUsername, $configuration::botHostname, $aServerName, $configuration::botName)); + return $socket; +} + +sub stripPrefix { + my $aLine = $_[0]; + + my $inPrefix = 0; + my $prefix = ""; + my $line = ""; + foreach my $i (0..length($aLine)-1) { + my $char = substr($aLine, $i, 1); + if($char eq ":" && ($i==0 || $inPrefix)) { + $inPrefix = !$inPrefix; + next; + } + if($inPrefix) { + $prefix.=$char; + next; + } + if($char ne "\r" && $char ne "\n") { + $line.=$char; + } + } + return ($prefix, $line); +} + +sub parseIRCCommand { + my $aCommand = $_[0]; + + my @output; + my $inPrefix = 0; + my $inLongArg = 0; + my $currentString = ""; + my $prefix = ""; + foreach my $i (0..length($aCommand)-1) { + my $char = substr($aCommand, $i, 1); + if($char eq "\r" || $char eq "\n") { + next; + } + if($char eq ":" && $i==0) { + $inPrefix = 1; + next; + } + if($char eq " " && $inPrefix) { + $inPrefix = 0; + next; + } + if($inPrefix) { + $prefix.=$char; + next; + } + if($char eq ":" && !$inLongArg) { + $inLongArg = 1; + next; + } + if($inLongArg) { + $currentString.=$char; + next; + } + if($char eq " " && length($currentString)>0) { + push(@output, $currentString); + $currentString = ""; + next; + } + $currentString.=$char; + } + if(length($currentString)>0) { + push(@output, $currentString); + } + if(length($prefix)>0) { + push(@output, $prefix); + } + return @output; +} + +sub getUsernameFromHost { + my $aHost = $_[0]; + + my $output = ""; + foreach my $i (0..length($aHost)-1) { + my $char = substr($aHost, $i, 1); + if($char eq "!") { + last; + } + $output.=$char; + } + return $output; +} + +sub prepareLogFile { + my $aLogFiles = $_[0]; + my $aServerName = $_[1]; + my $aChannelName = $_[2]; + + if(!exists($aLogFiles->{$aChannelName})) { + my $outputFileFolder = $configuration::logFolder."/".$aServerName."/".$aChannelName; + if(!(-e $outputFileFolder)) { + File::Path::make_path($outputFileFolder); + } + my $outputFilePath = $outputFileFolder."/".localtime->dmy("-").".txt"; + open(my $file, ">>", $outputFilePath); + if($file) { + printf(":: Logger -> Outputting channel '%s' at '%s' to '%s'\n", $aChannelName, $aServerName, $outputFilePath); + $aLogFiles->{$aChannelName} = $file; + } + else { + print(":: Logger -> Failed to open '$outputFilePath' for writing\n"); + return 0; + } + } + return 1; +} + +sub handlePing { + my $aStream = $_[0]; + my $aCommand = $_[1]; + + my $aCommandLength = scalar(@$aCommand); + if($aCommandLength!=2) { + printf("Encountered invalid PING command (2 arguments expected, %d provided)\n", $aCommandLength); + return; + } + printf(":: Response -> PONG :%s\n", $aCommand->[1]); + $aStream->send(sprintf("PONG :%s\r\n", $aCommand->[1])); +} + +sub handlePrivMsg { + my $aStream = $_[0]; + my $aCommand = $_[1]; + my $aServerName = $_[2]; + my $aJoinedChannels = $_[3]; + my $aLogFiles = $_[4]; + + my $aCommandLength = scalar(@$aCommand); + if($aCommandLength!=4) { + printf("Encountered invalid PRIVMSG command (4 arguments expected, %d provided)\n", $aCommandLength); + return; + } + if(!prepareLogFile($aLogFiles, $aServerName, $aCommand->[1])) { + return; + } + $aLogFiles->{$aCommand->[1]}->print(sprintf("(%s) %s: %s\n", localtime->strftime("%H:%M:%S"), getUsernameFromHost($aCommand->[3]), $aCommand->[2])); + $aLogFiles->{$aCommand->[1]}->flush(); +} + +sub handleJoin { + my $aCommand = $_[0]; + my $aServerName = $_[1]; + my $aLogFiles = $_[2]; + + my $aCommandLength = scalar(@$aCommand); + if($aCommandLength!=3) { + printf("Encountered invalid JOIN command (3 arguments expected, %d provided)\n", $aCommandLength); + return; + } + if(!prepareLogFile($aLogFiles, $aServerName, $aCommand->[1])) { + return; + } + $aLogFiles->{$aCommand->[1]}->print(sprintf("(%s) %s has joined %s\n", localtime->strftime("%H:%M:%S"), getUsernameFromHost($aCommand->[2]), $aCommand->[1])); + $aLogFiles->{$aCommand->[1]}->flush(); +} + +sub handleQuit { + my $aCommand = $_[0]; + my $aServerName = $_[1]; + my $aJoinedChannels = $_[2]; + my $aLogFiles = $_[3]; + + my $aCommandLength = scalar(@$aCommand); + if($aCommandLength!=3 && $aCommandLength!=2) { + print("Encountered invalid QUIT command (3 or 2 arguments expected, $aCommandLength provided)\n"); + return; + } + my $reason = ""; + if($aCommandLength==3) { + $reason = $aCommand->[1]; + } + foreach my $channel (@$aJoinedChannels) { + if(!prepareLogFile($aLogFiles, $aServerName, $channel)) { + next; + } + $aLogFiles->{$channel}->print(sprintf("(%s) %s has quit (%s)\n", localtime->strftime("%H:%M:%S"), getUsernameFromHost($aCommand->[2]), $reason)); + $aLogFiles->{$channel}->flush(); + } +} + +sub handlePart { + my $aCommand = $_[0]; + my $aServerName = $_[1]; + my $aLogFiles = $_[2]; + + my $aCommandLength = scalar(@$aCommand); + if($aCommandLength!=3) { + print("Encountered invalid PART command (3 arguments expected, $aCommandLength provided)\n"); + return; + } + if(!prepareLogFile($aLogFiles, $aServerName, $aCommand->[1])) { + return; + } + $aLogFiles->{$aCommand->[1]}->print(sprintf("(%s) %s has left %s\n", localtime->strftime("%H:%M:%S"), getUsernameFromHost($aCommand->[2]), $aCommand->[1])); + $aLogFiles->{$aCommand->[1]}->flush(); +} + +sub joinChannel { + my $aStream = $_[0]; + my $aChannel = $_[1]; + + $aStream->send(sprintf("JOIN %s\r\n", $aChannel)); +} + +sub joinChannels { + my $aStream = $_[0]; + my $aChannels = $_[1]; + + foreach my $channel (@$aChannels) { + joinChannel($aStream, $channel); + } +} + +sub connectionWorker { + my $aHost = $_[0]; + my $aPort = $_[1]; + my $aServerName = $_[2]; + my $aChannels = $_[3]; + + my %logFiles; + my $stream = connectToServer($aHost, $aPort, $aServerName); + while(!eof($stream)) { + my $line = readline($stream); + my @command = parseIRCCommand($line); + printf(":: Server -> %s", $line); + + given($command[0]) { + when("PING") { handlePing($stream, \@command); } + when("PRIVMSG") { handlePrivMsg($stream, \@command, $aServerName, $aChannels, \%logFiles); } + when("JOIN") { handleJoin(\@command, $aServerName, \%logFiles); } + when("QUIT") { handleQuit(\@command, $aServerName, $aChannels, \%logFiles); } + when("PART") { handlePart(\@command, $aServerName, \%logFiles); } + when("376") { joinChannels($stream, $aChannels); } # end of MOTD + } + } + close($stream); +} + +my $db = DBI->connect("DBI:SQLite:dbname=$configuration::database", "", "", {RaiseError=>1}); +my $query = $db->prepare(qq(select * from servers;)); +$query->execute(); +while(my @row = $query->fetchrow_array()) { + my $id = $row[0]; + my $name = $row[1]; + my $host = $row[2]; + my $port = $row[3]; + + $query = $db->prepare(qq(select name from channels where server_id=$id;)); + $query->execute(); + my @channels; + while(my @channelsRow = $query->fetchrow_array()) { + my $name = $channelsRow[0]; + push(@channels, $name); + } + + threads->create("connectionWorker", $host, $port, $name, \@channels); +} + +foreach my $thread (threads->list(threads::running)) { + $thread->join(); +} diff --git a/prepare_database.sh b/prepare_database.sh new file mode 100644 index 0000000..d7c36cf --- /dev/null +++ b/prepare_database.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sqlite3 irclogger.db < database_settings.sql