2023-09-05 19:46:15 +02:00
# irclogger_web
2023-09-05 10:04:47 +02:00
# Copyright (C) 2023 mrkubax10 <mrkubax10@onet.pl>
# This program is free software: you can redistribute it and/or modify
2023-09-11 18:55:32 +02:00
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
2023-09-05 10:04:47 +02:00
# 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
2023-09-11 18:55:32 +02:00
# GNU Affero General Public License for more details.
2023-09-05 10:04:47 +02:00
2023-09-11 18:55:32 +02:00
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
2023-09-05 10:04:47 +02:00
2023-09-12 10:19:46 +02:00
package logger ;
2023-09-05 10:04:47 +02:00
use IO::Socket ;
2023-09-14 18:13:02 +02:00
use IO::Select ;
2023-09-05 10:04:47 +02:00
use List::Util ;
use Time::Piece ;
use File::Path ;
use threads ;
2023-09-14 18:13:02 +02:00
use threads::shared ;
2023-09-05 10:04:47 +02:00
use lib "." ;
use configuration ;
use feature qw( switch ) ;
use strict ;
use warnings ;
sub connectToServer {
my $ aServer = $ _ [ 0 ] ;
my $ aPort = $ _ [ 1 ] ;
my $ aServerName = $ _ [ 2 ] ;
2023-09-14 18:13:02 +02:00
my $ socket = IO::Socket::INET - > new ( PeerAddr = > $ aServer , PeerPort = > $ aPort , Proto = > "tcp" ) ;
2023-09-05 10:04:47 +02:00
$ 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 ;
}
2023-09-14 18:13:02 +02:00
sub readLineFromBuffer {
my $ aBuffer = $ _ [ 0 ] ;
my $ output = "" ;
my $ bufferLength = length ( $ aBuffer ) ;
foreach my $ i ( 0 .. $ bufferLength - 1 ) {
my $ char = substr ( $ aBuffer , $ i , 1 ) ;
if ( $ char eq "\n" || ( $ char eq "\r" && $ i + 1 < $ bufferLength && substr ( $ aBuffer , $ i + 1 , 1 ) eq "\n" ) ) {
my $ outputLength = length ( $ output ) ;
if ( $ char eq "\r" && $ i + 1 < $ bufferLength && substr ( $ aBuffer , $ i + 1 , 1 ) eq "\n" ) {
$ outputLength += 2 ;
}
else {
$ outputLength + + ;
}
return ( $ output , substr ( $ aBuffer , $ outputLength , $ bufferLength - $ outputLength ) ) ;
}
$ output . = $ char ;
}
return ( "" , $ aBuffer ) ;
}
2023-09-05 10:04:47 +02:00
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 ) ;
2023-09-17 20:16:19 +02:00
$ aLogFiles - > { $ aChannelName } { "file" } = $ file ;
$ aLogFiles - > { $ aChannelName } { "names" } = [] ;
2023-09-05 10:04:47 +02:00
}
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 ;
}
2023-09-17 20:16:19 +02:00
$ aLogFiles - > { $ aCommand - > [ 1 ] } { "file" } - > print ( sprintf ( "(%s) %s: %s\n" , localtime - > strftime ( "%H:%M:%S" ) , getUsernameFromHost ( $ aCommand - > [ 3 ] ) , $ aCommand - > [ 2 ] ) ) ;
$ aLogFiles - > { $ aCommand - > [ 1 ] } { "file" } - > flush ( ) ;
2023-09-05 10:04:47 +02:00
}
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 ;
}
2023-09-17 20:16:19 +02:00
$ aLogFiles - > { $ aCommand - > [ 1 ] } { "file" } - > print ( sprintf ( "(%s) %s has joined %s\n" , localtime - > strftime ( "%H:%M:%S" ) , getUsernameFromHost ( $ aCommand - > [ 2 ] ) , $ aCommand - > [ 1 ] ) ) ;
$ aLogFiles - > { $ aCommand - > [ 1 ] } { "file" } - > flush ( ) ;
2023-09-05 10:04:47 +02:00
}
sub handleQuit {
my $ aCommand = $ _ [ 0 ] ;
my $ aServerName = $ _ [ 1 ] ;
2023-09-17 20:16:19 +02:00
my $ aLogFiles = $ _ [ 2 ] ;
2023-09-05 10:04:47 +02:00
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 ] ;
}
2023-09-17 20:16:19 +02:00
my $ username = getUsernameFromHost ( $ aCommand - > [ $ aCommandLength - 1 ] ) ;
foreach my $ channel ( keys ( %$ aLogFiles ) ) {
my $ found = 0 ;
foreach my $ name ( @ { $ aLogFiles - > { $ channel } { "names" } } ) {
if ( $ name eq $ username ) {
$ found = 1 ;
last ;
}
}
if ( ! $ found || ! prepareLogFile ( $ aLogFiles , $ aServerName , $ channel ) ) {
2023-09-05 10:04:47 +02:00
next ;
}
2023-09-17 20:16:19 +02:00
$ aLogFiles - > { $ channel } { "file" } - > print ( sprintf ( "(%s) %s has quit (%s)\n" , localtime - > strftime ( "%H:%M:%S" ) , $ username , $ reason ) ) ;
$ aLogFiles - > { $ channel } { "file" } - > flush ( ) ;
2023-09-05 10:04:47 +02:00
}
}
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 ;
}
2023-09-17 20:16:19 +02:00
$ aLogFiles - > { $ aCommand - > [ 1 ] } { "file" } - > print ( sprintf ( "(%s) %s has left %s\n" , localtime - > strftime ( "%H:%M:%S" ) , getUsernameFromHost ( $ aCommand - > [ 2 ] ) , $ aCommand - > [ 1 ] ) ) ;
$ aLogFiles - > { $ aCommand - > [ 1 ] } { "file" } - > flush ( ) ;
2023-09-05 10:04:47 +02:00
}
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 ) ;
}
}
2023-09-17 20:16:19 +02:00
sub handleNames {
my $ aCommand = $ _ [ 0 ] ;
my $ aChannels = $ _ [ 1 ] ;
my $ aLogFiles = $ _ [ 2 ] ;
my $ aCommandLength = scalar ( @$ aCommand ) ;
if ( $ aCommandLength != 6 ) {
print ( "Encountered invalid NAMES command (6 arguments expected, $aCommandLength provided)\n" ) ;
return ;
}
my @ names = split ( " " , $ aCommand - > [ 4 ] ) ;
if ( ! defined ( $ aLogFiles - > { $ aCommand - > [ 3 ] } ) ) {
return ;
}
push ( @ { $ aLogFiles - > { $ aCommand - > [ 3 ] } { "names" } } , @ names ) ;
}
2023-09-14 18:13:02 +02:00
our @ connections : shared ;
2023-09-16 21:08:42 +02:00
our $ running : shared = 1 ;
2023-09-14 18:13:02 +02:00
2023-09-05 10:04:47 +02:00
sub connectionWorker {
my $ aHost = $ _ [ 0 ] ;
my $ aPort = $ _ [ 1 ] ;
my $ aServerName = $ _ [ 2 ] ;
my $ aChannels = $ _ [ 3 ] ;
2023-09-14 18:13:02 +02:00
my $ buffer = "" ;
my @ actionQueue : shared ;
my @ connection : shared = ( $ aServerName , \ @ actionQueue ) ;
push ( @ connections , \ @ connection ) ;
2023-09-16 21:08:42 +02:00
my % logFiles ;
while ( $ running ) {
my $ stream = connectToServer ( $ aHost , $ aPort , $ aServerName ) ;
my $ streamSelect = IO::Select - > new ( $ stream ) ;
while ( ! eof ( $ stream ) ) {
if ( scalar ( @ actionQueue ) > 0 ) {
given ( $ actionQueue [ 0 ] ) {
when ( "JOIN" ) {
joinChannel ( $ stream , $ actionQueue [ 1 ] ) ;
}
}
@ actionQueue = ( ) ;
2023-09-14 18:13:02 +02:00
}
2023-09-16 21:08:42 +02:00
my @ canRead = $ streamSelect - > can_read ( 0 ) ;
if ( scalar ( @ canRead ) == 0 ) {
next ;
2023-09-14 18:13:02 +02:00
}
2023-09-16 21:08:42 +02:00
my $ tempBuffer ;
$ stream - > recv ( $ tempBuffer , 512 ) ;
$ buffer . = $ tempBuffer ;
my ( $ line , $ remaining ) = readLineFromBuffer ( $ buffer ) ;
2023-09-14 18:13:02 +02:00
$ buffer = $ remaining ;
2023-09-16 21:08:42 +02:00
while ( length ( $ line ) > 0 ) {
my @ command = parseIRCCommand ( $ line ) ;
2023-09-17 20:16:19 +02:00
printf ( ":: Server -> %s\n" , $ line ) ;
2023-09-16 21:08:42 +02:00
given ( $ command [ 0 ] ) {
when ( "PING" ) { handlePing ( $ stream , \ @ command ) ; }
when ( "PRIVMSG" ) { handlePrivMsg ( $ stream , \ @ command , $ aServerName , $ aChannels , \ % logFiles ) ; }
when ( "JOIN" ) { handleJoin ( \ @ command , $ aServerName , \ % logFiles ) ; }
2023-09-17 20:16:19 +02:00
when ( "QUIT" ) { handleQuit ( \ @ command , $ aServerName , \ % logFiles ) ; }
2023-09-16 21:08:42 +02:00
when ( "PART" ) { handlePart ( \ @ command , $ aServerName , \ % logFiles ) ; }
when ( "376" ) { joinChannels ( $ stream , $ aChannels ) ; } # end of MOTD
2023-09-17 20:16:19 +02:00
when ( "353" ) { handleNames ( \ @ command , $ aChannels , \ % logFiles ) ; } # NAMES reply
2023-09-16 21:08:42 +02:00
}
( $ line , $ remaining ) = readLineFromBuffer ( $ buffer ) ;
$ buffer = $ remaining ;
}
2023-09-05 10:04:47 +02:00
}
2023-09-16 21:08:42 +02:00
close ( $ stream ) ;
2023-09-05 10:04:47 +02:00
}
}
2023-09-12 10:19:46 +02:00
sub createLogger {
my $ aName = $ _ [ 0 ] ;
my $ aHost = $ _ [ 1 ] ;
my $ aPort = $ _ [ 2 ] ;
my $ aChannels = $ _ [ 3 ] ;
2023-09-14 18:13:02 +02:00
threads - > create ( "connectionWorker" , $ aHost , $ aPort , $ aName , $ aChannels ) ;
}
sub getActionQueueByServerName {
my $ aServerName = $ _ [ 0 ] ;
foreach my $ connection ( @ connections ) {
if ( $ connection - > [ 0 ] eq $ aServerName ) {
return $ connection - > [ 1 ] ;
}
}
2023-09-12 10:19:46 +02:00
}
2023-09-05 10:04:47 +02:00
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 ) ;
}
2023-09-12 10:19:46 +02:00
createLogger ( $ name , $ host , $ port , \ @ channels ) ;
2023-09-05 10:04:47 +02:00
}
2023-09-12 10:19:46 +02:00
$ db - > disconnect ( ) ;
1 ;