# ChatGPT IRC Bot Copyright (C) 2026 cartwright
require 'socket'
require 'openssl'
require 'net/http'
require 'uri'
require 'json'

class ChatGPTRobot
  # ==========================================
  # CONFIGURATION SECTION
  # ==========================================
  CONFIG = {
    server:         'lion.tx.us.dal.net',
    port:           6697,
    ssl:            true,
    nick:           'John_Doe',         
    
    # Multi-Channel Support Configurations
    channels:       ['#watcher', '#allnitecafe', '#cafechat'], # List all channels to monitor
    output_channel: '#watcher',                                # Central reporting channel for alerts
    
    # Whitelist Configuration (Nicks listed here bypass all keyword triggers entirely)
    whitelist:      ['Quizimodo', 'Rebecca', 'CafeNews', 'Belanna__', 'CafeChat', 'FatFemale', 'Holbrook', 'Mimi^', 'mondino', 'Nando', 'Pigeon', 'sandro', 'SexyHotChick', 'toolman', 'Turkey', 'USA', 'zero', 'QuiziOFF'],
    
    login_cmd:      '',
    modes:          '',
    
    # Target Enforcement Lists & Rules (Supports both Strings and Regexp patterns)
    keywords:       [
      'wife', 'husband', 'iq', 'dumb', 'boring', 'jerk', 
      /^lo{2,}l$/i, # Regex matching lool, loool, etc., standalone and case-insensitively
      'jew', 'jews', 'losers', 'loosers', 'ugly', 'j3rks', 'frying pan', 
      'stup1d', 'stu|p1d', 'stup|1d', 'b0ring', 'slut', 'faggot', 'gay'
    ], 
    
    # OpenAI/Groq Configuration
    openai_key:     'your_grok_or_openai_key', # If using OpenAI change the API endpoint below
    model:          'llama-3.1-8b-instant',
    max_context:    20,     # Context window for AI evaluation

    # Operator Configuration
    admin_nick:     'cartwright'       
  }

  def initialize
    @config = CONFIG
    @join_time = nil
    @context = Hash.new { |hash, key| hash[key] = [] } 
    
    # Scoped tracking hash: @tracked_users[channel][nick.downcase] = [messages]
    @tracked_users = Hash.new { |hash, key| hash[key] = {} }                            
    @allow_dynamic_join = false 
  end

  def connect
    puts "Connecting to #{@config[:server]}:#{@config[:port]}..."
    tcp_socket = TCPSocket.new(@config[:server], @config[:port])
    
    if @config[:ssl]
      ssl_context = OpenSSL::SSL::SSLContext.new
      ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE)
      @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
      @socket.sync_close = true
      @socket.connect
    else
      @socket = tcp_socket
    end

    send_raw("NICK #{@config[:nick]}")
    send_raw("USER #{@config[:nick]} 0 * :#{@config[:nick]} Bot")
  end

  def shutdown
    puts "\nShutting down ChatGPT Bot..."
    if @socket && !@socket.closed?
      send_raw("QUIT :Going offline!")
      @socket.close
    end
    puts "Goodbye!"
    exit
  end
  
  def send_raw(msg)
    if msg.start_with?("JOIN ") && !@join_time.nil? && !@allow_dynamic_join
      puts ">> SECURITY ALERT: Blocked attempted use of the JOIN command: #{msg}"
      return
    end

    puts ">> #{msg}"
    @socket.puts(msg)
  end

  def evaluate_derogatory(channel, user_nick, user_message)
    uri = URI.parse("https://api.groq.com/openai/v1/chat/completions")
    header = {
      'Content-Type'  => 'application/json',
      'Authorization' => "Bearer #{@config[:openai_key]}"
    }
    
    system_instruction = <<~TEXT
      Do not talk. Your sole job is to evaluate if the target conversation block is actively derogatory, insulting, or abusive.
      
      CRITICAL EVALUATION RULES:
      1. The input contains a series of numbered lines sent by the target user. Look at the context of the entire conversation.
      2. If the user is merely expressing casual apathy, general boredom, disinterest, or indifference (e.g., saying "boring", "nothing", "i don't care", "trivia"), you MUST respond with NO. Conversational passivity or negative mood is NOT an active insult.
      3. Analyze the overall background history alongside these target lines to determine if this sequence represents an active insult or ongoing attack.
      4. If and only if the block is clearly and unambiguously being used to insult, mock, threaten, or demean someone maliciously, respond with exactly one word: YES.
      5. Otherwise, respond with exactly one word: NO.
      6. Do not include any punctuation, quotes, or extra text.
    TEXT

    payload = {
      model: @config[:model],
      messages: [
        { role: 'system', content: system_instruction }
      ] + @context[channel] + [
        { role: 'user', content: "TARGET LINES TO EVALUATE -> \nUser #{user_nick} said:\n#{user_message}" }
      ]
    }

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    request = Net::HTTP::Post.new(uri.request_uri, header)
    request.body = payload.to_json
    response = http.request(request)
    
    if response.code == '200'
      res_body = JSON.parse(response.body)
      reply = res_body['choices'][0]['message']['content'].strip.upcase
      puts ">> AI Evaluation for #{user_nick} in #{channel}: #{reply}"
      return reply == 'YES'
    else
      puts "OpenAI Error: #{response.body}"
      return false
    end
  rescue => e
    puts "Exception encountered: #{e.message}"
    return false
  end

  def send_msg(target, msg)
    msg.each_line do |msg_line|
      cleaned_line = msg_line.strip
      next if cleaned_line.empty?
      send_raw("PRIVMSG #{target} :#{cleaned_line}")
      sleep 0.4 
    end
  end
  
  def run
    connect
    Signal.trap("INT") { shutdown }
    Signal.trap("TERM") { shutdown }
    
    begin
      while line = @socket.gets
        line.strip!
        puts "<< #{line}"
        
        if line.start_with?("PING")
          send_raw(line.sub("PING", "PONG"))
          next
        end

        if line.match(/ 376 /) || line.match(/ 422 /)
          send_raw(@config[:login_cmd]) if @config[:login_cmd] && !@config[:login_cmd].empty?
          sleep 2
          
          # Join all channels defined in the config array
          @config[:channels].each do |chan|
            @socket.puts("JOIN #{chan}")
            sleep 0.2
          end
          
          send_raw("MODE #{@config[:nick]} #{@config[:modes]}") if @config[:modes]
          @join_time = Time.now
        end

        if match = line.match(/^:(.+?)!.+? INVITE #{@config[:nick]} :(.+)$/)
          inviter = match[1]
          target_channel = match[2].strip
          if inviter.downcase == @config[:admin_nick].downcase
            @allow_dynamic_join = true
            send_raw("JOIN #{target_channel}")
            @allow_dynamic_join = false
          end
          next
        end
        
        if match = line.match(/^:(.+?)!.+? PRIVMSG (#\S+) :(.+)$/)
          nick    = match[1]
          channel = match[2]
          message = match[3].strip

          next if @join_time.nil? || (Time.now - @join_time) < 4
          
          # ==========================================
          # ADMIN COMMANDS
          # ==========================================
          if nick == @config[:admin_nick]
            case message
            when /^!voice\s+(\S+)/
              send_raw("MODE #{channel} +v #{$1}")
              next
            when /^!op\s+(\S+)/
              send_raw("MODE #{channel} +o #{$1}")
              next
            when '!clear'
              @context[channel] = []
              @tracked_users[channel].clear
              next
            end
          end

          # ==========================================
          # WATCH LOGIC & CONTEXT MANAGEMENT
          # ==========================================
          # Check if nickname matches the whitelist array case-insensitively
          is_whitelisted = @config[:whitelist].any? { |w| w.downcase == nick.downcase }

          unless is_whitelisted
            contains_keyword = @config[:keywords].any? do |pattern|
              if pattern.is_a?(Regexp)
                message =~ pattern
              else
                message.downcase.include?(pattern.downcase)
              end
            end
            is_tracked = @tracked_users[channel].key?(nick.downcase)

            # Step 1: Initialize isolated channel buffer if keyword drops
            if contains_keyword && !is_tracked
              puts ">> Keyword triggered by #{nick} in #{channel}. Collecting 5 messages."
              @tracked_users[channel][nick.downcase] = []
              is_tracked = true
              
              report_target = @config[:output_channel]
              send_msg(report_target, "[Surveillance Activated] Now gathering a 5-line batch for [User: #{nick}] in [Channel: #{channel}]. Trigger context: \"#{message}\"")
            end

            # Step 2: Accumulate messages if surveillance is active for this user in this channel
            if is_tracked
              @tracked_users[channel][nick.downcase] << message

              if @tracked_users[channel][nick.downcase].size < 5
                @context[channel] << { role: 'user', content: "#{nick}: #{message}" }
                @context[channel] = @context[channel].last(@config[:max_context])
                next
              else
                batched_lines = @tracked_users[channel][nick.downcase].map.with_index { |msg, idx| "[Line #{idx + 1}] #{msg}" }.join(" | ")
                puts ">> Submitting batched transaction to AI for #{nick} from #{channel}..."

                report_target = @config[:output_channel] # Instantiated early to prevent downstream scope errors

                if evaluate_derogatory(channel, nick, batched_lines)
                  if nick.downcase == @config[:admin_nick].downcase
                    send_raw("PRIVMSG #{channel} :Tried to kick bot admin #{nick}, suppressed.")
                  else
                    send_msg(report_target, "Unfriendly pattern identified from [User: #{nick}] in [Channel: #{channel}] across last 5 lines:")
                    send_msg(report_target, @tracked_users[channel][nick.downcase].map { |m| "-> \"#{m}\"" }.join("\n"))
                    # send_raw("KICK #{channel} #{nick} :Automated enforcement - pattern breach.")
                    
                    @tracked_users[channel].delete(nick.downcase)
                    next 
                  end
                end
                send_msg(report_target, "[Surveillance de-activated]")
                @tracked_users[channel].delete(nick.downcase)
              end
            end
          end

          # Step 3: Append ordinary traffic to channel-specific background logs
          @context[channel] << { role: 'user', content: "#{nick}: #{message}" }
          @context[channel] = @context[channel].last(@config[:max_context])
        end
      end
    rescue IOError, Errno::ECONNRESET
      puts "Connection lost."
    ensure
      shutdown
    end
  end
end

bot = ChatGPTRobot.new
bot.run
