481 lines
17 KiB
Ruby
481 lines
17 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DiscordInteractions
|
|
class YiffyAPI
|
|
attr_reader :raw
|
|
|
|
GREEN_TICK = APIKey::WebhookMethods::GREEN_TICK
|
|
RED_TICK = APIKey::WebhookMethods::RED_TICK
|
|
|
|
def initialize(raw)
|
|
@raw = raw
|
|
end
|
|
|
|
def [](key)
|
|
@raw[key.to_s]
|
|
end
|
|
|
|
# rubocop:disable Metrics/BlockNesting
|
|
# noinspection RubyCaseWithoutElseBlockInspection
|
|
def handle
|
|
return { type: DiscordConstants::InteractionResponseTypes::PONG } if raw["type"] == DiscordConstants::InteractionTypes::PING
|
|
|
|
if raw["guild_id"].nil?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "My commands cannot be used in Direct Messages.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
CurrentUser.user = CurrentUser.user = APIUser.find_or_create_by(id: raw["member"]["user"]["id"])
|
|
CurrentUser.ip_addr = "0.0.0.0"
|
|
|
|
case raw["type"]
|
|
when DiscordConstants::InteractionTypes::APPLICATION_COMMAND
|
|
options = DiscordInteractions::InteractionOptions.new(raw["data"]["options"], raw["data"]["resolved"])
|
|
sub = options.get_subcommand
|
|
case raw["data"]["name"]
|
|
when "apikey"
|
|
keys = APIKey.for_owner(raw["member"]["user"]["id"])
|
|
case sub[0]
|
|
when "create"
|
|
if keys.length >= 3
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "You already have the maximum amount of api keys. Contact a developer if you believe an exception should be made.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::MODAL,
|
|
data: {
|
|
custom_id: "apikey-create",
|
|
components: [
|
|
{
|
|
type: DiscordConstants::ComponentTypes::ACTION_ROW,
|
|
components: [
|
|
{
|
|
custom_id: "apikey-create.name",
|
|
placeholder: "My Awesome Application",
|
|
min_length: 3,
|
|
max_length: 50,
|
|
label: "Name",
|
|
style: DiscordConstants::TextInputStyles::SHORT,
|
|
type: DiscordConstants::ComponentTypes::TEXT_INPUT,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: DiscordConstants::ComponentTypes::ACTION_ROW,
|
|
components: [
|
|
{
|
|
custom_id: "apikey-create.usage",
|
|
placeholder: "Showing porn to my friends",
|
|
min_length: 5,
|
|
max_length: 400,
|
|
label: "Usage",
|
|
style: DiscordConstants::TextInputStyles::PARAGRAPH,
|
|
type: DiscordConstants::ComponentTypes::TEXT_INPUT,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
title: "Create API Key",
|
|
},
|
|
}
|
|
when "delete"
|
|
key = keys.find { |k| k.encoded == options.get_string("key", required: true) }
|
|
if key.nil? || key.owner_id.to_s != raw["member"]["user"]["id"]
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Invalid key specified.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
if key.disabled?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "This key has been disabled by a developer. To have this key deleted or removed, concat a developer.\n\nDisable Reason: **#{key.disabled_reason || '(None)'}**",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Are you sure you want to delete the key **#{key.application_name}**? This action cannot be undone.",
|
|
components: [
|
|
{
|
|
type: DiscordConstants::ComponentTypes::ACTION_ROW,
|
|
components: [
|
|
{
|
|
custom_id: "apikey-delete-yes.#{key.encoded}.#{raw['member']['user']['id']}",
|
|
label: "Yes",
|
|
style: DiscordConstants::ButtonStyles::SUCCESS,
|
|
type: DiscordConstants::ComponentTypes::BUTTON,
|
|
},
|
|
{
|
|
custom_id: "apikey-delete-no.#{raw['member']['user']['id']}",
|
|
label: "No",
|
|
style: DiscordConstants::ButtonStyles::DANGER,
|
|
type: DiscordConstants::ComponentTypes::BUTTON,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
**common,
|
|
},
|
|
}
|
|
|
|
when "list"
|
|
if keys.empty?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "You do not have any API keys.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
text = []
|
|
|
|
keys.each_with_index do |apikey, index|
|
|
text.push(
|
|
<<~TEXT,
|
|
#{index + 1}.)
|
|
- Key: ||#{apikey.key}||
|
|
- Application: `#{apikey.application_name}`
|
|
- Usage: `#{apikey.usage || 'None Specified'}`
|
|
- Active: #{apikey.active? ? GREEN_TICK : RED_TICK}
|
|
- Disabled: #{apikey.disabled? ? "#{GREEN_TICK} (Reason: #{apikey.disabled_reason || 'None Specified'})" : RED_TICK}
|
|
- Unlimited: #{apikey.unlimited? ? GREEN_TICK : RED_TICK}
|
|
- Services: #{apikey.access_string}
|
|
TEXT
|
|
)
|
|
end
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "We found the following api keys:\n\n#{text.join("\n\n")}",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
when "apidev"
|
|
unless Websites.config.yiffyapi_administrators.include?(raw["member"]["user"]["id"])
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "You are not allowed to use that.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
case sub[0]
|
|
when "list"
|
|
user_id = options.get_user("user", required: true)["id"]
|
|
keys = APIKey.for_owner(user_id)
|
|
|
|
if keys.empty?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "That user does not have any API keys.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
text = []
|
|
|
|
keys.each_with_index do |apikey, index|
|
|
text.push(
|
|
<<~TEXT,
|
|
#{index + 1}.)
|
|
- Key: ||#{apikey.key}||
|
|
- Application: `#{apikey.application_name}`
|
|
- Usage: `#{apikey.usage || 'None Specified'}`
|
|
- Active: #{apikey.active? ? GREEN_TICK : RED_TICK}
|
|
- Disabled: #{apikey.disabled? ? "#{GREEN_TICK} (Reason: #{apikey.disabled_reason || 'None Specified'})" : RED_TICK}
|
|
- Unlimited: #{apikey.unlimited? ? GREEN_TICK : RED_TICK}
|
|
- Services: #{apikey.access_string}
|
|
TEXT
|
|
)
|
|
end
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "We found the following api keys for <@#{user_id}>:\n\n#{text.join("\n\n")}",
|
|
**common,
|
|
},
|
|
}
|
|
|
|
when "disable"
|
|
key = options.get_string("key", required: true)
|
|
reason = options.get_string("reason") || "None Provided"
|
|
apikey = APIKey.find_by(key: key)
|
|
if apikey.nil?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "I couldn't find that api key.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
if apikey.disabled?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "That api key is already disabled.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
apikey.update(disabled: true, disabled_reason: reason)
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Api key successfully disabled.",
|
|
**common,
|
|
},
|
|
}
|
|
|
|
when "enable"
|
|
key = options.get_string("key", required: true)
|
|
apikey = APIKey.find_by(key: key)
|
|
if apikey.nil?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "I couldn't find that api key.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
unless apikey.disabled?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "That api key is not disabled.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
apikey.update(disabled: false, disabled_reason: nil)
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Api key successfully enabled.",
|
|
**common,
|
|
},
|
|
}
|
|
when "stats"
|
|
key = options.get_string("key")
|
|
ip = options.get_string("ip")
|
|
if key
|
|
apikey = APIKey.find_by(key: key)
|
|
if apikey.nil?
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "I couldn't find that api key.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
end
|
|
|
|
stats = APIKey.stats(ip: ip, key: apikey)
|
|
if key.present?
|
|
stats = flatten_hash(stats[:key])
|
|
text = "Stats for **#{apikey.application_name}** (||#{apikey.key}||)\n\n"
|
|
elsif ip.present?
|
|
stats = flatten_hash(stats[:ip])
|
|
text = "Stats for **#{ip}**\n\n"
|
|
else
|
|
stats = flatten_hash(stats[:global])
|
|
text = "Global stats\n\n"
|
|
end
|
|
|
|
stats.each do |name, value|
|
|
text += "**#{name}**: #{ActiveSupport::NumberHelper.number_to_delimited(value)}\n"
|
|
end
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: text,
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
end
|
|
when DiscordConstants::InteractionTypes::MESSAGE_COMPONENT
|
|
action, *other, user = raw["data"]["custom_id"].split(".")
|
|
if user != raw["member"]["user"]["id"]
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "That is not yours to play with.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
case action
|
|
when "apikey-delete-yes"
|
|
key = APIKey.for_owner(user).find { |k| k.encoded == other[0] }
|
|
if key.nil? || key.owner_id.to_s != user
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Invalid key specified.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
key.destroy
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::UPDATE_MESSAGE,
|
|
data: {
|
|
content: "Key deleted.",
|
|
components: [],
|
|
**common,
|
|
},
|
|
}
|
|
when "apikey-delete-no"
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::UPDATE_MESSAGE,
|
|
data: {
|
|
content: "Cancelled.",
|
|
components: [],
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
when DiscordConstants::InteractionTypes::APPLICATION_COMMAND_AUTOCOMPLETE
|
|
options = DiscordInteractions::InteractionOptions.new(raw["data"]["options"], raw["data"]["resolved"])
|
|
sub, = options.get_subcommand
|
|
opt = options.get_focused_option(required: true)
|
|
case raw["data"]["name"]
|
|
when "apikey"
|
|
case sub
|
|
when "delete"
|
|
keys = APIKey.for_owner(raw["member"]["user"]["id"])
|
|
value = opt["value"]
|
|
keys = keys.filter { |k| k.application_name.start_with?(value) }
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
|
|
data: {
|
|
choices: keys.map do |k|
|
|
{
|
|
name: k.application_name,
|
|
value: k.encoded,
|
|
}
|
|
end,
|
|
},
|
|
}
|
|
end
|
|
end
|
|
when DiscordConstants::InteractionTypes::MODAL_SUBMIT
|
|
case raw["data"]["custom_id"]
|
|
when "apikey-create"
|
|
name = raw["data"]["components"][0]["components"][0]["value"]
|
|
usage = raw["data"]["components"][1]["components"][0]["value"]
|
|
if name.length < 3 || name.length > 50
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Name must be between 3 and 50 characters.",
|
|
components: [],
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
if usage.length < 5 || usage.length > 400
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Usage must be between 5 and 400 characters.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
key = APIKey.create(
|
|
owner_id: raw["member"]["user"]["id"],
|
|
application_name: name,
|
|
usage: usage,
|
|
)
|
|
|
|
if key.errors.present?
|
|
Rails.logger.error("Failed to create apikey for #{raw['member']['user']['id']}: #{key.errors.full_messages.join(', ')}")
|
|
return {
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "An error occurred while creating your API key.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
|
|
{
|
|
type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE,
|
|
data: {
|
|
content: "Your API key: `#{key.key}`. Provide this in the `Authorization` header. You must still provide a unique user agent. If you have any issues, contact a developer.",
|
|
**common,
|
|
},
|
|
}
|
|
end
|
|
end
|
|
end
|
|
# rubocop:enable Metrics/BlockNesting
|
|
|
|
private
|
|
|
|
def flatten_hash(hash)
|
|
hash.each_with_object({}) do |(k, v), h|
|
|
if v.is_a?(Hash)
|
|
flatten_hash(v).map do |h_k, h_v|
|
|
h[:"#{k}.#{h_k}"] = h_v
|
|
end
|
|
else
|
|
h[k] = v
|
|
end
|
|
end
|
|
end
|
|
|
|
def common
|
|
{
|
|
flags: DiscordConstants::MessageFlags::EPHEMERAL,
|
|
allowed_mentions: {
|
|
users: [],
|
|
roles: [],
|
|
everyone: [],
|
|
replied_user: false,
|
|
},
|
|
}
|
|
end
|
|
end
|
|
end
|