Xzanth/pugbot

View on GitHub
lib/pugbot/handlers.rb

Summary

Maintainability
B
6 hrs
Test Coverage
module PugBot
  # The plugin to be imported into a cinch bot instance that actually interprets
  # the users input, tracks players and controls nearly all running of the pug
  # bot itself.
  class BotPlugin
    # Set up variables, called when the bot first connects to irc. Start an
    # array of names to not pmsg back.
    # @return [void]
    def initialize(*args)
      super
      @names = ["Q", bot.nick]
      @queue_list = QueueList.new(self)
      @bot.pug_plugin = self
    end

    def setup(*)
      @channel = Channel(config[:channel])
      @xtremecount = 0
    end

    # Don't allow anyone else to change the channel topic, warn them with
    # a notice.
    # @return [void]
    def topic_changed(m)
      return if m.user.nick == bot.nick
      m.user.notice EDIT_TOPIC
    end

    # Update the channel topic to the list of the queues in the queue_list
    # @return [void]
    def update_topic
      topic = @queue_list.queues.map.with_index do |queue, index|
        "{ Queue #{index + 1}: #{queue} }"
      end
      full_topic = topic.join(" - ")
      unless @topic.nil?
        full_topic << " - " unless full_topic.empty?
        full_topic << @topic
      end
      @channel.topic = full_topic
    end

    # Inform anyone we haven't informed previously that we are a bot when they
    # private message us.
    # @return [void]
    def private_message(m)
      return if m.user.nil?
      return send m.message if m.user.authname == "Xzanth"
      nick = m.user.nick
      m.reply I_AM_BOT and @names.push(nick) unless @names.include?(nick)
    end

    # When a user joins a channel, welcome them and if they are being tracked
    # cancel their countdown.
    # @return [void]
    def joined_channel(m)
      user = m.user
      user.rejoined if user.track

      user.notice format(WELCOME, m.channel)
    end

    # When a tracked user leaves the channel start a timer before they are
    # removed from queues.
    # @return [void]
    def left_channel(_m, user)
      return unless user.track
      user.timer = timer_user_leave(user)
      send format(DISCONNECTED, user.nick)
    end

    ############################################################################
    # @!group !mumble

    # Inform channel of the mumble info.
    # @return [void]
    def mumble(m)
      m.reply MUMBLE_INFO
    end

    ############################################################################
    # @!group !ts3

    # Inform channel of the ts3 info.
    # @return [void]
    def ts3(m)
      m.reply TS3_INFO
    end

    ############################################################################
    # @!group !version

    # Inform channel of the pugbot version.
    # @return [void]
    def version(m)
      m.reply format(VERSION_REPLY, VERSION)
    end

    ############################################################################
    # @!group !topic

    # Inform channel of the pugbot version.
    # @return [void]
    def topic(m, text)
      return user.notice ACCESS_DENIED unless m.channel.opped?(user)
      @topic = text
      update_topic
    end

    ############################################################################
    # @!group !status

    # Parse a status command, give status about a queue specified by arg. Give
    # info about the default queue if no arg is given.
    # @param [String] arg Describes the queue to give info about
    # @return [void]
    # @see Queue.print_long
    def status(m, arg)
      queue = @queue_list.find_queue_by_arg(arg)
      user = m.user
      if arg == "all"
        return @queue_list.queues.each { |q| user.notice q.print_long }
      end
      return user.notice QUEUE_NOT_FOUND if queue.nil?
      user.notice queue.print_long
      unless queue.games.empty?
        queue.games.each.with_index do |g, i|
          i += 1
          user.notice "Game #{i} - #{g}"
        end
      end
    end

    ############################################################################
    # @!group !start

    # Parse a start command, which can only be run by operators. Check if
    # a queue already exists with that name then if the number is acceptable
    # issue a call to start a new queue. Which will default to a max player
    # number of 10 if num is not given.
    # @param [String] name The name the queue should have
    # @param [String] num A string of the number of max players for the queue
    # @return [void]
    # @see QueueList.new_queue
    def start(m, name, num)
      num = num.to_i
      user = m.user
      return user.notice ACCESS_DENIED unless m.channel.opped?(user)
      return user.notice NAME_TAKEN if @queue_list.find_queue_by_name(name)
      return user.notice ODD_NUMBER if num.odd?
      return user.notice TOO_LARGE if num > 32
      @queue_list.new_queue(name, num)
      update_topic
    end

    ############################################################################
    # @!group !add

    # Parse an add command, adding to the default if no argument is supplied
    # otherwise parsing the arg variable and adding to specific queues.
    # @param [String] arg Describes the queue to add the user to
    # @return [void]
    # @see #try_join
    # @see #try_join_all
    def add(m, arg)
      queue = @queue_list.find_queue_by_arg(arg)
      user = m.user
      if /.+otenet.gr/ =~ user.host
        @xtremecount += 1
        Timer(7, shots: 1) { @xtremecount -= 1 }
        @channel.kick(user, "shut the fuck up xtreme") if @xtremecount >= 3
      end
      return try_join_all(user) if arg == "all"
      return user.notice QUEUE_NOT_FOUND if queue.nil?
      return user.notice ALREADY_IN_QUEUE if queue.listed_either?(user)
      try_join(user, queue)
      update_topic
    end

    # Try joining a queue, fail if already playing. If have just finished only
    # add to the wait queue otherwise add to normal queue.
    # @param [Cinch::User] user The user that is trying to join
    # @param [Queue] queue The queue they are trying to join
    # @return [void]
    # @see add
    # @see Queue.add
    # @see Queue.add_wait
    def try_join(user, queue)
      return user.notice YOU_ARE_PLAYING if playing?(user)
      if user.status == :finished
        user.notice FINISHED_IN_QUEUE
        queue.add_wait(user)
      else
        queue.add(user)
      end
    end

    # Try joining all queues the user is not already queued for. Fail if already
    # playing and add to the wait queue if just finished otherwise add to normal
    # queue.
    # @param [Cinch::User] user The user trying to join all queues
    # @return [void]
    # @see add
    # @see Queue.add
    # @see Queue.add_wait
    def try_join_all(user)
      queues = @queue_list.queues.select { |q| !q.listed_either?(user) }
      return user.notice YOU_ARE_PLAYING if playing?(user)
      return user.notice ALREADY_IN_ALL_QUEUES if queues.empty?
      if user.status == :finished
        user.notice FINISHED_IN_QUEUE
        queues.each { |q| q.add_wait(user) }
      else
        queues.each { |q| q.add(user) }
      end
      update_topic
    end

    ############################################################################
    # @!group !del

    # Parse a del command either trying to remove from all queues with no
    # arguments or just removing from a specific queue according to arg.
    # @param [String] arg Describes the queue to remove the user from
    # @return [void]
    # @see Queue.remove
    def del(m, arg)
      queue = @queue_list.find_queue_by_arg(arg)
      user = m.user
      return user.notice YOU_ARE_PLAYING if playing?(user)
      return del_from_all(m, user) if arg.nil? || arg == "all"
      return user.notice QUEUE_NOT_FOUND if queue.nil?
      return user.notice YOU_NOT_IN_QUEUE unless queue.listed_either?(user)
      m.reply format(LEFT, user.nick, queue.name)
      queue.remove(user)
      update_topic
    end

    # Delete a user from all the queues they are queued in, noticing them if
    # they are not in any, otherwise announcing that they have left all queues.
    # @param [Cinch::User] user The user that wishes to remove themself
    # @return [void]
    # @see #del
    # @see Queue.remove
    def del_from_all(m, user)
      queues = @queue_list.queues.select { |q| q.listed_either?(user) }
      return user.notice YOU_NOT_IN_ANY_QUEUES if queues.empty?
      queues.each { |q| q.remove(user) }
      m.reply format(LEFT_ALL, user.nick)
      update_topic
    end

    ############################################################################
    # @!group !remove

    # Parse a remove command, only allowing ops to execute and removing the user
    # from all queues if no argument is specified, otherwise parsing the
    # argument and removing the user from the specified queue.
    # @param [String] name The nick of the user to be removed
    # @param [String] arg Describes the queue to remove the user from
    # @return [void]
    # @see #remove_from_all
    # @see #remove_from_queue
    def remove(m, name, arg)
      queue = @queue_list.find_queue_by_arg(arg)
      user = User(name)
      return m.user.notice ACCESS_DENIED unless m.channel.opped?(m.user)
      return m.user.notice USERS_NOT_FOUND if user.nil?
      return remove_from_all(m, user) if arg.nil? || arg == "all"
      return m.user.notice QUEUE_NOT_FOUND if queue.nil?
      remove_from_queue(m, user, queue)
      update_topic
    end

    # Remove a user from a specified queue and reply to the channel, or notice
    # the remover and return :not_in_queue if the user is not in the queue.
    # @param [Cinch::User] user The user to be removed
    # @param [Queue] queue The queue to remove the user from
    # @return [void]
    # @see #remove
    # @see Queue.remove
    def remove_from_queue(m, user, queue)
      unless queue.listed_either?(user)
        m.user.notice format(NOT_IN_QUEUE, user.nick)
      end
      queue.remove(user)
      m.reply format(REMOVED, user.nick, queue.name, m.user.nick)
    end

    # Remove a user from all queues, noticing if they are not in any queues and
    # alerting the user if so.
    # @param [Cinch::User] user The user to be removed
    # @return [void]
    # @see #remove
    # @see Queue.remove
    def remove_from_all(m, user)
      queues = @queue_list.queues.select { |q| q.listed_either?(user) }
      return user.notice YOU_NOT_IN_ANY_QUEUES if queues.empty?
      queues.each { |q| q.remove(user) }
      m.reply format(REMOVED, user.nick, "all queues", m.user.nick)
      update_topic
    end

    ############################################################################
    # @!group !end

    # End a queue, a command for operators that deletes a queue from the
    # queuelist and allows all current players to immeditaely join new queues.
    # @param [String] arg The queue to delete.
    # @return [void]
    # @see QueueList.remove
    def end(m, arg)
      queue = @queue_list.find_queue_by_arg(arg)
      return m.user.notice ACCESS_DENIED unless m.channel.opped?(m.user)
      return m.user.notice QUEUE_NOT_FOUND if queue.nil?
      m.reply format(ENDED, queue.name, m.user)
      queue.games.each do |game|
        game.users.each { |user| user.status = :standby }
      end
      @queue_list.remove_queue(queue)
      update_topic
    end

    ############################################################################
    # @!group !finish

    # Finish the game currently being played.
    # @param [String] arg The queue to finish the games in
    # @param [String] arg2 The number game to be finished
    # @return [void]
    # @see Queue.find_game_by_arg
    # @see Game.finish
    def finish(m, arg1, arg2)
      user = m.user
      if arg1.nil?
        game = @queue_list.find_game_playing(m.user)
        return user.notice FINISH_NOT_INGAME if game.nil?
      else
        queue = @queue_list.find_queue_by_arg(arg1)
        return user.notice QUEUE_NOT_FOUND if queue.nil?
        return user.notice NO_GAME if queue.games.empty?
        game = queue.find_game_by_arg(arg2)
        return user.notice FINISH_AMBIGUOUS_GAME if game.nil?
      end
      game.finish
      update_topic
    end

    ############################################################################
    # @!group !sub

    # Sub a user assigned to a game with one not assigned to a game, check that
    # the input is correct and then run the {#swap} function.
    # @param [String] user The nick of the user in a game
    # @param [String] sub The nick of the user not in a game
    # @return [void]
    # @see #swap
    def sub(m, user, sub)
      user_u = User(user)
      sub_u = User(sub)
      user_game = @queue_list.find_game_playing(user_u)
      sub_game = @queue_list.find_game_playing(sub_u)
      return m.user.notice USERS_NOT_FOUND if user_u.nil? || sub_u.nil?
      return m.user.notice format(NOT_PLAYING, user) if user_game.nil?
      return m.user.notice format(ALREADY_PLAYING, sub) unless sub_game.nil?
      swap(m, user_u, sub_u, user_game)
      update_topic
    end

    # Swap too users, one playing and one not.
    # @param [Cinch::User] user1 The user currently playing a game
    # @param [Cinch::User] user2 The user currently not playing a game
    # @return [void]
    # @see #sub
    # @see Queue.sub
    # @see Queue.remove
    def swap(m, user1, user2, user_game)
      user2.status = :ingame
      user1.status = :standby
      user_game.sub(user1, user2)
      m.reply format(SUBBED, user1.nick, user2.nick, m.user.nick)
    end

    ############################################################################
    # @!group !shutdown

    # Quit the program immediately, can only be performed by operator.
    # @return [void]
    def shutdown(m)
      return m.user.notice ACCESS_DENIED unless m.channel.opped?(m.user)
      m.reply format(KILLED, m.user.nick)
      abort format(KILLED, m.user.nick)
    end

    ############################################################################
    # @!group !restart

    # Effectively restart the bot, clearing the queue list and resetting all
    # users to an untracked state.
    # @return [void]
    def restart(m)
      return m.user.notice ACCESS_DENIED unless m.channel.opped?(m.user)
      m.reply format(RESTARTED, m.user.nick)
      @queue_list = QueueList.new(self)
      @channel.users.keys.each do |u|
        u.track = false
        u.status = :standby
      end
      update_topic
    end

    ############################################################################
    # @!group !save

    # Save the entire list of queues to a file so in the case of a crash/restart
    # everything does not have to be remade.
    # @param [String] file The filename to save to
    # @return [void]
    def save(m, file)
      return m.user.notice ACCESS_DENIED unless m.channel.opped?(m.user)
      File.open(File.expand_path("#{file}.json"), "w") do |f|
        f.write(@queue_list.to_json)
      end
      m.reply format(SAVED, m.user.nick)
    end

    ############################################################################
    # @!group !load

    # Load a file that has been saved earlier with !save as the current queue
    # list. If supplied with no arguments load default file.
    # @return [void]
    def load(m, file)
      file =  config[:default_file] if file.nil?
      return m.user.notice ACCESS_DENIED unless m.channel.opped?(m.user)
      begin
        f = File.read(File.expand_path("#{file}.json"))
      rescue Errno::ENOENT
        return m.reply NO_SAVE_FILE
      end
      queue_list = JSON.parse(f)
      @queue_list = QueueList.new(self)
      @queue_list.from_hash(queue_list)
      update_topic
      m.reply format(LOADED, m.user.nick)
    end

    ############################################################################
    # @!group HelperFunctions

    # Is a user currently playing in a game?
    # @param [String] user The nick of the user to test
    # @return [Boolean] Whether or not they are playing
    def playing?(user)
      user.status == :ingame
    end

    ############################################################################
  end
end