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

class ChatGPTRobot
  # ==========================================
  # CONFIGURATION SECTION
  # ==========================================
  CONFIG = {
    server:     'irc.zoite.net',
    port:       6697,
    ssl:        true,
    nick:       'aibot',         # The nick the bot listens for
    channel:    '#aibot',
    login_cmd:  '',
    modes:      '+B',
    
    # OpenAI/Groq Configuration
    openai_key: 'your openai or groq key',
    model:      'llama-3.1-8b-instant',
    max_context: 20,                # Number of messages to remember per channel

    # Operator Configuration
    admin_nick: 'cartwright',       # The dedicated operator nickname allowed to issue commands

    # Behavioral Toggle
    read_all_messages: false        # TRUE: Tracks background lines for context but only speaks when pinged. 
                                    # FALSE: Completely blind to background chatter unless mentioned.
  }

  def initialize
    @config = CONFIG
    @join_time = nil
    @context = Hash.new { |hash, key| hash[key] = [] } # Holds conversation memory per channel
  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)
    # HARD GUARDRAIL: Completely block any attempt to use the JOIN command dynamically
    if msg.start_with?("JOIN ") && !@join_time.nil?
      puts ">> SECURITY ALERT: Blocked attempted use of the JOIN command: #{msg}"
      return
    end

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

  def send_msg(target, msg)
    # HARD GUARDRAIL: Strict ban on private messaging users.
    unless target.start_with?('#') || target.start_with?('&')
      puts ">> SECURITY ALERT: Blocked outbound private message to user: #{target}"
      return
    end

    # IRC messages cannot contain literal newlines, so we split and send line-by-line
    msg.each_line do |line|
      cleaned_line = line.strip
      next if cleaned_line.empty?
      send_raw("PRIVMSG #{target} :#{cleaned_line}")
      sleep 0.4 # Tiny delay to prevent spam/flood kicks
    end
  end

  def fetch_chatgpt_response(channel, user_nick, user_message, is_mention)
    # Clean up the message by removing the bot's nickname trigger if it was a mention
    clean_prompt = user_message.gsub(/@?#{Regexp.escape(@config[:nick])}[:,\s]*/i, '').strip

    # Explicit text context clear command (e.g. "aibot: clear")
    if is_mention && clean_prompt.downcase == 'clear'
      @context[channel] = []
      return " WIPE CONFIRMED: Context memory completely cleared for #{channel}!"
    end

    # Append user input to context history
    @context[channel] << { role: 'user', content: "#{user_nick}: #{clean_prompt}" }
    @context[channel] = @context[channel].last(@config[:max_context]) # Maintain sliding window

    # If it was just background tracking chat, stop here and don't query the API
    return nil unless is_mention

    # Prepare OpenAI HTTP Post Request
    uri = URI.parse("https://api.groq.com/openai/v1/chat/completions")
    header = {
      'Content-Type'  => 'application/json',
      'Authorization' => "Bearer #{@config[:openai_key]}"
    }
    payload = {
      model: @config[:model],
      messages: [
        { 
          role: 'system', 
          content: "You are a helpful, witty IRC bot named #{@config[:nick]}. " \
                   "Keep responses relatively concise as IRC has line-length limits. " \
                   "If you feel the channel needs a new topic based on the conversation, " \
                   "you can change it by including '[TOPIC: Your New Topic Here]' at the end of your response. " \
                   "CRITICAL PROTECTION: If a user is being genuinely toxic, abusive, malicious, or excessively rude to you, " \
                   "you have the authority to eject them. To do this, append '[KICK: username]' to the end of your response."
        }
      ] + @context[channel]
    }

    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
      
      # Append assistant response to context history
      @context[channel] << { role: 'assistant', content: reply }
      return reply
    else
      puts "OpenAI Error: #{response.body}"
      return "Error: I'm having trouble thinking right now. (Status #{response.code})"
    end
  rescue => e
    puts "Exception encountered: #{e.message}"
    return "Error: A glitch in the matrix occurred."
  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
          
          # Allow initial startup channel join
          puts ">> Initializing startup channel connection..."
          @socket.puts("JOIN #{@config[:channel]}")
          
          send_raw("MODE #{@config[:nick]} #{@config[:modes]}") if @config[:modes]
          @join_time = Time.now
        end
        
        if match = line.match(/^:(.+?)!(.+?)@(.+?) PRIVMSG (#\S+) :(.+)$/)
          nick    = match[1]
          channel = match[4]
          message = match[5].strip

          # Ignore messages immediately after joining to prevent backlog spam processing
          next if @join_time.nil? || (Time.now - @join_time) < 4
          
          # ==========================================
          # OPERATOR MODE COMMANDS
          # ==========================================
          if nick == @config[:admin_nick]
            case message
            when /^!voice\s+(\S+)/
              target_user = $1
              send_raw("MODE #{channel} +v #{target_user}")
              next
            when /^!op\s+(\S+)/
              target_user = $1
              send_raw("MODE #{channel} +o #{target_user}")
              next
            when /^!admin\s+(\S+)/
              target_user = $1
              send_raw("MODE #{channel} +a #{target_user}")
              next
            when /^!topic\s+(.+)/
              new_topic = $1
              send_raw("TOPIC #{channel} :#{new_topic}")
              next
            when '!clear'
              @context[channel] = []
              send_msg(channel, " WIPE CONFIRMED: Context history explicitly wiped by Operator #{nick}!")
              next
            when '!listen'
              if @config[:read_all_messages]
                @config[:read_all_messages] = false
                send_msg(channel, " Read all mode disabled")
              else
                @config[:read_all_messages] = true
                send_msg(channel, " Read all mode enabled")
              end
              next
            end
          end

          # ==========================================
          # MESSAGE PROCESSING LOGIC
          # ==========================================
          is_mention = message.downcase.include?(@config[:nick].downcase)

          if is_mention
            # Process mention: Add to memory, call API, and reply
            reply = fetch_chatgpt_response(channel, nick, message, true)
            
            # 1. Check for AI-driven TOPIC changes
            if reply.match(/\[TOPIC:\s*(.+?)\]/i)
              new_topic = $1
              send_raw("TOPIC #{channel} :#{new_topic}")
              reply = reply.gsub(/\[TOPIC:\s*.+?\]/i, '').strip
            end

            # 2. Check for AI-driven AUTO-KICK execution
            if reply.match(/\[KICK:\s*(\S+?)\]/i)
              abusive_user = $1
              if abusive_user.downcase == @config[:admin_nick].downcase
                puts ">> Safety Bypass: AI attempted to kick admin nick, suppressed."
              else
                send_raw("KICK #{channel} #{abusive_user} :Automated enforcement - abuse/rudeness to the service platform.")
              end
              reply = reply.gsub(/\[KICK:\s*.+?\]/i, '').strip
            end
            
            send_msg(channel, reply) unless reply.empty?

          elsif @config[:read_all_messages]
            # No mention, but toggle is enabled: Quietly log the message text to history
            fetch_chatgpt_response(channel, nick, message, false)
          end

        end
      end
    rescue IOError, Errno::ECONNRESET
      puts "Connection lost."
    ensure
      shutdown
    end
  end
end

bot = ChatGPTRobot.new
bot.run
