class SSCBot::ChatLog::MessageParser

@author Jonathan Bradley Whited @since 0.1.0

Constants

MAX_NAMELEN

Attributes

check_history_count[RW]
commands[R]
messages[R]
namelen[RW]
regex_cache[R]

Public Class Methods

new(autoset_namelen: true,check_history_count: 5,namelen: nil,store_history: true,strict: true) click to toggle source
Calls superclass method
# File lib/ssc.bot/chat_log/message_parser.rb, line 41
def initialize(autoset_namelen: true,check_history_count: 5,namelen: nil,store_history: true,strict: true)
  super()

  @autoset_namelen = autoset_namelen
  @check_history_count = check_history_count
  @commands = {}
  @messages = []
  @namelen = namelen
  @regex_cache = {}
  @store_history = store_history
  @strict = strict
end

Public Instance Methods

clear_history() click to toggle source
# File lib/ssc.bot/chat_log/message_parser.rb, line 374
def clear_history
  @messages.clear
end
command?(type,name,delete: true) click to toggle source
# File lib/ssc.bot/chat_log/message_parser.rb, line 393
def command?(type,name,delete: true)
  return true if @check_history_count < 1

  type_hash = @commands[type]

  if !type_hash.nil?
    index = type_hash[name]

    if !index.nil? && (@messages.length - index) <= @check_history_count
      type_hash.delete(name) if delete

      return true
    end
  end

  return false
end
match_kill?(line) click to toggle source

@example Format

'  Killed.Name(100) killed by: Killer.Name'
# File lib/ssc.bot/chat_log/message_parser.rb, line 413
def match_kill?(line)
  return false if line.length < 19 # '  N(0) killed by: N'

  return /\A  (?<killed>.*?\S)\((?<bounty>\d+)\) killed by: (?<killer>.*?\S)\z/.match(line)
end
match_player(line,type_name:,name_prefix: '',name_suffix: '> ',type_prefix: /../,use_namelen: true) click to toggle source

The Ruby interpreter should cache the args' default values, so no reason to manually cache them unless a variable is involved inside.

@example Default Format

'X Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 59
def match_player(line,type_name:,name_prefix: '',name_suffix: '> ',type_prefix: /../,use_namelen: true)
  cached_regex = @regex_cache[type_name]

  if cached_regex.nil?
    cached_regex = {}
    @regex_cache[type_name] = cached_regex
  end

  use_namelen &&= !@namelen.nil?
  key = use_namelen ? @namelen : :no_namelen
  regex = cached_regex[key]

  if regex.nil?
    name_prefix = Util.quote_str_or_regex(name_prefix)
    name_suffix = Util.quote_str_or_regex(name_suffix)
    type_prefix = Util.quote_str_or_regex(type_prefix)

    if use_namelen
      name = /.{#{@namelen}}/
    else
      name = /.*?\S/
    end

    name = Util.quote_str_or_regex(name)

    # Be careful to not use spaces ' ', but to use '\\ ' (or '\s') instead
    #   because of the '/x' option.
    regex = /
      \A#{type_prefix}
      #{name_prefix}(?<name>#{name})#{name_suffix}
      (?<message>.*)\z
    /x

    cached_regex[key] = regex
  end

  return regex.match(line)
end
match_pub?(line) click to toggle source

@example Format

'  Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 421
def match_pub?(line)
  return false if line.length < 5 # '  N> '

  match = match_player(line,type_name: :pub,type_prefix: '  ')

  if !match.nil?
    name = Util.u_lstrip(match[:name])

    if name.empty? || name.length > MAX_NAMELEN
      return false
    end
  end

  return match
end
match_q_find?(line) click to toggle source

@example Format

'  Not online, last seen more than 10 days ago'
'  Not online, last seen 9 days ago'
'  Not online, last seen 18 hours ago'
'  Not online, last seen 0 hours ago'
'  Name - Public 0'
'  TWCore - (Private arena)'
'  Name is in SSCJ Devastation'
'  Name is in SSCC Metal Gear CTF'
# File lib/ssc.bot/chat_log/message_parser.rb, line 446
def match_q_find?(line)
  return false if line.length < 7 # '  N - A'
  return false unless command?(:pub,:?find)

  if line.start_with?('  Not online, last seen ')
    match = line.match(/(?<more>more) than (?<days>\d+) days ago\z/)
    match = line.match(/(?<days>\d+) days? ago\z/) if match.nil?
    match = line.match(/(?<hours>\d+) hours? ago\z/) if match.nil?

    return match
  else
    match = line.match(/\A  (?<player>.+) is in (?<zone>.+)\z/)
    match = line.match(/\A  (?<player>.+) - (?<arena>.+)\z/) if match.nil?

    if match
      caps = match.named_captures

      player = caps['player']

      return false if player.length > MAX_NAMELEN

      if caps.key?('arena')
        area = caps['arena']
      elsif caps.key?('zone')
        area = caps['zone']
      else
        return false
      end

      # If do /\A  (?<player>[^[[:space:]]].+[^[[:space:]])/, then it won't
      #   capture names/zones/arenas that are only 1 char long, so do this.
      [player[0],player[-1],area[0],area[-1]].each do |c|
        if c =~ /[[:space:]]/
          return false
        end
      end

      return match
    end
  end

  return false
end
match_q_log?(line) click to toggle source

@example Format

'  Log file open: session.log'
'  Log file closed'
# File lib/ssc.bot/chat_log/message_parser.rb, line 493
def match_q_log?(line)
  return false if line.length < 17

  match = /\A  Log file open: (?<filename>.+)\z/.match(line)
  match = /\A  Log file closed\z/.match(line) if match.nil?

  return match
end
match_q_namelen?(line) click to toggle source

@example Format

'  Message Name Length: 24'
# File lib/ssc.bot/chat_log/message_parser.rb, line 504
def match_q_namelen?(line)
  return false if line.length < 24 # '...: 0'
  return false if line[21] != ':'

  return /\A  Message Name Length: (?<namelen>\d+)\z/.match(line)
end
match_remote?(line) click to toggle source

@example Format

# NOT affected by namelen.
'P :Self.Name:Message'
'P (Name)>Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 515
def match_remote?(line)
  return false if line.length < 5 # 'P :N:'

  case line[2]
  when ':'
    return match_player(line,type_name: :remote.out,
      name_prefix: ':',name_suffix: ':',use_namelen: false)
  when '('
    return match_player(line,type_name: :remote.in,
      name_prefix: '(',name_suffix: ')>',use_namelen: false)
  end

  return false
end
parse(line) click to toggle source
# File lib/ssc.bot/chat_log/message_parser.rb, line 98
def parse(line)
  if line.nil?
    if @strict
      raise ArgumentError,"invalid line{#{line.inspect}}"
    else
      line = ''
    end
  end

  message = nil

  if !line.empty?
    case line[0]
    when 'C'
      message = parse_chat(line)
    when 'E'
      message = parse_freq(line)
    when 'P'
      if (match = match_remote?(line))
        message = parse_remote(line,match: match)
      else
        message = parse_private(line)
      end
    when 'T'
      message = parse_team(line)
    else
      # Check this one first to prevent abuse from pubbers.
      if (match = match_pub?(line))
        message = parse_pub(line,match: match)
      else
        if (match = match_kill?(line))
          message = parse_kill(line,match: match)
        elsif (match = match_q_log?(line))
          message = parse_q_log(line,match: match)
        elsif (match = match_q_namelen?(line))
          message = parse_q_namelen(line,match: match)
        else
          # These are last because too flexible.
          if (match = match_q_find?(line))
            message = parse_q_find(line,match: match)
          end
        end
      end
    end
  end

  if message.nil?
    message = Message.new(line,type: :unknown)
  end

  if @store_history
    @messages << message
  end

  return message
end
parse_chat(line) click to toggle source

@example Format

# NOT affected by namelen.
'C 1:Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 158
def parse_chat(line)
  match = match_player(line,type_name: :chat,name_prefix: /(?<channel>\d+)\:/,use_namelen: false)
  player = parse_player(line,type_name: :chat,match: match)

  return nil if player.nil?

  channel = match[:channel].to_i

  return ChatMessage.new(line,channel: channel,name: player.name,message: player.message)
end
parse_freq(line) click to toggle source

@example Format

'E Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 171
def parse_freq(line)
  player = parse_player(line,type_name: :freq)

  return nil if player.nil?

  return FreqMessage.new(line,name: player.name,message: player.message)
end
parse_kill(line,match:) click to toggle source

@example Format

'  Killed.Name(100) killed by: Killer.Name'
# File lib/ssc.bot/chat_log/message_parser.rb, line 181
def parse_kill(line,match:)
  if match.nil?
    if @strict
      raise ParseError,"invalid kill message{#{line}}"
    else
      return nil
    end
  end

  killed = match[:killed]
  bounty = match[:bounty].to_i
  killer = match[:killer]

  return KillMessage.new(line,killed: killed,bounty: bounty,killer: killer)
end
parse_player(line,type_name:,match: :default) click to toggle source

@example Default Format

'X Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 199
def parse_player(line,type_name:,match: :default)
  if match.nil?
    if @strict
      raise ParseError,"invalid #{type_name} message{#{line}}"
    else
      return nil
    end
  elsif match == :default
    # Use type_name of :player (not passed in param) for regex_cache.
    match = match_player(line,type_name: :player)
  end

  name = Util.u_lstrip(match[:name])
  message = match[:message]

  if name.empty? || name.length > MAX_NAMELEN
    if @strict
      raise ParseError,"invalid player name for #{type_name} message{#{line}}"
    else
      return nil
    end
  end

  return PlayerMessage.new(line,type: :unknown,name: name,message: message)
end
parse_private(line) click to toggle source

@example Format

'P Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 227
def parse_private(line)
  player = parse_player(line,type_name: :private)

  return nil if player.nil?

  return PrivateMessage.new(line,name: player.name,message: player.message)
end
parse_pub(line,match:) click to toggle source

@example Format

'  Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 237
def parse_pub(line,match:)
  player = parse_player(line,type_name: :pub,match: match)

  return nil if player.nil?

  cmd = Util.u_strip(player.message).downcase

  if cmd.start_with?('?find')
    store_command(:pub,:?find) # See: match_q_find?()
  end

  return PubMessage.new(line,name: player.name,message: player.message)
end
parse_q_find(line,match:) click to toggle source

@example Format

'  Not online, last seen more than 10 days ago'
'  Not online, last seen 9 days ago'
'  Not online, last seen 18 hours ago'
'  Not online, last seen 0 hours ago'
'  Name - Public 0'
'  TWCore - (Private arena)'
'  Name is in SSCJ Devastation'
'  Name is in SSCC Metal Gear CTF'
# File lib/ssc.bot/chat_log/message_parser.rb, line 260
def parse_q_find(line,match:)
  if match.nil?
    if @strict
      raise ParseError,"invalid ?find message{#{line}}"
    else
      return nil
    end
  end

  caps = match.named_captures
  q_find = nil

  if (days = caps['days'])
    more = caps.key?('more')
    days = days.to_i

    q_find = QFindMessage.new(line,find_type: :days,more: more,days: days)
  elsif (hours = caps['hours'])
    hours = hours.to_i

    q_find = QFindMessage.new(line,find_type: :hours,hours: hours)
  elsif (player = caps['player'])
    if (arena = caps['arena'])
      private = (arena == '(Private arena)')

      q_find = QFindMessage.new(line,find_type: :arena,player: player,arena: arena,private: private)
    elsif (zone = caps['zone'])
      q_find = QFindMessage.new(line,find_type: :zone,player: player,zone: zone)
    end
  end

  if q_find.nil? && @strict
    raise ParseError,"invalid ?find message{#{line}}"
  end

  return q_find
end
parse_q_log(line,match:) click to toggle source

@example Format

'  Log file open: session.log'
'  Log file closed'
# File lib/ssc.bot/chat_log/message_parser.rb, line 301
def parse_q_log(line,match:)
  if match.nil?
    if @strict
      raise ParseError,"invalid ?log message{#{line}}"
    else
      return nil
    end
  end

  filename = match.named_captures['filename']
  log_type = filename.nil? ? :close : :open

  return QLogMessage.new(line,log_type: log_type,filename: filename)
end
parse_q_namelen(line,match:) click to toggle source

@example Format

'  Message Name Length: 24'
# File lib/ssc.bot/chat_log/message_parser.rb, line 318
def parse_q_namelen(line,match:)
  if match.nil?
    if @strict
      raise ParseError,"invalid ?namelen message{#{line}}"
    else
      return nil
    end
  end

  namelen = match[:namelen].to_i

  if namelen < 1
    if @strict
      raise ParseError,"invalid namelen for ?namelen message{#{line}}"
    else
      return nil
    end
  elsif namelen > MAX_NAMELEN
    warn("namelen{#{namelen}} > max{#{MAX_NAMELEN}} for ?namelen message{#{line}}",uplevel: 0)
  end

  if @autoset_namelen
    @namelen = namelen
  end

  return QNamelenMessage.new(line,namelen: namelen)
end
parse_remote(line,match:) click to toggle source

@example Format

# NOT affected by namelen.
'P :Self.Name:Message'
'P (Name)>Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 350
def parse_remote(line,match:)
  player = parse_player(line,type_name: :remote.private,match: match)

  return nil if player.nil?

  own = (line[2] == ':')
  squad = (player.name[0] == '#')

  return RemoteMessage.new(line,
    own: own,squad: squad,
    name: player.name,message: player.message,
  )
end
parse_team(line) click to toggle source

@example Format

'T Name> Message'
# File lib/ssc.bot/chat_log/message_parser.rb, line 366
def parse_team(line)
  player = parse_player(line,type_name: :team)

  return nil if player.nil?

  return TeamMessage.new(line,name: player.name,message: player.message)
end
reset_namelen() click to toggle source
# File lib/ssc.bot/chat_log/message_parser.rb, line 378
def reset_namelen
  @namelen = nil
end
store_command(type,name) click to toggle source
# File lib/ssc.bot/chat_log/message_parser.rb, line 382
def store_command(type,name)
  type_hash = @commands[type]

  if type_hash.nil?
    type_hash = {}
    @commands[type] = type_hash
  end

  type_hash[name] = @messages.length # Index of when command was found/stored
end