Wednesday, July 15, 2026 4:57:28 AM
> broadcast.rb
# frozen_string_literal: true

module ESM
  module Command
    module Server
      class Broadcast < ApplicationCommand
        #################################
        #
        # Arguments (required first, then order matters)
        #

        # Required: All variants require a message
        argument :message, required: true, preserve: true

        # Optional: Omitting defaults to "preview"
        argument(
          :broadcast_to,
          display_name: :to,
          checked_against: ESM::Regex::BROADCAST,
          modifier: lambda do |content|
            return if content.blank?
            return content if %w[all preview].include?(content)

            # If we start with a community ID, just accept the match
            return content if content.match("^#{ESM::Regex::COMMUNITY_ID_OPTIONAL.source}_")

            # Add the community ID to the front of the match
            "#{current_community.community_id}_#{content}"
          end
        )

        #
        # Configuration
        #

        change_attribute :allowlist_enabled, default: true

        command_namespace :server, :admin
        command_type :admin

        limit_to :text

        #################################

        def on_execute
          check_for_message_length!

          # Send just the preview
          return reply(broadcast_embed) if arguments.broadcast_to == "preview" || arguments.broadcast_to.blank?

          # Preload the servers
          load_servers

          # Send a preview of the embed
          reply(broadcast_embed(server_ids: @server_id_sentence))
          reply("`---------------------------------`")

          embed =
            ESM::Embed.build do |e|
              e.title = I18n.t("commands.broadcast.confirmation_embed.title")
              e.description = I18n.t("commands.broadcast.confirmation_embed.description", server_ids: @server_id_sentence)

              e.add_field(name: I18n.t("commands.broadcast.confirmation_embed.field_name"), value: I18n.t("commands.broadcast.confirmation_embed.field_value"))
            end

          # Send the confirmation request
          reply(embed)

          response = ESM.discord_bot.await_response(current_user, expected: [I18n.t("yes"), I18n.t("no")], timeout: 120)
          if response.nil? || response.downcase == I18n.t("no").downcase
            return reply(I18n.t("commands.broadcast.cancellation_reply"))
          end

          # Get all of the users to broadcast to
          users = load_users
          users.each { |user| ESM.discord_bot.deliver(broadcast_embed(server_ids: @server_id_sentence), to: user.discord_id) }

          # Send the success message back
          reply(
            ESM::Embed.build(
              :success,
              description: I18n.t("commands.broadcast.success_message", user: current_user.mention)
            )
          )
        end

        private

        def broadcast_embed(server_ids: nil)
          # For the preview
          if server_ids.blank?
            server = current_community.servers.first
            server_ids = server&.server_id || "<server_id_here>"
          end

          ESM::Embed.build do |e|
            e.title = I18n.t("commands.broadcast.broadcast_embed.title", community_name: current_community.community_name, server_ids: server_ids)
            e.description = arguments.message
            e.color = :orange
            e.footer = I18n.t("commands.broadcast.broadcast_embed.footer")
          end
        end

        def load_servers
          @servers =
            if arguments.broadcast_to == "all"
              current_community.servers
            else
              # Find the server, but check existence and if the server belongs to this community
              server = ESM::Server.find_by_server_id(arguments.broadcast_to)

              raise_invalid_server_id! if server.nil?
              raise_no_server_access! if server.community_id != current_community.id

              [server]
            end

          # For the broadcast message
          @server_id_sentence = @servers.map { |server| "`#{server.server_id}`" }.to_sentence
        end

        def load_users
          users =
            @servers.map do |server|
              denied_user_ids = ESM::UserNotificationPreference.where(server_id: server.id, custom: false).pluck(:user_id)
              denied_steam_uids = ESM::User.where(id: denied_user_ids).pluck(:steam_uid).reject(&:nil?)

              # Get every user that has used a command for this server, but has not explicitly denied custom
              # This code will also inherently find users who have set the preference to true since that creates a cooldown
              # This query is complex to avoid querying and loading a lot of data.
              user_data = ESM::Cooldown
                .where(server_id: server.id)
                .where.not(user_id: denied_user_ids, steam_uid: denied_steam_uids)
                .uniq { |cooldown| [cooldown.user_id, cooldown.steam_uid] }
                .map { |cooldown| [cooldown.user_id, cooldown.steam_uid] }

              user_ids = user_data.map(&:first).reject(&:nil?).uniq
              steam_uids = user_data.map(&:second).reject(&:nil?).uniq

              ESM::User.where(id: user_ids).or(ESM::User.where(steam_uid: steam_uids))
            end

          # Flatten and make sure we are only sending to each user once
          users.flatten.reject(&:nil?).uniq(&:discord_id)
        end

        def check_for_message_length!
          raise_error!(:message_length, user: current_user.mention) if arguments.message.size > 2000
        end

        def raise_no_server_access!
          raise_error!(:no_server_access, user: current_user.mention, community_id: current_community.community_id)
        end

        def raise_invalid_server_id!
          raise_error!(
            :invalid_server_id,
            path_prefix: "command_errors",
            user: current_user.mention,
            provided_server_id: arguments.broadcast_to
          )
        end
      end
    end
  end
end
All opinions represented herein are my own
- © 2024 - 2026 itsthedevman
- build 43aa0d7