#!/usr/bin/perl

# added ircbot user/UID match 998 iptables persistent rules
# sudo useradd -r -s /bin/false -d /nonexistent ircbot
# sudo -u ircbot perl llamar_new_01_fixedcont_readurl6-saveloadsecure.pl
# sudo iptables -I OUTPUT -m owner --uid-owner ircbot -d 192.168.1.180 -j ACCEPT
# sudo iptables -A OUTPUT -m owner --uid-owner ircbot -d 10.0.0.0/8 -j DROP
# sudo iptables -A OUTPUT -m owner --uid-owner ircbot -d 172.16.0.0/12 -j DROP  
# sudo iptables -A OUTPUT -m owner --uid-owner ircbot -d 192.168.0.0/16 -j DROP
# sudo iptables -A OUTPUT -m owner --uid-owner ircbot -d 169.254.0.0/16 -j DROP
# sudo iptables -A OUTPUT -m owner --uid-owner ircbot -d 127.0.0.0/8 -j DROP
# sudo netfilter-persistent save  |   sudo iptables-save > /etc/iptables/rules.v4

use strict;
use warnings;
use IO::Select;
use IO::Socket::INET;
use JSON::XS;
use Types::Serialiser;
use URI;
use LWP::UserAgent;
use HTTP::Cookies;
use Data::Dumper;
use String::ShellQuote;
use IPC::Open2;
use IO::Select;
use Encode;
use Storable;
use Socket qw(inet_aton inet_ntoa);
use URI;
use URI::Escape qw(uri_unescape);
use File::Spec;
use File::Basename;
use Storable qw(store retrieve);

# Define a safe directory for saves (adjust path as needed)
my $SAFE_DIR = '/home/superkuh/tests/chatsaves/';
my $MAX_FILENAME_LENGTH = 50;

# now using local llama.cpp server.cpp HTTP API started on the same computer like,
#superkuh@bob:~/app_installs/llama.cpp-2023-10-21/llama.cpp/build/bin$ ./server -m /home/superkuh/app_installs/llama.cpp/models/collectivecognition-v1.1-mistral-7b.Q4_K_M.gguf -c 2048 --port 8080 --threads 1 --n-gpu-layers 42

# --- Configuration ---

# Debugging
my $debugon = 1;

# Web paths
my $webserverdirectorypath = '/home/superkuh/limbo/www/imgai'; # No trailing slash

# llava (image analysis) command and models
my $command = '/home/superkuh/app_installs/llama.cpp-2023-10-31/llama.cpp/build/bin/llava';
my $imagemodel = '/home/superkuh/app_installs/llama.cpp/models/llava-1v5-7b-ggml-model-q5_k.gguf';
my $mmproj = '/home/superkuh/app_installs/llama.cpp/models/mmproj-model-f16.gguf';

# LLM API Server IPs
my $serverip = '192.168.1.180';
my $serverip2 = '192.168.1.180'; # for !toggleserver

# OpenAI API Key (for !lamebot)
my $openai_api_key = 'PRIVATEKEYPRIVATEKEYPRIVATEKEYPRIVATEKEY';

# Chat History & Memory
my %userhistory;
if (-e 'userhistory') {%userhistory = %{retrieve('userhistory')};}
my $historylength = 99;
my $maxhistorylength = 200;
my %channelprimedirective;

# !readurl Configuration
my $linestokeepurltext = 20; # How many lines to keep !readurl context before summarizing
my %url_context_lifetimes;   # Tracks the countdown for URL contexts
my $max_readurl_text_chunk_length = 20000;

# LLM Generation Parameters
my $npredict = 400;
my @logitbiases;
my $globalstoptoken = "\n";
my $alpacaswitch = 0;
my $gpt4toggle = 0;
my $gpt35turboswitch = 1;

# Tokenizer JSON for !tokenize command
my $json_file = "mistral24b-tokenizer.json";

# IRC Server Settings
my $irc_server = 'irc.libera.chat';
my $irc_port   = 6667;
my $irc_nick   = 'llamar';
my $irc_channel = '##llm'; # Default channel

# List of channels to join on startup
my %joinedchannels;
$joinedchannels{$irc_channel} = 1; # Mark default channel as joined
#$joinedchannels{"#somethingelse"} = 0;
#$joinedchannels{"###another"} = 0;
#$joinedchannels{"#startrek"} = 0;

# IRC Bot Credentials & Settings
my $username = 'llamar';
my $password = 'PRIVATEKEYPRIVATEKEYPRIVATEKEYPRIVATEKEY';
my $ircmessagemaxlength = 432;
my %pmwhitelist = ();
my $lamebottrigger = "\!lamebot";
my $llamatrigger = "\!llama";
my $multi = ""; # For multi-line messages

# --- System Prompt (Prime Directive) ---
my $prepromptfile = '/home/superkuh/tests/pp-generic-12bablitered.txt';
my $primedirective = '';
my $primedirectivedefault;
if (open my $file_handle, '<', $prepromptfile) {
    while (<$file_handle>) {
        $primedirective .= $_;
    }
    close $file_handle;
    $primedirectivedefault = $primedirective;
} else {
    die "Error: Unable to open file '$prepromptfile': $!";
}

if ($debugon) {
	print "\n<primedirective>$primedirective";
	print "</primedirective>\n";
}

# --- Main Program ---

# connect goto workaround
CONNECT: print "\nTrying to reconnect to IRC server.\n";

# Connect to IRC server
my $sock = IO::Socket::INET->new("$irc_server:$irc_port") or die "Can't connect to IRC server: $!";
print "Connected to IRC server\n";

# Send nick and user commands
print $sock "NICK $irc_nick\r\n";
print $sock "USER $irc_nick 8 * :superkuh.com/llamar.html LLM AI Bot\r\n";

# Log in to the account
print $sock "PRIVMSG nickserv :IDENTIFY $username $password\r\n" if $password;

# Join the specified channel
print $sock "JOIN $irc_channel\r\n";
print "JOIN $irc_channel\r\n" if $debugon;
# Join all the other channels
foreach my $key (keys %joinedchannels) {
	if ($joinedchannels{$key} == 0) {
		print $sock "JOIN $key\r\n";
		print "JOIN $key\r\n" if $debugon;
		$joinedchannels{$key}++;
		}
}

my $select = IO::Select->new($sock);
my $timeout = 240; # 4 minutes

my $stupidworkaroundforp2pnet = 0;
# Loop to receive and respond to messages
while (1) {
    my @ready = $select->can_read($timeout);
    
if (@ready) {
    my $line = <$sock>;

	if ($line =~ /^PING(.*)$/i) {
		print $sock "PONG $1\r\n";
		print "PONG $1\r\n" if $debugon;
		
		if ($stupidworkaroundforp2pnet < 1) {
			print "stupidworkaroundcounter: $stupidworkaroundforp2pnet\n" if $debugon;
			# Join the specified channel
			print $sock "JOIN $irc_channel\r\n";
			print "JOIN $irc_channel\r\n" if $debugon;
			print $sock "PRIVMSG nickserv :IDENTIFY $username $password\r\n" if $password;
			$stupidworkaroundforp2pnet++;
			
			foreach my $key (keys %joinedchannels) {
			    if ($joinedchannels{$key} == 0) {
			    	print $sock "JOIN $key\r\n";
			    	print "JOIN $key\r\n" if $debugon;
			    	$joinedchannels{$key}++;
			    }
			}
		}
		
		next;
	}

	## respond to private messages
	if ($line =~ /^:([^!]+)![^@]+\@[^ ]+ PRIVMSG $irc_nick :(.+)/) {
		my $usernick = $1;
		my $message = $2;
		my $thingtosendtoonirc = $usernick;

		if ($pmwhitelist{lc($usernick)}) {
            # PM Command handling from original script
			if ($message =~ /^!join\s(.*)/){
				my $newchannel = $1; chomp($newchannel);
				print $sock "JOIN $newchannel\r\n";
				print $sock "PRIVMSG $thingtosendtoonirc :joining channel: $newchannel\r\n";
				$joinedchannels{$newchannel}++;
				next;
			}
			if ($message =~ /^!part\s(.*)/){
				my $newchannel = $1; chomp($newchannel);
				print $sock "PART $newchannel\r\n";
				print $sock "PRIVMSG $thingtosendtoonirc :leaving channel: $newchannel\r\n";
				$joinedchannels{$newchannel}--;
				next;
			}
            # Add other PM commands here...

			if ($message =~ /^$lamebottrigger/) {
				takemessagesendtoopenaiandsendthatresponseout_notstupid($message, $thingtosendtoonirc, $usernick, 1);
			} elsif ($message =~ /$llamatrigger/) {
				takemessagesendtolocalllamaAPIandsendthatresponseout($message, $thingtosendtoonirc, $usernick, 0);		
			} else {
				takemessagesendtolocalllamaAPIandsendthatresponseout($message, $thingtosendtoonirc, $usernick, 0);		
			}
		} else {
			print $sock "PRIVMSG $thingtosendtoonirc :Ask superkuh to put you on the private message whitelist.\r\n";
		}
		next;
	}

	## respond to public messages in a room.
	if ($line =~ /[^ ]+ PRIVMSG (#+[\w_-]+) :(.+)/) {
		my $anyircchannel = $1;
		my $message = $2;
		my ($usernick, $userandhostandeverythingelse) = split '!', $line, 2;
		$usernick =~ s/^://;
		my $thingtosendtoonirc = $anyircchannel;
		
		$message =~ s/[\p{Cc}\p{Cf}]//g;
		$message =~ s/\[0m\[0m>//g;
			
		if ($message =~ /^\s*ACTION.+/) {
			$message =~ s/^\s*ACTION/I/;
		}

		next if $usernick =~ /^(Bark|bella|Lolo|electrabot|GPT4|gptpaste|Llama3|ramall)$/;

		my $timestamp = get_timestamp();
		addtomessagehistory("$timestamp <$usernick>: $message", $thingtosendtoonirc);
		print "Added to history for $anyircchannel: $timestamp <$usernick>: $message\n" if $debugon;

		### --- Command and Response Logic --- ###
        
if ($message =~ /^!readurl\s+(.+)$/i) {     
    my $raw_input = $1;
    
    # Input validation and sanitization
    # Remove leading/trailing whitespace
    $raw_input =~ s/^\s+|\s+$//g;
    
    # Check for obviously malicious patterns
    if ($raw_input =~ /[<>'"&`|;$(){}[\]\\]/ ||     # Script injection chars
        $raw_input =~ /\s/ ||                        # No spaces allowed in URL
        $raw_input =~ /\x00-\x1f\x7f-\xff/ ||      # No control chars or high ASCII
        length($raw_input) > 2048 ||                 # Reasonable URL length limit
        length($raw_input) < 10) {                   # Minimum reasonable URL length
        next;
    }
    
    # Basic URL format validation - must start with http:// or https://
    unless ($raw_input =~ /^https?:\/\/[a-zA-Z0-9._-]+/) {
        next;
    }
    
    # URL decode to catch encoded malicious chars
    my $decoded_input = uri_unescape($raw_input);
    
    # Check decoded version for malicious patterns
    if ($decoded_input =~ /[<>'"&`|;$(){}[\]\\]/ ||
        $decoded_input =~ /\x00-\x1f\x7f-\xff/ ||
        $decoded_input ne $raw_input) {  # If decoding changed it, be suspicious
        next;
    }
    
    my $url_to_read = $raw_input;  # Now we trust it enough to use
    my $display_url = $url_to_read;
    $display_url =~ s/^https?:\/\///;
    
    # Additional length check on display URL to prevent message flooding
    if (length($display_url) > 100) {
        $display_url = substr($display_url, 0, 97) . '...';
    }
    
    # Function to check if an IP address is private/local
    sub is_private_ip {
        my $ip_str = shift;
        return 0 unless defined $ip_str;
        
        # Additional input validation
        return 0 if length($ip_str) > 15;  # Max IPv4 length
        return 0 unless $ip_str =~ /^[\d.]+$/;
        
        # Convert IP string to binary format for reliable comparison
        my $ip_bin = eval { inet_aton($ip_str) };
        return 0 unless defined $ip_bin;
        
        # Convert back to dotted decimal for consistent checking
        my $normalized_ip = inet_ntoa($ip_bin);
        
        # Split into octets for numeric comparison
        my @octets = split /\./, $normalized_ip;
        return 0 unless @octets == 4;
        
        # Validate each octet is in valid range
        for my $octet (@octets) {
            return 0 unless $octet =~ /^\d+$/ && $octet >= 0 && $octet <= 255;
        }
        
        my ($a, $b, $c, $d) = @octets;
        
        # Check various private/local ranges
        return 1 if $a == 127;                                    # 127.0.0.0/8 loopback
        return 1 if $a == 10;                                     # 10.0.0.0/8 private
        return 1 if $a == 172 && $b >= 16 && $b <= 31;          # 172.16.0.0/12 private
        return 1 if $a == 192 && $b == 168;                     # 192.168.0.0/16 private
        return 1 if $a == 169 && $b == 254;                     # 169.254.0.0/16 link-local
        return 1 if $a == 0;                                     # 0.0.0.0/8 this network
        return 1 if $a >= 224 && $a <= 239;                     # 224.0.0.0/4 multicast
        return 1 if $a >= 240;                                   # 240.0.0.0/4 reserved
        return 1 if $normalized_ip eq '255.255.255.255';         # broadcast
        
        return 0;
    }
    
    # Function to validate a single URL
    sub validate_url {
        my $url = shift;
        
        # Additional input validation
        return 0 unless defined $url && length($url) > 0;
        return 0 if length($url) > 2048;
        
        my $uri = eval { URI->new($url) };
        return 0 unless $uri && ($uri->scheme eq 'http' || $uri->scheme eq 'https');
        
        my $host = $uri->host;
        return 0 unless defined $host;
        
        # Validate hostname format
        return 0 if length($host) > 253;  # Max domain name length
        return 0 unless $host =~ /^[a-zA-Z0-9._-]+$/;
        return 0 if $host =~ /\.\./;  # No double dots
        return 0 if $host =~ /^\./ || $host =~ /\.$/;  # No leading/trailing dots
        
        # Check port if present
        my $port = $uri->port;
        if (defined $port) {
            return 0 unless $port =~ /^\d+$/ && $port > 0 && $port <= 65535;
            # Block common dangerous ports
            return 0 if $port == 22 || $port == 23 || $port == 25 || $port == 110 ||
                       $port == 143 || $port == 993 || $port == 995 || $port == 3389;
        }
        
        # Check if hostname is obviously local first
        if ($host =~ /^localhost$/i) {
            return 0;
        }
        
        # Check for IPv6 local addresses
        if ($host =~ /^\[([^\]]+)\]$/) {
            my $ipv6 = $1;
            return 0 if length($ipv6) > 39;  # Max IPv6 length
            if ($ipv6 =~ /^::1$/i ||                    # loopback
                $ipv6 =~ /^fe80:/i ||                   # link-local
                $ipv6 =~ /^fc00:/i ||                   # unique local
                $ipv6 =~ /^fd[0-9a-f][0-9a-f]:/i) {    # unique local
                return 0;
            }
        }
        
        # Try to resolve hostname to IP and check if it's private
        my $resolved_ip;
        
        # First check if it's already an IP (including obfuscated formats)
        if ($host =~ /^[\d.]+$/ || $host =~ /^0[0-7]+\.0[0-7]+\.0[0-7]+\.0[0-7]+$/ || 
            $host =~ /^0x[0-9a-fA-F]+$/ || $host =~ /^\d+$/) {
            # Looks like an IP address (decimal, octal, hex, or integer format)
            # Let inet_aton handle the conversion with error handling
            my $ip_bin = eval { inet_aton($host) };
            if (defined $ip_bin) {
                $resolved_ip = inet_ntoa($ip_bin);
                if (is_private_ip($resolved_ip)) {
                    return 0;
                }
            }
        } else {
            # It's a hostname, try to resolve it with timeout protection
            eval {
                local $SIG{ALRM} = sub { die "DNS timeout\n" };
                alarm(5);  # 5 second DNS timeout
                my $ip_bin = inet_aton($host);
                alarm(0);
                if (defined $ip_bin) {
                    $resolved_ip = inet_ntoa($ip_bin);
                    if (is_private_ip($resolved_ip)) {
                        return 0;
                    }
                }
            };
            alarm(0);  # Make sure alarm is cleared
        }
        
        return 1;  # URL appears safe
    }
    
    # Function to check redirects with enhanced security
    sub check_redirect_chain {
        my $start_url = shift;
        my $max_redirects = 5;  # Reduced from 10 to prevent abuse
        
        # Validate the initial URL
        return 0 unless validate_url($start_url);
        
        # Create a UserAgent with strict security settings
        my $ua = LWP::UserAgent->new(
            timeout => 10,
            max_redirect => 0,  # We'll handle redirects manually
            max_size => 1048576,  # 1MB limit for HEAD requests (shouldn't be needed but safety)
            #agent => 'Mozilla/5.0 (compatible; Bot/1.0)',
            agent => 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0',
            protocols_allowed => ['http', 'https'],
            requests_redirectable => [],  # No automatic redirects
        );
        
        # Disable dangerous features
        $ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0);  # For testing only
        
        my $current_url = $start_url;
        my $redirect_count = 0;
        my %seen_urls = ();  # Prevent redirect loops
        
        while ($redirect_count < $max_redirects) {
            # Check for redirect loops
            return 0 if exists $seen_urls{$current_url};
            $seen_urls{$current_url} = 1;
            
            my $response = eval { $ua->head($current_url) };
            return 0 unless defined $response;  # Request failed
            
            # If it's not a redirect, we're done checking
            unless ($response->is_redirect) {
                last;
            }
            
            # Get the redirect location
            my $location = $response->header('Location');
            last unless defined $location;
            
            # Validate location header
            return 0 if length($location) > 2048;
            return 0 if $location =~ /[<>'"&`|;$(){}[\]\\]/;
            
            # Resolve relative URLs with error handling
            my $uri = eval { URI->new_abs($location, $current_url) };
            return 0 unless defined $uri;
            
            my $next_url = $uri->as_string;
            
            # Validate the redirect target
            unless (validate_url($next_url)) {
                return 0;  # Redirect leads to private/local address
            }
            
            $current_url = $next_url;
            $redirect_count++;
        }
        
        return 1;  # All URLs in the redirect chain are safe
    }
    
    # Check the URL and its potential redirect chain
    unless (check_redirect_chain($url_to_read)) {
        next;  # URL or its redirects lead to private/local addresses
    }
    
    my $page_content = pullpage2($url_to_read);
    
    if (defined $page_content) {
        my $text_content = untag($page_content);
        $text_content =~ s/\s+/ /g;
        $text_content =~ s/^\s+|\s+$//g;
        
        # Ensure text content is safe before adding to history
        $text_content =~ s/[<>&"']//g;  # Strip potentially dangerous chars
        
        if (length($text_content) > $max_readurl_text_chunk_length) {
            $text_content = substr($text_content, 0, $max_readurl_text_chunk_length) . '...';
        }
        
        # Sanitize for IRC output as well
        $display_url =~ s/[<>&"'\r\n]//g;
        
        my $history_entry = "[SYSTEM: User $usernick provided content from $url_to_read]: $text_content";
        my $context_to_track = { content => $history_entry, countdown => $linestokeepurltext };
        push @{ $url_context_lifetimes{$thingtosendtoonirc} }, $context_to_track;
        addtomessagehistory($history_entry, $thingtosendtoonirc);
        
        #print $sock "PRIVMSG $thingtosendtoonirc :Reading content from $display_url ... added it to my short-term memory.\r\n";
    } else {
        print $sock "PRIVMSG $thingtosendtoonirc :Sorry, I couldn't fetch the content from that URL.\r\n";
    }
    next;
}

		if ($message =~ /^!memorywipe ?(\d)?/){
			my $num_lines_to_remove = $1;
			if ($num_lines_to_remove) {
				my $array_ref = $userhistory{$thingtosendtoonirc};
   				pop @$array_ref for (1 .. $num_lines_to_remove);
				print $sock "PRIVMSG $thingtosendtoonirc :memory wiped: $num_lines_to_remove lines removed.\r\n";
			} else {
                delete $userhistory{$thingtosendtoonirc};
                print $sock "PRIVMSG $thingtosendtoonirc :memory wiped\r\n";
            }
			next;
		}

		if ($message =~ /^!togglestop/) {
			$globalstoptoken = ($globalstoptoken eq "\n") ? "\n\n\n" : "\n";
			my $status = ($globalstoptoken eq "\n") ? "set" : "disabled";
			print $sock "PRIVMSG $thingtosendtoonirc :stop on newline $status\r\n";
            next;
		}
		
		if ($message =~ /^!toggleserver/) {
			if ($serverip eq "127.0.0.1") {
				$serverip = $serverip2;
				print $sock "PRIVMSG $thingtosendtoonirc :server ip changing to faster LAN gpu\r\n";
			} else {
				$serverip = '127.0.0.1';
				print $sock "PRIVMSG $thingtosendtoonirc :server ip changing to localhost\r\n";
			}
            next;
		}

		if ($message =~ /^!replace (.+)?/){
			my $replacementmessage = $1;
    		pop @{$userhistory{$thingtosendtoonirc}};
			print $sock "PRIVMSG $thingtosendtoonirc :last message replaced.\r\n";
            my $replace_timestamp = get_timestamp();
			addtomessagehistory("$replace_timestamp <$usernick>: $replacementmessage", $thingtosendtoonirc);
			next;	
		}
		
if ($message =~ /^!save\s+([\w\d-]+)$/i) {
    my $requested_name = $1;
    my $safe_filepath = get_safe_filepath($requested_name);
    
    if (!defined $safe_filepath) {
        print $sock "PRIVMSG $thingtosendtoonirc :Invalid save name. Use only letters, numbers, hyphens, and underscores.\r\n";
        next;
    }
    
    # Create safe directory if it doesn't exist
    unless (-d $SAFE_DIR) {
        unless (mkdir($SAFE_DIR, 0700)) {
            warn "Failed to create save directory: $!";
            print $sock "PRIVMSG $thingtosendtoonirc :Save failed - directory error.\r\n";
            next;
        }
    }
    
    # Attempt to save with error handling
    eval {
        store \%userhistory, $safe_filepath;
        print $sock "PRIVMSG $thingtosendtoonirc :Saved history state as $requested_name.\r\n";
    };
    if ($@) {
        warn "Failed to save $safe_filepath: $@";
        print $sock "PRIVMSG $thingtosendtoonirc :Save failed.\r\n";
    }
    next;
}

if ($message =~ /^!load\s+([\w\d-]+)$/i) {
    my $requested_name = $1;
    my $safe_filepath = get_safe_filepath($requested_name);
    
    if (!defined $safe_filepath) {
        print $sock "PRIVMSG $thingtosendtoonirc :Invalid load name. Use only letters, numbers, hyphens, and underscores.\r\n";
        next;
    }
    
    if (-e $safe_filepath && -f $safe_filepath) {
        # Additional safety check - verify file size is reasonable
        my $filesize = -s $safe_filepath;
        if ($filesize > 10_000_000) {  # 10MB limit, adjust as needed
            print $sock "PRIVMSG $thingtosendtoonirc :File too large to load safely.\r\n";
            next;
        }
        
        eval {
            my $loaded_data = retrieve($safe_filepath);
            if (ref($loaded_data) eq 'HASH') {
                %userhistory = %{$loaded_data};
                print $sock "PRIVMSG $thingtosendtoonirc :Loaded \"$requested_name\".\r\n";
            } else {
                print $sock "PRIVMSG $thingtosendtoonirc :Invalid save file format.\r\n";
            }
        };
        if ($@) {
            warn "Failed to load $safe_filepath: $@";
            print $sock "PRIVMSG $thingtosendtoonirc :Load failed - corrupted file.\r\n";
        }
    } else {
        print $sock "PRIVMSG $thingtosendtoonirc :No save with name $requested_name.\r\n";
    }
    next;
}
		
		if ($message =~ /^!imagemodel\s(.+)?/i) {
			my $model = $1;
			if ($model) {
				if ($model =~ /llama/) {
					$imagemodel = '/home/superkuh/app_installs/llama.cpp/models/llava-1v5-7b-ggml-model-q5_k.gguf';
					$mmproj = '/home/superkuh/app_installs/llama.cpp/models/mmproj-model-f16.gguf';
				} elsif ($model =~ /mistral/) {
					$imagemodel = '/home/superkuh/app_installs/llama.cpp/models/bakllava-q4_k.gguf';
					$mmproj = '/home/superkuh/app_installs/llama.cpp/models/bakllava-mmproj-model-f16.gguf';
				} elsif ($model =~ /obsidian/) {
					$imagemodel = '/home/superkuh/app_installs/llama.cpp/models/obsidian-q6.gguf';
					$mmproj = '/home/superkuh/app_installs/llama.cpp/models/mmproj-obsidian-f16.gguf';	
				}
                print $sock "PRIVMSG $thingtosendtoonirc :Image model set to: $imagemodel\r\n";
			} else {
				print $sock "PRIVMSG $thingtosendtoonirc :Available: llama, mistral, obsidian. Current: $imagemodel\r\n";
			}
            next;
		}

        # !image command logic here...
		
		if ($message =~ /^!primedirective\s(.+)/){
			my $primed = $1;
			if ($primed eq "wipe") {
				undef $primedirective;
			} elsif ($primed =~ /(https?\:\/\/\S+)/gi) {
				my $page = pullpage2($1);
				$channelprimedirective{$thingtosendtoonirc} = substr($page, 0, 1600);
				print $sock "PRIVMSG $thingtosendtoonirc :Updated pre-prompt for $thingtosendtoonirc.\r\n";
			} else {
                $primedirective = $primed;
                print $sock "PRIVMSG $thingtosendtoonirc :Got it.\r\n";
            }
			next;
		}
		
		if ($message =~ /^!tokenize\s(.+)/){
			my $tokenids = send_to_local_llama_servercpp_api_tokenize($1);
			print $sock "PRIVMSG $thingtosendtoonirc :Token IDs: $tokenids\r\n";
			next;
		}
		
		if ($message =~ /^!npredict\s(\d+)/){
			$npredict = $1;
			print $sock "PRIVMSG $thingtosendtoonirc :npredict length: $npredict\r\n";
			next;
		}
		
		if ($message =~ /^!memorysize\s(\d+)/){
			my $memorysize = $1;
			if ($memorysize <= $maxhistorylength) {
				$historylength = $memorysize;
			}
			print $sock "PRIVMSG $thingtosendtoonirc :historylength set to: $historylength\r\n";
			next;
		}
		
		### --- NICKNAME TRIGGER --- ###
		if ($message =~ /$irc_nick/i or $message =~ /^$lamebottrigger/ or $message =~ /^$llamatrigger/) {
			if ($message =~ /(good(\s+fuckin[\'g]?)?\s+(bo(t|y)|g([ui]|r+)rl))|(bot(\s|\-)?snack)/i) {
				print $sock "PRIVMSG $thingtosendtoonirc ::)\r\n";
				next;
			}
			if ($message =~ /^$lamebottrigger/) {
				takemessagesendtoopenaiandsendthatresponseout_notstupid($message, $thingtosendtoonirc, $usernick, 1);
			} else {
				takemessagesendtolocalllamaAPIandsendthatresponseout($message, $thingtosendtoonirc, $usernick, 0);
			}
		}
		next;
	}

	## User list tracking
	if ($line =~ /^:(\S+)!\S+\s+JOIN\s+:\s*(\S+)/) {
        $pmwhitelist{lc $1} = 1;
    } elsif ($line =~ /^:(\S+)!\S+\s+PART\s+:\s*(\S+)/) {
        delete $pmwhitelist{lc $1};
    } elsif ($line =~ /^:(\S+)!\S+\s+QUIT\s*/) {
        delete $pmwhitelist{lc $1};
    } elsif ($line =~ /^:\S+\s+353\s+\S+\s+\S+\s+(\S+)\s+:(.*)/) {
        my @names = split /\s+/, $2;
        foreach my $name (@names) { $name =~ s/^[@+]//; $pmwhitelist{lc $name} = 1; }
    } elsif ($line =~ /^:([^!]+)![^@]+\@[^ ]+ NICK :(.+)/) {
        delete $pmwhitelist{lc $1};
        $pmwhitelist{lc $2} = 1;
    }
	} else {
        print "No data received for $timeout seconds. Reconnecting...\n";
		goto CONNECT;
    }
}

sub get_timestamp {
    my ($sec, $min, $hour, $mday, $mon, $year) = localtime(time);
    return sprintf("[%04d-%02d-%02d %02d:%02d:%02d]", $year + 1900, $mon + 1, $mday, $hour, $min, $sec);
}

sub takemessagesendtolocalllamaAPIandsendthatresponseout {
	my ($message, $thingtosendtoonirc, $usernick, $gpt35turbo) = @_;
	
	$message = cleanupusermessages($message);
	return if $message eq '';
	
	if (defined $channelprimedirective{$thingtosendtoonirc}) {
		$primedirective = $channelprimedirective{$thingtosendtoonirc};
	} else {
		$primedirective = $primedirectivedefault;
	}
    $primedirective =~ s/\\n/\n/g;
    
	my $chathistorylines = join("\n", @{$userhistory{$thingtosendtoonirc} // []});
    
    my $final_prompt = $primedirective . $chathistorylines . "\n" . get_timestamp() . " <$irc_nick>:";
    print "FINAL_PROMPT: $final_prompt\n" if $debugon;

	my $response = send_to_local_llama_servercpp_api($final_prompt);
	
	if ($response =~ /.*?[:,)]\s*$/) {
		my $tmpresponse = $response;
		chomp($tmpresponse);
		my $continuation_prompt = $final_prompt . $tmpresponse;
		my $continued_response = send_to_local_llama_servercpp_api($continuation_prompt);
		$response = "$tmpresponse " . $continued_response;
	}
	
	return unless ($response); 

    my $bot_timestamp = get_timestamp();
	if (length($response) > $ircmessagemaxlength) {
		my @irclengthmessages = cutintoirclengthmessagesandreturnarrayofthem($response);
		print $sock "PRIVMSG $thingtosendtoonirc :$_\r\n" for @irclengthmessages;
	} else {
		print $sock "PRIVMSG $thingtosendtoonirc :$response\r\n";
	}
    addtomessagehistory("$bot_timestamp <$irc_nick>: $response", $thingtosendtoonirc);
}

sub send_to_local_llama_servercpp_api {
	my ($input) = @_;
	my $ua = LWP::UserAgent->new;
	my $url = 'http://' . $serverip . ':8080/completion';
	my %params = (
		prompt => $input,
		n_predict => 0+$npredict,
		temperature => 0.15,
		repeat_penalty => 1.2,
		stop => ['!llama', "\n\n", $globalstoptoken, "<", "["],
   		cache_prompt => \1,
	);
	my $response = $ua->post($url, 'Content-Type' => 'application/json', Content => encode_json(\%params));
	if ($response->is_success) {
        my $json = decode_json($response->content);
        return cleanupopenaimessages($json->{'content'});
    } else {
		return "API Error: " . $response->status_line;
	}
}

sub summarize_text {
    my ($text_to_summarize) = @_;
    my $clean_text = $text_to_summarize;
    $clean_text =~ s/^\[SYSTEM.*?\]:\s*//;
    #my $summary_prompt = "Provide a concise, one-sentence summary of the following text:\n\n$clean_text";
    my $summary_prompt = "Provide a concise, paragraph long summary of the following text. Ignore boilerplate stuff at the start and end if it exists and concentration on summarizing the major text of the article:\n\n$clean_text";
    my $summary = send_to_local_llama_servercpp_api($summary_prompt);
    return undef if $summary =~ /^API Error:/;
    return $summary;
}

sub addtomessagehistory {
    my ($message, $context_key) = @_;
    push @{$userhistory{$context_key}}, $message;
    shift @{$userhistory{$context_key}} while @{$userhistory{$context_key}} > $historylength;

    return unless exists $url_context_lifetimes{$context_key};
    my @active_contexts;
    foreach my $context (@{ $url_context_lifetimes{$context_key} }) {
        $context->{countdown}--;
        if ($context->{countdown} <= 0) {
            my $summary = summarize_text($context->{content});
            if (defined $summary) {
                my $summary_entry = "[SYSTEM: Summary of previous URL context]: $summary";
                my $history_ref = $userhistory{$context_key};
                for my $i (0 .. $#{$history_ref}) {
                    if ($history_ref->[$i] eq $context->{content}) {
                        $history_ref->[$i] = $summary_entry;
                        last;
                    }
                }
            }
        } else {
            push @active_contexts, $context;
        }
    }
    if (@active_contexts) {
        $url_context_lifetimes{$context_key} = \@active_contexts;
    } else {
        delete $url_context_lifetimes{$context_key};
    }
}

sub cleanupusermessages {
	my ($message) = @_;
	$message =~ s/\x03\d{0,2}(,\d{0,2})?|\x0f|\x02|\x16|\x1d|\x1f//g;
	$message =~ s/$irc_nick[,\s:]*//ig;
	$message = ucfirst($message);
	$message =~ s/^\s+|\s+$//g;
	$message =~ s/$lamebottrigger//g;
	$message .= '.' unless $message =~ /[.?!]$/; 
	return $message;
}

sub cleanupopenaimessages {
	my ($text) = @_;
	$text =~ s/\n/ /g;
	$text =~ s/^\s+|\s+$//g;
    $text =~ s/^\. //;
	return $text;
}

sub cutintoirclengthmessagesandreturnarrayofthem {
	my ($response) = @_;
    return ($response) if length($response) <= $ircmessagemaxlength;
	my @multiplemessages = split /(?(?{pos() % $ircmessagemaxlength})(?!))/, $response;
	s/^\s+|\s+$//g for @multiplemessages;
	return @multiplemessages;
}

sub pullpage2_old {
  my ($text) = @_;
  if ($text =~ m!(https?://[\w_-]+(?:(?:\.[\w_-]+)+)[\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])!) {
    my $cua = LWP::UserAgent->new( protocols_allowed => ['http', 'https'], timeout => 3 );
    $cua->agent('Mozilla/5.0');
    $cua->max_size(40000);
    my $cres = $cua->get($1);
    return $cres->decoded_content if $cres->is_success;
  }
  return undef;
}

sub pullpage2 {
  my ($text) = @_;
  if ($text =~ m!(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])!) {
    my $text_uri = "$1://$2$3";
    ## original
    #my $cres = `w3m -dump $text_uri`;
    ## extra crispy
    #my $cres = `w3m -o user_agent="Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" -dump $text_uri`;
    ## hot
    my $user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0';
    my $safe_uri = shell_quote($text_uri);
    my $safe_ua = shell_quote($user_agent);
    my $cres = `w3m -o user_agent=$safe_ua -dump $safe_uri`;
    return $cres if $cres;
  }
  return undef;
}

sub untag {
  local $_ = $_[0] || $_;
  s{< (?: (!--) | (\?) | (?i:(TITLE|SCRIPT|APPLET|OBJECT|STYLE)) | ([!/A-Za-z]) ) (?(4) (?: (?! [\s=] ["`'] ) [^>] | [\s=] `[^`]*` | [\s=] '[^']*' | [\s=] "[^"]*" )* | .*? ) (?(1) (?<=--) ) (?(2) (?<=\?) ) (?(3) </ (?i:\3) (?:\s[^>]*)? ) >}{}gsx;
  return $_ ? $_ : "";
}

sub send_to_local_llama_servercpp_api_tokenize {
	my ($input) = @_;
	my $ua = LWP::UserAgent->new;
	my $url = "http://$serverip:8080/tokenize";
	my %params = ("content" => "$input");
	my $response = $ua->post($url, 'Content-Type' => 'application/json', Content => encode_json(\%params));
	if ($response->is_success) {
        my $json = decode_json($response->content);
        my $tokens_ref = $json->{"tokens"};
		my $text = "";
		foreach my $number (@$tokens_ref) {
        	my $lookuptext = tokenvocablookup($json_file, $number);
        	$text .= "$number:$lookuptext ";
    	}	
        return $text;
    } else {
		return "Error: " . $response->status_line;
	}
}

sub tokenvocablookup {
    my ($json_file, $token_number) = @_;
    open my $json_fh, '<', $json_file or die "Cannot open $json_file: $!";
    my $json_text = do { local $/; <$json_fh> };
    close $json_fh;
    my $data = JSON::XS::decode_json($json_text);
    if (exists $data->{model}{vocab}) {
        my $vocab = $data->{model}{vocab};
        foreach my $key (keys %$vocab) {
            if ($vocab->{$key} == $token_number) {
                $key =~ s/\\/\\\\/g; $key =~ s/\n/\\n/g; $key =~ s/\r/\\r/g; $key =~ s/\t/\\t/g;
                return $key;
            }
        }
    }
    return "Token not found";
}

sub takemessagesendtoopenaiandsendthatresponseout_notstupid {
	my ($message, $thingtosendtoonirc, $usernick, $gpt35turbo) = @_;
	$message = cleanupusermessages($message);
	return if $message eq '';
	my $chathistorylines = join("\n", @{$userhistory{$thingtosendtoonirc} // []});
	my $response = send_to_openai_api_chatgpt35_notstupid($message, $chathistorylines);
    my $bot_timestamp = get_timestamp();
	if (length($response) > $ircmessagemaxlength) {
		my @irclengthmessages = cutintoirclengthmessagesandreturnarrayofthem($response);
		print $sock "PRIVMSG $thingtosendtoonirc :$_\r\n" for @irclengthmessages;
        addtomessagehistory("$bot_timestamp <$irc_nick>: $response", $thingtosendtoonirc);
	} else {
		print $sock "PRIVMSG $thingtosendtoonirc :$response\r\n";
		addtomessagehistory("$bot_timestamp <$irc_nick>: $response", $thingtosendtoonirc);
	}
}

sub send_to_openai_api_chatgpt35_notstupid {
	my ($input, $memorylines) = @_;
	my $primedirectivelocal = $primedirective;
	my $url = "https://api.openai.com/v1/chat/completions";
	my $model = "gpt-4o";
	my $payload = { "model" => $model, "messages" => [], "max_tokens" => 190 };
	if ($primedirectivelocal) {
		push @{$payload->{messages}}, { "role" => "system", "content" => "$primedirectivelocal" };
	}
	my @memoryarray = split /\n/, $memorylines;
	foreach my $historyline (@memoryarray) {
		push @{$payload->{messages}}, { "role" => "user", "content" => "$historyline" };
	}
	push @{$payload->{messages}}, { "role" => "user", "content" => "$input" };
	my $json_payload = encode_json($payload);
	my $ua = LWP::UserAgent->new();
	my $req = HTTP::Request->new('POST', $url);
	$req->header('Authorization' => "Bearer $openai_api_key", 'Content-Type' => 'application/json');
	$req->content($json_payload);
	my $response = $ua->request($req);
	if ($response->is_success) {
        my $json = decode_json($response->content);
		my $text = $json->{'choices'}->{'message'}->{'content'};
       	$text =~ s/\n/ /g;
      	$text =~ s/^\s+|\s+$//g;
        $text =~ s/^\. //;
        return $text;
	} else {
		return "Error: " . $response->status_line;
	}
}

sub sanitize_filename {
    my ($filename) = @_;
    
    # Remove any path separators and potentially dangerous characters
    $filename =~ s/[^a-zA-Z0-9_-]//g;
    
    # Limit length to prevent issues
    $filename = substr($filename, 0, $MAX_FILENAME_LENGTH);
    
    # Ensure it's not empty after sanitization
    return undef if length($filename) == 0;
    
    return $filename;
}

sub get_safe_filepath {
    my ($basename) = @_;
    
    # Sanitize the filename
    my $safe_name = sanitize_filename($basename);
    return undef unless defined $safe_name;
    
    # Create the full path within our safe directory
    my $filename = "$safe_name.chathistory";
    my $filepath = File::Spec->catfile($SAFE_DIR, $filename);
    
    # Resolve any relative path components and ensure it's still in our safe directory
    my $abs_filepath = File::Spec->rel2abs($filepath);
    my $abs_safe_dir = File::Spec->rel2abs($SAFE_DIR);
    
    # Check that the resolved path is still within our safe directory
    unless (index($abs_filepath, $abs_safe_dir) == 0) {
        warn "Attempted path traversal attack: $basename";
        return undef;
    }
    
    return $abs_filepath;
}
