Compare commits

..

5 Commits

Author SHA1 Message Date
2b4b5d39ed User (verification date guesser): Reintroduce changes that I lost 2025-06-24 00:07:43 -04:00
0b05c9a8ba Remove unused 2025-06-24 00:07:24 -04:00
8735d6bfa8 Make user slash command work 2025-06-23 23:59:08 -04:00
ba4c807644 QueryParams guard 2025-06-23 23:59:01 -04:00
c71c9229e8 Refactor StrafesNET module (WIP, more to come) 2025-06-23 23:59:01 -04:00
9 changed files with 90 additions and 250 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
discordia.log discordia.log
gateway.json gateway.json
APIKeys.lua API_Keys.lua
Token.lua Token.lua
deps deps
sensdb.lua sensdb.lua

3
src/Modules/APIKeys.lua Normal file
View File

@ -0,0 +1,3 @@
return {
StrafesNET = "strafe_3fdb3338a3b05a64566f20df51a4e343"
}

View File

@ -1,13 +1,6 @@
local Http = require('coro-http') local Http = require('coro-http')
local HTTPRequest = Http.request local HTTPRequest = Http.request
local Timer = require("timer")
local Sleep = Timer.sleep
local function Wait(n)
return Sleep(n * 1000)
end
local json = require('json') local json = require('json')
local METHODS = { local METHODS = {
@ -57,7 +50,7 @@ local function NormalizeHeaders(Response)
end end
end end
local function Request(Method, Url, Params, RequestHeaders, RequestBody, Callback, MaxRetries) local function Request(Method, Url, Params, RequestHeaders, RequestBody, Callback)
if not METHODS[Method] then if not METHODS[Method] then
error("[HTTP] Method " .. Method .. " is not supported.") error("[HTTP] Method " .. Method .. " is not supported.")
end end
@ -72,49 +65,21 @@ local function Request(Method, Url, Params, RequestHeaders, RequestBody, Callbac
local QueryString = QueryParams(Params) -- at worse (I think), this is an empty string (which cannot mess up the request) local QueryString = QueryParams(Params) -- at worse (I think), this is an empty string (which cannot mess up the request)
local FormattedHeaders = CreateHeaders(RequestHeaders) -- at worse, this will just be an empty table (which cannot mess up the request) local FormattedHeaders = CreateHeaders(RequestHeaders) -- At worse, this will just be an empty table (which cannot mess up the request)
local RequestUrl = Url .. QueryString local RequestUrl = Url .. QueryString
print(RequestUrl) print(RequestUrl)
MaxRetries = MaxRetries or 10
local function DoRequest()
local Attempt = 0
local Delay = 2
while Attempt <= MaxRetries do
local Headers, Body = HTTPRequest(Method, RequestUrl, FormattedHeaders, RequestBody)
NormalizeHeaders(Headers)
print("Attempt:", Attempt + 1, "Status code:", Headers.code)
-- we will assume <400 = success i guess
if Headers.code and Headers.code < 400 then
return Headers, TryDecodeJson(Body)
end
Attempt = Attempt + 1
if Attempt > MaxRetries then
break
end
print("Request failed, retrying in " .. Delay .. " seconds...")
Wait(Delay)
Delay = Delay * 2 -- exponential back-off
end
local Headers, Body = HTTPRequest(Method, RequestUrl, FormattedHeaders, RequestBody)
NormalizeHeaders(Headers)
return Headers, TryDecodeJson(Body)
end
if Callback and type(Callback) == "function" then if Callback and type(Callback) == "function" then
return coroutine.wrap(function() return coroutine.wrap(function()
local Headers, DecodedBody = DoRequest() local Headers, Body = HTTPRequest(Method, RequestUrl, FormattedHeaders, RequestBody)
Callback(Headers, DecodedBody) NormalizeHeaders(Headers)
Callback(Headers, TryDecodeJson(Body))
end) end)
else else
return DoRequest() local Headers, Body = HTTPRequest(Method, RequestUrl, FormattedHeaders, RequestBody)
NormalizeHeaders(Headers)
return Headers, TryDecodeJson(Body)
end end
end end

View File

@ -2,7 +2,7 @@ local HttpRequest = require("./HttpRequest.lua")
local Request = HttpRequest.Request local Request = HttpRequest.Request
local APIKeys = require("./APIKeys.lua") local APIKeys = require("./APIKeys.lua")
local RequestHeaders = { local Headers = {
["Content-Type"] = "application/json", ["Content-Type"] = "application/json",
["X-API-Key"] = APIKeys.StrafesNET ["X-API-Key"] = APIKeys.StrafesNET
} }
@ -22,12 +22,6 @@ local ROBLOX_THUMBNAIL_URL = 'https://thumbnails.roblox.com/v1/'
local ROBLOX_INVENTORY_API = 'https://inventory.roblox.com/v1/' local ROBLOX_INVENTORY_API = 'https://inventory.roblox.com/v1/'
local ROBLOX_GROUPS_ROLES_URL = 'https://groups.roblox.com/v2/users/%s/groups/roles' local ROBLOX_GROUPS_ROLES_URL = 'https://groups.roblox.com/v2/users/%s/groups/roles'
local GAME_IDS = {
BHOP = 1,
SURF = 2,
-- FLY_TRIALS = 5
}
ROBLOX_THUMBNAIL_SIZES = { ROBLOX_THUMBNAIL_SIZES = {
[48] = '48x48', [48] = '48x48',
[50] = '50x50', [50] = '50x50',
@ -57,9 +51,6 @@ local STRAFESNET_API_ENDPOINTS = {
}, },
TIMES = { TIMES = {
LIST = "time", LIST = "time",
WORLD_RECORD = {
GET = "time/worldrecord"
},
GET = "time/%d" GET = "time/%d"
}, },
USERS = { USERS = {
@ -71,64 +62,34 @@ local STRAFESNET_API_ENDPOINTS = {
} }
} }
local RankConstants = {
Magic1 = 0.7,
Magic2 = 0.22313016
}
local StrafesNET = {} local StrafesNET = {}
StrafesNET.GameIds = GAME_IDS
function StrafesNET.CalculatePoints(Rank, Count)
local ExpMagic2 = math.exp(RankConstants.Magic2)
local Num1 = ExpMagic2 - 1.0
local ExpDenomExp = math.max(-700.0, -RankConstants.Magic2 * Count)
local Denom1 = 1.0 - math.exp(ExpDenomExp)
local ExpRankExp = math.max(-700.0, -RankConstants.Magic2 * Rank)
local ExpRank = math.exp(ExpRankExp)
local Part1 = RankConstants.Magic1 * (Num1 / Denom1) * ExpRank
local Part2 = (1.0 - RankConstants.Magic1) * (1.0 + 2.0 * (Count - Rank)) / (Count * Count)
return Part1 + Part2
end
function StrafesNET.CalculateSkill(Rank, Count)
local Denominator = Count - 1
if Denominator == 0 then
return 0
else
return (Count - Rank) / Denominator
end
end
function StrafesNET.ListMaps(GameId, PageSize, PageNumber) function StrafesNET.ListMaps(GameId, PageSize, PageNumber)
local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.MAPS.LIST local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.MAPS.LIST
local Params = { game_id = GameId, page_size = PageSize or 10, page_number = PageNumber or 1 } local Params = { game_id = GameId, page_size = PageSize or 10, page_number = PageNumber or 1 }
return Request("GET", RequestUrl, Params, RequestHeaders) return Request("GET", RequestUrl, Params, Headers)
end end
function StrafesNET.GetMap(MapId) function StrafesNET.GetMap(MapId)
local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.MAPS.GET:format(MapId) local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.MAPS.GET:format(MapId)
local Params = { id = MapId } local Params = { id = MapId }
return Request("GET", RequestUrl, Params, RequestHeaders) return Request("GET", RequestUrl, Params, Headers)
end end
function StrafesNET.ListRanks(GameId, ModeId, StyleId, SortBy, PageSize, PageNumber) function StrafesNET.ListRanks(GameId, ModeId, StyleId, SortBy, PageSize, PageNumber)
local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.RANKS.LIST local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.RANKS.LIST
local Params = { local Params = {
game_id = GameId, gameId = GameId,
mode_id = ModeId, modeId = ModeId,
style_id = StyleId, styleId = StyleId,
sort_by = SortBy or 1, sort_by = SortBy or 1,
page_size = PageSize or 10, page_size = PageSize or 10,
page_number = PageNumber or 1 page_number = PageNumber or 1
} }
return Request("GET", RequestUrl, Params, RequestHeaders) return Request("GET", RequestUrl, Params, Headers)
end end
function StrafesNET.ListTimes(MapId, GameId, ModeId, StyleId, UserId, SortBy, PageSize, PageNumber) function StrafesNET.ListTimes(UserId, MapId, GameId, ModeId, StyleId, SortBy, PageSize, PageNumber)
local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.TIMES.LIST local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.TIMES.LIST
local Params = { local Params = {
user_id = UserId, user_id = UserId,
@ -136,30 +97,16 @@ function StrafesNET.ListTimes(MapId, GameId, ModeId, StyleId, UserId, SortBy, Pa
game_id = GameId, game_id = GameId,
mode_id = ModeId, mode_id = ModeId,
style_id = StyleId, style_id = StyleId,
sort_by = SortBy or 0, sort_by = SortBy or 1,
page_size = PageSize or 10,
page_number = PageNumber or 1
}
return Request("GET", RequestUrl, Params, RequestHeaders)
end
function StrafesNET.GetWorldRecords(UserId, MapId, GameId, ModeId, StyleId, PageSize, PageNumber)
local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.TIMES.WORLD_RECORD.GET
local Params = {
user_id = UserId,
map_id = MapId,
game_id = GameId,
mode_id = ModeId,
style_id = StyleId,
page_size = PageSize or 10, page_size = PageSize or 10,
page_number = PageNumber or 0 page_number = PageNumber or 0
} }
return Request("GET", RequestUrl, Params, RequestHeaders) return Request("GET", RequestUrl, Params, Headers)
end end
function StrafesNET.GetTime(TimeId) function StrafesNET.GetTime(TimeId)
local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.TIMES.GET:format(TimeId) local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.TIMES.GET:format(TimeId)
return Request("GET", RequestUrl, nil, RequestHeaders) return Request("GET", RequestUrl, nil, Headers)
end end
function StrafesNET.ListUsers(StateId, PageSize, PageNumber) function StrafesNET.ListUsers(StateId, PageSize, PageNumber)
@ -169,12 +116,12 @@ function StrafesNET.ListUsers(StateId, PageSize, PageNumber)
page_size = PageSize or 10, page_size = PageSize or 10,
page_number = PageNumber or 1, page_number = PageNumber or 1,
} }
return Request("GET", RequestUrl, Params, RequestHeaders) return Request("GET", RequestUrl, Params, Headers)
end end
function StrafesNET.GetUser(UserId) function StrafesNET.GetUser(UserId)
local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.USERS.GET:format(UserId) local RequestUrl = STRAFESNET_API_URL .. STRAFESNET_API_ENDPOINTS.USERS.GET:format(UserId)
return Request("GET", RequestUrl, nil, RequestHeaders) return Request("GET", RequestUrl, nil, Headers)
end end
function StrafesNET.GetUserRank(UserId, GameId, ModeId, StyleId) function StrafesNET.GetUserRank(UserId, GameId, ModeId, StyleId)
@ -184,49 +131,7 @@ function StrafesNET.GetUserRank(UserId, GameId, ModeId, StyleId)
mode_id = ModeId, mode_id = ModeId,
style_id = StyleId, style_id = StyleId,
} }
return Request("GET", RequestUrl, Params, RequestHeaders) return Request("GET", RequestUrl, Params, Headers)
end
-- util stuff or something
function StrafesNET.GetMapCompletionCount(MapId, GameId, ModeId, StyleId)
local Headers, Response = StrafesNET.ListTimes(MapId, GameId, ModeId, StyleId)
if Headers.code >= 400 then
return error("HTTP Error while getting map completion count")
end
return Response.pagination.total_items
end
function StrafesNET.GetAllUserTimes(UserId, GameId, ModeId, StyleId)
local Times = {}
local CurrentPage = 1
local Headers, Response = StrafesNET.ListTimes(nil, GameId, ModeId, StyleId, UserId, 0, 100, CurrentPage)
if Headers.code >= 400 then
return error("HTTP error while getting times for something")
end
for TimeIndex, Time in next, Response.data do
Times[Time.id] = Time
end
local TotalPages = Response.pagination.total_pages
while CurrentPage < TotalPages do
CurrentPage = CurrentPage + 1
local _Headers, _Response = StrafesNET.ListTimes(nil, GameId, ModeId, StyleId, UserId, 0, 100, CurrentPage)
if _Headers.code >= 400 then
return error("HTTP error while getting times for something")
end
for _, Time in next, _Response.data do
Times[Time.id] = Time
end
end
return Times
end
function StrafesNET.GetAllMaps()
local Maps = {}
-- TODO
return Maps
end end
function StrafesNET.GetRobloxInfoFromUserId(USER_ID) function StrafesNET.GetRobloxInfoFromUserId(USER_ID)
@ -256,6 +161,42 @@ function StrafesNET.GetRobloxInfoFromDiscordId(DISCORD_ID)
return Request("GET", ROBLOX_API_URL .. "users/" .. body.result.robloxId) return Request("GET", ROBLOX_API_URL .. "users/" .. body.result.robloxId)
end end
function StrafesNET.GetUserFromAny(user, message)
local str = user:match('^["\'](.+)[\'"]$')
local num = user:match('^(%d+)$')
if str then
local roblox_user = StrafesNET.GetRobloxInfoFromUsername(str)
if not roblox_user.id then return 'User not found' end
return roblox_user
elseif num then
local roblox_user = StrafesNET.GetRobloxInfoFromUserId(user)
if not roblox_user.id then return 'Invalid user id' end
return roblox_user
elseif user == 'me' then
local me = message.author
local roblox_user = StrafesNET.GetRobloxInfoFromDiscordId(me.id)
if not roblox_user.id then
return
'You are not registered with the fiveman1 api, use !link with the rbhop bot to link your roblox account'
end
return roblox_user
elseif user:match('<@%d+>') then
local user_id = user:match('<@(%d+)>')
local member = message.guild:getMember(user_id)
local roblox_user = StrafesNET.GetRobloxInfoFromDiscordId(member.id)
if not roblox_user.id then
return
'User is not registered with the fiveman1 api, use !link with the rbhop bot to link your roblox account'
end
return roblox_user
else
local roblox_user = StrafesNET.GetRobloxInfoFromUsername(user)
if not roblox_user.id then return 'User not found' end
return roblox_user
end
end
function StrafesNET.GetUserOnlineStatus(USER_ID) function StrafesNET.GetUserOnlineStatus(USER_ID)
if not USER_ID then return 'empty id' end if not USER_ID then return 'empty id' end

View File

View File

@ -1,7 +1,6 @@
local Discordia = require('discordia') local Discordia = require('discordia')
local json = require('json') local json = require('json')
local HttpRequest = require('../Modules/HttpRequest.lua') local http_request = require('../Modules/http.lua')
local Request = HttpRequest.Request
local SubCommandHandler = require('../Modules/SubCommandHandler.lua') local SubCommandHandler = require('../Modules/SubCommandHandler.lua')
Discordia.extensions() Discordia.extensions()
@ -13,10 +12,8 @@ local MinecraftSubCommandHandler = SubCommandHandler.new()
local MinecraftMainCommand = SlashCommandTools.slashCommand('minecraft', 'Minecraft server related commands') local MinecraftMainCommand = SlashCommandTools.slashCommand('minecraft', 'Minecraft server related commands')
local MinecraftStatusSubCommand = SlashCommandTools.subCommand('status', local MinecraftStatusSubCommand = SlashCommandTools.subCommand('status', 'Get the Minecraft server status according to the preferred IP address set for this server')
'Get the Minecraft server status according to the preferred IP address set for this server') local MinecraftSetIpSubCommand = SlashCommandTools.subCommand('setip', 'Set the preferred Minecraft server IP address for this server')
local MinecraftSetIpSubCommand = SlashCommandTools.subCommand('setip',
'Set the preferred Minecraft server IP address for this server')
local MinecraftSetIpOptions = SlashCommandTools.string('ip', 'The IP address of the server') local MinecraftSetIpOptions = SlashCommandTools.string('ip', 'The IP address of the server')
MinecraftSetIpOptions:setRequired(true) MinecraftSetIpOptions:setRequired(true)
@ -56,49 +53,46 @@ MinecraftSubCommandHandler:AddSubCommand(MinecraftStatusSubCommand.name, functio
return Interaction:reply('There is no data for this Discord server', true) return Interaction:reply('There is no data for this Discord server', true)
end end
local ServerIPStr = ServerMinecraftData.IP .. ':' .. ServerMinecraftData.PORT local ServerIPStr = ServerMinecraftData.IP..':'..ServerMinecraftData.PORT
local Headers, Body = Request("GET", ('https://api.mcsrvstat.us/3/%s'):format(ServerIPStr), nil, local Response, Headers = http_request('GET', ('https://api.mcsrvstat.us/3/%s'):format(ServerIPStr))
{ ["User-Agent"] = "tommy-bot/1.0 Main-Release" })
if not Headers.code == 200 then local IsOnline = Response.online
return error("Something went wrong")
end
local IsOnline = Body.online
local EmbedData local EmbedData
if IsOnline then if IsOnline then
local MaxPlayers = Body.players.max local MaxPlayers = Response.players.max
local OnlinePlayers = Body.players.online local OnlinePlayers = Response.players.online
local AnonymousPlayers = OnlinePlayers local AnonymousPlayers = OnlinePlayers
local Players = {} local Players = {}
if OnlinePlayers > 0 then if OnlinePlayers>0 then
for PlayerIndex, PlayerData in next, Body.players.list do for PlayerIndex, PlayerData in next, Response.players.list do
table.insert(Players, PlayerData.name) table.insert(Players, PlayerData.name)
AnonymousPlayers = AnonymousPlayers - 1 AnonymousPlayers = AnonymousPlayers-1
end end
else else
table.insert(Players, 'No players online') table.insert(Players, 'No players online')
end end
if AnonymousPlayers > 0 then if AnonymousPlayers>0 then
for AnonymousPlayerIndex = 1, AnonymousPlayers do for AnonymousPlayerIndex = 1, AnonymousPlayers do
table.insert(Players, 'Anonymous Player') table.insert(Players, 'Anonymous Player')
end end
end end
EmbedData = { EmbedData = {
title = 'Server Status for ' .. ServerIPStr, title = 'Server Status for '..ServerIPStr,
description = Body.motd.clean[1] .. ' (' .. Body.version .. ')', description = Response.motd.clean[1]..' ('..Response.version..')',
fields = { fields = {
{ name = 'Players', value = OnlinePlayers .. '/' .. MaxPlayers, inline = true }, {name = 'Players', value = OnlinePlayers..'/'..MaxPlayers, inline = true},
{ name = 'List of players', value = table.concat(Players, '\n'), inline = true } {name = 'List of players', value = table.concat(Players, '\n'), inline = true}
}, },
color = COLOURS.GREEN color = COLOURS.GREEN
} }
else else
EmbedData = { EmbedData = {
title = 'Server Status for ' .. ServerIPStr, title = 'Server Status for '..ServerIPStr,
description = 'Server is offline', description = 'Server is offline',
color = COLOURS.RED color = COLOURS.RED
} }
end end
return Interaction:reply({ embed = EmbedData }) return Interaction:reply({embed = EmbedData})
end) end)
MinecraftSubCommandHandler:AddSubCommand(MinecraftSetIpSubCommand.name, function(Interaction, Command, Args) MinecraftSubCommandHandler:AddSubCommand(MinecraftSetIpSubCommand.name, function(Interaction, Command, Args)
@ -113,15 +107,15 @@ MinecraftSubCommandHandler:AddSubCommand(MinecraftSetIpSubCommand.name, function
if not ServerIP then if not ServerIP then
return Interaction:reply('Invalid server IP') return Interaction:reply('Invalid server IP')
end end
local ServerPort = ServerIPStr:match(ServerIP .. ':(%d+)') or 25565 local ServerPort = ServerIPStr:match(ServerIP..':(%d+)') or 25565
local GuildMinecraftData = { IP = ServerIP, PORT = ServerPort } local GuildMinecraftData = {IP = ServerIP, PORT = ServerPort}
local GlobalMinecraftData = json.decode(io.open('minecraft_data.json', 'r'):read('*a')) local GlobalMinecraftData = json.decode(io.open('minecraft_data.json','r'):read('*a'))
GlobalMinecraftData[GuildId] = GuildMinecraftData GlobalMinecraftData[GuildId] = GuildMinecraftData
io.open('minecraft_data.json', 'w+'):write(json.encode(GlobalMinecraftData)):close() io.open('minecraft_data.json','w+'):write(json.encode(GlobalMinecraftData)):close()
return Interaction:reply('Successfully added `' .. ServerIP .. ':' .. ServerPort .. '` for ServerId=' .. GuildId) return Interaction:reply('Successfully added `'..ServerIP..':'..ServerPort..'` for ServerId='..GuildId)
end) end)
return { return {

View File

@ -1,56 +0,0 @@
local SlashCommandTools = require('discordia-slash').util.tools()
local Discordia = require('discordia')
local Date = Discordia.Date
Discordia.extensions()
local StrafesNET = require('../Modules/StrafesNET.lua')
local CalculateCommand = SlashCommandTools.slashCommand('calculate', 'Calculate rank and skill points')
local UsernameOption = SlashCommandTools.string('username', 'Username to look up')
local UserIdOption = SlashCommandTools.integer('user_id', 'User ID to look up')
local MemberOption = SlashCommandTools.user('member', 'User to look up')
local GameIdOption = SlashCommandTools.integer
CalculateCommand:addOption(UsernameOption)
CalculateCommand:addOption(UserIdOption)
CalculateCommand:addOption(MemberOption)
local function Callback(Interaction, Command, Args)
local UserInfo
if Args then
if Args.username then
local Headers, Response = StrafesNET.GetRobloxInfoFromUsername(Args.username)
if Headers.code < 400 then
UserInfo = Response
end
elseif Args.user_id then
local Headers, Response = StrafesNET.GetRobloxInfoFromUserId(Args.user_id)
if Headers.code < 400 then
UserInfo = Response
end
elseif Args.member then
local Headers, Response = StrafesNET.GetRobloxInfoFromDiscordId(Args.member.id)
if Headers.code < 400 then
UserInfo = Response
end
end
else
local Headers, Response = StrafesNET.GetRobloxInfoFromDiscordId((Interaction.member or Interaction.user).id)
if Headers.code < 400 then
UserInfo = Response
end
end
if UserInfo == nil then
error("SOMETHING WENT REALLY WRONG")
end
-- Add args for game/style etc and grab all times and grab all placements
end
return {
Command = CalculateCommand,
Callback = Callback
}

View File

@ -159,7 +159,6 @@ local function Callback(Interaction, Command, Args)
local awardedDate = tonumber(Date.fromISO(badge.awardedDate):toSeconds()) local awardedDate = tonumber(Date.fromISO(badge.awardedDate):toSeconds())
if firstBadgeDate > awardedDate then if firstBadgeDate > awardedDate then
firstBadge = badgeId firstBadge = badgeId
firstBadgeDate = awardedDate
end end
-- badgesDates[badgeId]=awardedDate -- badgesDates[badgeId]=awardedDate
end end

View File

@ -5,12 +5,6 @@ local CommandCollector = require('./Modules/CommandCollector.lua')
local Client = Discordia.Client():useApplicationCommands() local Client = Discordia.Client():useApplicationCommands()
Discordia.extensions() Discordia.extensions()
table.clear = function(t)
for k in pairs(t) do
t[k] = nil
end
end
local MessageCommandCollector = CommandCollector.new('Message'):Collect() local MessageCommandCollector = CommandCollector.new('Message'):Collect()
local SlashCommandCollector = CommandCollector.new('Slash'):Collect() local SlashCommandCollector = CommandCollector.new('Slash'):Collect()
local UserCommandCollector = CommandCollector.new('User'):Collect() local UserCommandCollector = CommandCollector.new('User'):Collect()