From 8298c9b5733658bfacba0a06386b79dcfdcf8d64 Mon Sep 17 00:00:00 2001
From: tommy <thefamousdoge@hotmail.com>
Date: Wed, 10 Jul 2024 15:48:26 -0400
Subject: [PATCH 1/4] fix dependencies

---
 install_discordia.bat | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/install_discordia.bat b/install_discordia.bat
index bacc19a..407a56a 100644
--- a/install_discordia.bat
+++ b/install_discordia.bat
@@ -1,3 +1,3 @@
 .\exes\lit.exe install SinisterRectus/discordia
-.\exes\lit.exe install GitSparTV/discordia-slash
-git clone https://github.com/Bilal2453/discordia-interactions.git ./deps/discordia-interactions
\ No newline at end of file
+git clone git@github.com:GitSparTV/discordia-slash.git ./deps/discordia-slash
+git clone git@github.com:Bilal2453/discordia-interactions.git ./deps/discordia-interactions
\ No newline at end of file

From a9a5e1b556bf0dc0878836fc0d4c14025fbc6bc0 Mon Sep 17 00:00:00 2001
From: tommy <thefamousdoge@hotmail.com>
Date: Wed, 10 Jul 2024 15:49:51 -0400
Subject: [PATCH 2/4] Delete old and unused garbage

---
 src/main.lua                       | 111 +++++++------------
 src/modules/commands.lua           |  34 ------
 src/modules/commands/cmds.lua      |  14 ---
 src/modules/commands/download.lua  |  78 --------------
 src/modules/commands/map.lua       |  28 -----
 src/modules/commands/maps_init.lua |  71 ------------
 src/modules/commands/minecraft.lua | 114 --------------------
 src/modules/commands/pb.lua        |  60 -----------
 src/modules/commands/rank.lua      |  40 -------
 src/modules/commands/restart.lua   |  22 ----
 src/modules/commands/skillCalc.lua | 132 -----------------------
 src/modules/commands/user.lua      | 167 -----------------------------
 src/modules/commands/wrmap.lua     |  50 ---------
 13 files changed, 38 insertions(+), 883 deletions(-)
 delete mode 100644 src/modules/commands.lua
 delete mode 100644 src/modules/commands/cmds.lua
 delete mode 100644 src/modules/commands/download.lua
 delete mode 100644 src/modules/commands/map.lua
 delete mode 100644 src/modules/commands/maps_init.lua
 delete mode 100644 src/modules/commands/minecraft.lua
 delete mode 100644 src/modules/commands/pb.lua
 delete mode 100644 src/modules/commands/rank.lua
 delete mode 100644 src/modules/commands/restart.lua
 delete mode 100644 src/modules/commands/skillCalc.lua
 delete mode 100644 src/modules/commands/user.lua
 delete mode 100644 src/modules/commands/wrmap.lua

diff --git a/src/main.lua b/src/main.lua
index ba2209d..e80d873 100644
--- a/src/main.lua
+++ b/src/main.lua
@@ -1,80 +1,45 @@
-local discordia = require('discordia')
-local dcmd = require('discordia-slash')
-local token = require('./modules/token.lua')
-local commands=require('./modules/commands.lua')
-local prefix = ','
-local client = discordia.Client()
-client:useSlashCommands()
-_G.dcmd=dcmd
-_G.client = client
-_G.locked = false
+local Discordia = require('discordia')
+local DiscordiaSlash = require('discordia-slash')
+local Token = require('./Modules/Token.lua')
+local CommandCollector = require('./Modules/CommandCollector.lua')
+local Client = Discordia.Client():useApplicationCommands()
+Discordia.extensions()
 
-discordia.extensions()
+local MessageCommandCollector = CommandCollector.new('Message'):Collect()
+local SlashCommandCollector = CommandCollector.new('Slash'):Collect()
+local UserCommandCollector = CommandCollector.new('User'):Collect()
 
-client:on('ready',function()
-    commands:INIT()
-    local f=io.open('restart.txt','r+'):read()
-    local t=tostring(f):split(',')
-    if #t==3 then
-        client:getGuild(t[1]):getChannel(t[2]):send(
-            {
-                content='bot ready',
-                reference={
-                    message=client:getChannel(t[2]):getMessage(t[3]),
-                    mention=true
-                }
-            }
-        )
-        io.open('restart.txt','w+'):write(''):close()
-    else
-        print('restart.txt is empty or something so probably a first start')
-    end
+Client:on('ready', function()
+    -- local GlobalCommands = Client:getGlobalApplicationCommands()
+
+    -- for CommandId in pairs(GlobalCommands) do
+    --     Client:deleteGlobalApplicationCommand(CommandId)
+    -- end
+
+    MessageCommandCollector:Publish(Client)
+    SlashCommandCollector:Publish(Client)
+    UserCommandCollector:Publish(Client)
 end)
-function parseMentions(message)
-    local content=message.content
-    local usersMentioned={}
-    if #message.mentionedUsers>0 then
-        for user in message.mentionedUsers:iter() do
-            usersMentioned[user.id]=user
-        end
-    end
-    local msgSplit=content:split(' ')
-    for i,v in next, msgSplit do
-        if v:match('<@![0-9]+>') then
-            local id=v:match('<@!([0-9]+)>')
-            if usersMentioned[id] then
-                msgSplit[i]=usersMentioned[id].mentionString
-            end
-        end
-    end
-    return table.concat(msgSplit,' ') or '',usersMentioned
-end
 
-client:on('messageCreate', function(message)
-    if message.author.bot then return end
-    local content,mentions=parseMentions(message)
-    if content:sub(1,#prefix)==prefix and content~=prefix then
-        local cmd=content:sub(#prefix+1,#content)
-        local args=cmd:split(' ')
-        local cmdName=args[1]
-        table.remove(args,1)
-        local command=commands.command_list[cmdName]
-        if command~=nil then
-            if message.guild~=nil then
-                local tb
-                local s,e=xpcall(function()
-                    command.exec({message=message,args=args,mentions=mentions,t={client,discordia,token}})
-                end,function(err)
-                    tb = debug.traceback()
-                    return err
-                end)
-                if not s then
-                    message:reply('tripped : '..e:split('/')[#e:split('/')])
-                    print(e,tb)
-                end
-            end
-        end
+Client:on('slashCommand', function(Interaction, Command, Args)
+    local SlashCommand = SlashCommandCollector:Get(Command.name)
+    if SlashCommand then
+        SlashCommand.Callback(Interaction, Command, Args)
     end
 end)
 
-client:run('Bot '..token)
\ No newline at end of file
+Client:on('messageCommand', function(Interaction, Command, Message)
+    local MessageCommand = MessageCommandCollector:Get(Command.name)
+    if MessageCommand then
+        MessageCommand.Callback(Interaction, Command, Message)
+    end
+end)
+
+Client:on('userCommand', function(Interaction, Command, Member)
+    local UserCommand = UserCommandCollector:Get(Command.name)
+    if UserCommand then
+        UserCommand.Callback(Interaction, Command, Member)
+    end
+end)
+
+Client:run('Bot '..Token)
\ No newline at end of file
diff --git a/src/modules/commands.lua b/src/modules/commands.lua
deleted file mode 100644
index a802bf3..0000000
--- a/src/modules/commands.lua
+++ /dev/null
@@ -1,34 +0,0 @@
-local discordia=require('discordia')
-discordia.extensions()
-local commands={command_list={}}
-setmetatable(commands.command_list,{__index=function(self,index)
-    for i,v in pairs(self) do
-        for i2,v2 in pairs(v.alias) do
-            if v2==index then
-                return self[i]
-            end
-        end
-    end
-    return nil
-end})
-function commands:Add(name,alias,desc,exec)
-    name=type(name)=='string' and name or ('Command'..#self.command_list)
-    self.command_list[name]={
-        name=name,
-        alias=type(alias)=='table'and alias or {'None'},
-        desc=type(desc)=='string'and desc or ('No description provided'),
-        exec=type(exec)=='function'and exec or function(message)
-            return message:reply('No command assigned')
-        end
-    }
-    return self.command_list[name]
-end
-function commands:Get(name)
-    return self.command_list[name]
-end
-function commands:INIT()
-    for file in io.popen([[dir "./src/modules/commands" /b]]):lines() do require('./commands/'..file) end
-    print('commands done')
-end
-
-return commands
\ No newline at end of file
diff --git a/src/modules/commands/cmds.lua b/src/modules/commands/cmds.lua
deleted file mode 100644
index 7a0f5e8..0000000
--- a/src/modules/commands/cmds.lua
+++ /dev/null
@@ -1,14 +0,0 @@
-local discordia=require('discordia')
-local commands=require('./../commands.lua')
-discordia.extensions()
-commands:Add('cmds',{'commands','cmd','help'},'Returns a list of all commands',function(t)
-    local final='```\n'
-    for i,v in pairs(commands.command_list) do
-        local name=v.name
-        local alias=table.concat(v.alias,', ') or 'None'
-        local desc=v.desc
-        final=final..'Name: '..name..'\nDescription: '..desc..'\n'..'Aliases: '..alias..'\n\n'
-    end
-    final=final..'```'
-    t.message:reply(final)
-end)
\ No newline at end of file
diff --git a/src/modules/commands/download.lua b/src/modules/commands/download.lua
deleted file mode 100644
index 34c2ac1..0000000
--- a/src/modules/commands/download.lua
+++ /dev/null
@@ -1,78 +0,0 @@
-local discordia=require('discordia')
-local commands=require('./../commands.lua')
-discordia.extensions()
-
-function split(s,d)
-    local t,c,i={},'',0
-    for k in s:gmatch('.') do
-        i=i+1
-        if k==d and string.sub(s,i+1)~='' then
-            t[#t+1]=c
-            c=''
-            goto continue
-        end
-        c=c..k
-        ::continue::
-    end
-    t[#t+1]=c
-    return t
-end
-
-function clearTmp()
-    for file in io.popen([[dir "./tmp" /b]]):lines() do
-        if file then
-            os.remove('./tmp/'..file)
-        end
-    end
-end
-function isTmpEmpty()
-    local dir = io.popen([[dir "./tmp" /b]]):read()
-    return dir==nil, dir, dir~=nil and split(dir,'\n') or {}
-end
-
-commands:Add('sc',{},'download soundcloud song (usage: "sc [link]")', function(t)
-    local args = t.args
-    local message = t.message
-    if args[1] then
-        if args[1]:match('https://soundcloud.com/[%w-_]+/[%w-_]+') then
-            clearTmp()
-            local link=args[1]:match('https://soundcloud.com/[%w-_]+/[%w-_]+')
-            message:reply('Attempting to download song from <'..link..'>')
-            local filepath = ''
-            local s=io.popen('ytdl.exe -o "./tmp/%(uploader_id)s-%(display_id)s.%(ext)s" '..link)
-            local songName
-            repeat
-                local str = s:read()
-                if str then
-                    local tag = str:match('^%[(.+)%]')
-                    if tag=='soundcloud' then
-                        local song = str:match('^%[soundcloud%] (.+):')
-                        if song:match('%d+')~=song then
-                            songName = song:match('.+/(.+)')
-                        end
-                    end
-                end
-            until s:read()==nil
-            s:close()
-            if type(songName)=='string' and songName~='' then
-                message:reply('found song: '..songName)
-                local empty,file = isTmpEmpty()
-                if not empty then
-                    message:reply({file='./tmp/'..file})
-                    os.remove('./tmp/'..file)
-                end
-            end
-        else
-            message:reply('Invalid URL')
-        end
-    else
-        message:reply('No URL provided')
-    end
-end)
-
--- commands:Add('ct',{},'',function()
---     clearTmp()
--- end)
--- commands:Add('ft',{},'',function()
---     filterTmp()
--- end)
\ No newline at end of file
diff --git a/src/modules/commands/map.lua b/src/modules/commands/map.lua
deleted file mode 100644
index 9552d23..0000000
--- a/src/modules/commands/map.lua
+++ /dev/null
@@ -1,28 +0,0 @@
-local discordia=require('discordia')
-local API=require('./../strafes_net.lua')
-local commands=require('./../commands.lua')
-discordia.extensions()
-
-commands:Add('map',{},'get map info', function(t)
-    local args = t.args
-    local message = t.message
-
-    local game = API.GAMES[args[1]]
-    local map
-    if not game then
-        local str = table.concat(args,' ')
-        map = API.MAPS[1][str] or API.MAPS[2][str]
-    else
-        map = API.MAPS[game][table.concat(args,' ',2)]
-    end
-    
-    if not map then return message:reply('```No map found```') end
-    local formatted_message = '```'..
-                            'Map: '..map.DisplayName..' ('..API.GAMES[map.Game]..')\n'..
-                            'ID: '..map.ID..'\n'..
-                            'Creator: '..map.Creator..'\n'..
-                            'PlayCount: '..map.PlayCount..'\n'..
-                            'Published: '..os.date('%A, %B %d %Y @ %I:%M (%p)',map.Date)..
-                            '```'
-    return message:reply(formatted_message)
-end)
\ No newline at end of file
diff --git a/src/modules/commands/maps_init.lua b/src/modules/commands/maps_init.lua
deleted file mode 100644
index e9ec84b..0000000
--- a/src/modules/commands/maps_init.lua
+++ /dev/null
@@ -1,71 +0,0 @@
--- local discordia=require('discordia')
--- local API=require('./../strafes_net.lua')
--- discordia.extensions()
--- API.MAPS={}
-
--- local function insert(t, value)
---     local start, ending, mid, state = 1, #t, 1, 0
-
---     while start <= ending do
---         mid = math.floor((start + ending) / 2)
---         if #value.DisplayName < #t[mid].DisplayName then
---             ending, state = mid - 1, 0
---         else
---             start, state = mid + 1, 1
---         end
---     end
-
---     table.insert(t, mid + state, value)
--- end
-
--- for _, game in next, API.GAMES do
---     if type(tonumber(game)) == 'number' then
---         local count = 0 -- add into the maps table afterwards
---         local maps = {}
---         local res, headers = API:GetMaps(game)
---         local pages = tonumber(headers['Pagination-Count'])
-
---         count = count + #res
-
---         for _, v in next, res do
---             insert(maps, v)
---         end
-
---         if pages > 1 then
---             for i = 2, pages do
---                 res, headers = API:GetMaps(game, i)
---                 count = count + #res
-
---                 for _, j in next, res do
---                     insert(maps, j)
---                 end
---             end
---         end
-
---         setmetatable(maps, {__index = function(self, k)
---             if k=='count' then return self.count end
-
---             -- Just to make sure it goes in the right order
---             if type(k)=='string' then
---                 for i = 1, self.count do
---                     local v = self[i]
---                     if type(v) == 'table' and v.DisplayName:lower():find(tostring(k:gsub('%%', '%%%%'):gsub('^%^', '%%^'):gsub('%$$', '%%$'):gsub('%(', '%%('):gsub('%)', '%%)'):gsub('%.', '%%.'):gsub('%[', '%%['):gsub('%]', '%%]'):gsub('%*', '%%*'):gsub('%+', '%%+'):gsub('%-', '%%-'):gsub('%?', '%%?')):lower()) then
---                         return v
---                     end
---                 end
---             elseif type(k)=='number' then
---                 for i = 1, self.count do
---                     local v = self[i]
-
---                     if type(v) == 'table' and v.ID==k then
---                         return v
---                     end
---                 end
---             end
---         end})
-
---         maps.count = count
---         API.MAPS[game] = maps
---         print('map init done for game:', API.GAMES[game], 'count:', API.MAPS[game].count)
---     end
--- end
\ No newline at end of file
diff --git a/src/modules/commands/minecraft.lua b/src/modules/commands/minecraft.lua
deleted file mode 100644
index 0f0d1a4..0000000
--- a/src/modules/commands/minecraft.lua
+++ /dev/null
@@ -1,114 +0,0 @@
-local discordia=require('discordia')
-local json=require('json')
-local http_request=require('./../http.lua')
-local Commands=require('./../commands.lua')
-discordia.extensions()
-
-local COLOURS={
-	GREEN=0x00ff00,
-	RED=0xff0000
-}
-
---initialize minecraft ip data
-local MinecraftDataFile=io.open('minecraft_data.json','r')
-if not MinecraftDataFile or (MinecraftDataFile and MinecraftDataFile:read('*a')=='') then
-	print('no such file exists! so make it')
-	io.open('minecraft_data.json','w+'):write(json.encode({})):close()
-end
-if MinecraftDataFile then
-	MinecraftDataFile:close()
-end
-
-Commands:Add('setip',{},'set ip for status',function(CommandData)
-	local CommandArgs=CommandData.args
-	local CommandMessage=CommandData.message
-	local ServerIPStr=CommandArgs[1]
-	if not ServerIPStr then
-		return CommandMessage:reply('No IP provided')
-	end
-
-	local ServerIP=ServerIPStr:match("(%d+%.%d+%.%d+%.%d+)") or ServerIPStr:match("(%w*%.?%w+%.%w+)")
-	if not ServerIP then
-		return CommandMessage:reply('Invalid server IP')
-	end
-	local ServerPort=ServerIPStr:match(ServerIP..':(%d+)') or 25565
-
-	local GuildId=CommandMessage.guild.id
-	if not GuildId then
-		return CommandMessage:reply('You cannot use this command outside of a Discord server')
-	end
-
-	local GuildMinecraftData={IP=ServerIP,PORT=ServerPort}
-
-	local GlobalMinecraftData=json.decode(io.open('minecraft_data.json','r'):read('*a'))
-	GlobalMinecraftData[GuildId]=GuildMinecraftData
-	io.open('minecraft_data.json','w+'):write(json.encode(GlobalMinecraftData)):close()
-
-	return CommandMessage:reply({
-		content='Successfully added `'..ServerIP..':'..ServerPort..'` for ServerId='..GuildId,
-		reference={
-			message=CommandMessage,
-			mention=true
-		}
-	})
-end)
-
-Commands:Add('status',{},'get status for minecraft server',function(CommandData)
-	local CommandMessage=CommandData.message
-
-	local GuildId=CommandMessage.guild.id
-	if not GuildId then
-		return CommandMessage:reply('You cannot use this command outside of a Discord server')
-	end
-
-	local GlobalMinecraftData=json.decode(io.open('minecraft_data.json','r'):read('*a'))
-	if not GlobalMinecraftData then
-		return CommandMessage:reply('Could not read server data')
-	end
-
-	local ServerMinecraftData=GlobalMinecraftData[GuildId]
-	if not ServerMinecraftData then
-		return CommandMessage:reply('There is no data for this Discord server')
-	end
-
-	local ServerIPStr=ServerMinecraftData.IP..':'..ServerMinecraftData.PORT
-	local Response,Headers=http_request('GET',('https://api.mcsrvstat.us/3/%s'):format(ServerIPStr))
-
-	local IsOnline=Response.online
-	local EmbedData
-	if IsOnline then
-		local MaxPlayers=Response.players.max
-		local OnlinePlayers=Response.players.online
-		local AnonymousPlayers=OnlinePlayers
-		local Players={}
-		if OnlinePlayers>0 then
-			for PlayerIndex,PlayerData in next,Response.players.list do
-				table.insert(Players,PlayerData.name)
-				AnonymousPlayers=AnonymousPlayers-1
-			end
-		else
-			table.insert(Players,'No players online')
-		end
-		if AnonymousPlayers>0 then
-			for AnonymousPlayerIndex=1,AnonymousPlayers do
-				table.insert(Players,'Anonymous Player')
-			end
-		end
-		EmbedData={
-	        title='Server Status for '..ServerIPStr,
-	        description=Response.motd.clean[1]..' ('..Response.version..')',
-	        fields={
-	        	{name='Players',value=OnlinePlayers..'/'..MaxPlayers,inline=true},
-	        	{name='List of players',value=table.concat(Players,'\n'),inline=true}
-	        },
-	        color=COLOURS.GREEN
-	    }
-	else
-		EmbedData={
-			title='Server Status for '..ServerIPStr,
-			description='Server is offline',
-			color=COLOURS.RED
-		}
-	end
-	return CommandMessage:reply({embed=EmbedData})
-end)
\ No newline at end of file
diff --git a/src/modules/commands/pb.lua b/src/modules/commands/pb.lua
deleted file mode 100644
index bb306f7..0000000
--- a/src/modules/commands/pb.lua
+++ /dev/null
@@ -1,60 +0,0 @@
-local discordia=require('discordia')
-local API=require('./../strafes_net.lua')
-local commands=require('./../commands.lua')
-local pad = API.Pad
-
-discordia.extensions()
-
--- args: user, game, style, map
-commands:Add('pb', {}, 'get placement on map', function(t)
-    local args = t.args
-    local message = t.message
-
-    if #args < 4 then return message:reply('invalid arguments') end
-
-    local user = API:GetUserFromAny(args[1],message)
-    local sn_info = API:GetUser(user.id)
-    local game = API.GAMES[args[2]]
-    local style = API.STYLES[args[3]]
-    local map = API.MAPS[game][table.concat(args,' ',4)]
-
-    -- i love checks
-    if not game then return message:reply('invalid game') end
-    if not style then return message:reply('invalid style') end
-    if not map then return message:reply('invalid map') end
-    if not sn_info.ID then return message:reply('```No data with StrafesNET is associated with that user.```') end
-    if sn_info.State==2 then return message:reply('```This user is currently blacklisted```') end
-
-    local time = API:GetUserTimes(user.id, map.ID, style, game)[1]
-
-    if not time then return message:reply('```No time was found.```') end
-
-    local rank = API:GetTimeRank(time.ID).Rank
-    local count = tonumber(API:GetMapCompletionCount(time.Map, style))
-
-    if not rank or not count then
-        rank = 1
-        count = 1
-    end
-
-    local time_formatted = API.FormatTime(time.Time)
-    local date = os.date("%x", time.Date)
-    local placement = rank .. '/' .. count
-    local points = API.CalculatePoint(rank, count)
-
-    local t_n, d_n, p_n= #time_formatted, 8, math.max(#placement, 10)
-
-    local first_line = 'PB Time for map: '..map.DisplayName..' ('..API.GAMES[game]..', '..API.STYLES_LIST[style]..')'
-
-    local second_line = pad('Time:', t_n + 1) .. '| '
-                    .. pad('Date:', d_n + 1) .. '| '
-                    .. pad('Placement:', p_n + 1) .. '| '
-                    .. 'Points:'
-
-    local third_line = pad(time_formatted, t_n + 1) .. '| '
-                     .. pad(date, d_n + 1) .. '| '
-                     .. pad(placement, p_n + 1) .. '| '
-                     .. tostring(points)
-
-    return message:reply('```' .. first_line .. '\n' .. second_line .. '\n' .. third_line .. '```')
-end)
diff --git a/src/modules/commands/rank.lua b/src/modules/commands/rank.lua
deleted file mode 100644
index e13e930..0000000
--- a/src/modules/commands/rank.lua
+++ /dev/null
@@ -1,40 +0,0 @@
-local discordia=require('discordia')
-local API=require('./../strafes_net.lua')
-local commands=require('./../commands.lua')
-function dump(a,b,c,d)b=b or 50;d=d or("DUMP START "..tostring(a))c=c or 0;for e,f in next,a do local g;if type(f)=="string"then g="\""..f.."\""else g=tostring(f)end;d=d.."\nD "..string.rep(" ",c*2)..tostring(e)..": "..g;if type(f)=="table"then if c>=b then d=d.." [ ... ]"else d=dump(f,b,c+1,d)end end end;return d end
-discordia.extensions()
-commands:Add('rank',{},'rank <username|mention|"me"> <game> <style>', function(t)
-    local args=t.args
-    local message=t.message
-
-    if #args<3 then return message:reply('invalid arguments') end
-
-    local user=args[1]
-    local game=API.GAMES[args[2]]
-    local style=API.STYLES[args[3]]
-
-    if not game then return message:reply('invalid game') end
-    if not style then return message:reply('invalid style') end
-
-    user = API:GetUserFromAny(user,message)
-
-    local sn_info = API:GetUser(user.id)
-
-    if not sn_info.ID then return message:reply('```No data with StrafesNET is associated with that user.```') end
-    if sn_info.State==2 then return message:reply('```This user is currently blacklisted```') end
-
-    local rank = API:GetRank(user.id,game,style)
-    local rank_string = API.FormatRank(rank.Rank)
-    local skill = API.FormatSkill(rank.Skill)
-
-    local formatted_message = '```'..
-    'Name: '..user.displayName..' ('..user.name..')\n'..
-    'Style: '..API.STYLES_LIST[rank.Style]..'\n'..
-    'Rank: '..rank_string..'\n'..
-    'Skill: '..skill..'\n'..
-    'Placement: '..rank.Placement..'\n'..
-    'State: '..API.STATES[sn_info.State]..'\n'..
-    '```'
-
-    message:reply(formatted_message)
-end)
\ No newline at end of file
diff --git a/src/modules/commands/restart.lua b/src/modules/commands/restart.lua
deleted file mode 100644
index 0a81c02..0000000
--- a/src/modules/commands/restart.lua
+++ /dev/null
@@ -1,22 +0,0 @@
-local discordia=require('discordia')
-local commands=require('./../commands.lua')
-discordia.extensions()
-function wait(n)local c=os.clock local t=c()while c()-t < n do end;end
-commands:Add('restart',{},"restart bot [dev]", function(t)
-    if t.message.author==t.t[1].owner then
-        t.message:addReaction('👍')
-        t.t[1]:stop()
-        wait(1.5)
-        io.open('restart.txt','w+'):write(t.message.guild.id..','..t.message.channel.id..','..t.message.id):close()
-        os.execute('.\\exes\\luvit ./src/main.lua')
-    end
-end)
-commands:Add('leave',{},'leave',function(t)
-    if t.message.author==t.t[1].owner then
-        t.message:delete()
-        local left = t.message.guild:leave()
-        if left then
-            print('left')
-        end
-    end
-end)
\ No newline at end of file
diff --git a/src/modules/commands/skillCalc.lua b/src/modules/commands/skillCalc.lua
deleted file mode 100644
index d2c17e3..0000000
--- a/src/modules/commands/skillCalc.lua
+++ /dev/null
@@ -1,132 +0,0 @@
-local discordia=require('discordia')
-local API=require('./../strafes_net.lua')
-local commands=require('./../commands.lua')
-function sleep(n) local t = os.clock() while os.clock()-t <= n do end end
-discordia.extensions()
-local pad = API.Pad
-commands:Add('skill',{},'skill <username|mention|"me"> <game> <style> <sort?=skill|point>', function(t)
-    local args=t.args
-    local message=t.message
-    if not _G.locked then
-        if #args<3 then return message:reply('usage: `skill <username|mention|"me"> <game> <style> <sort?="skill"|"point">`') end
-        local user=args[1]
-        local game=API.GAMES[args[2]]
-        local style=API.STYLES[args[3]]
-        if not game then return message:reply('invalid game') end
-        if not style then return message:reply('invalid style') end
-        local sort = args[4]
-        if type(sort)=='string' and not sort:lower():find('skill') and not sort:lower():find('point') then
-            return message:reply('invalid sort option, valid options are "skill" or "point"')
-        elseif sort==nil then
-            sort = 'skill'
-        end
-        print('getting user')
-        local user = API:GetUserFromAny(user,message)
-        if type(user)=='string' then return message:reply('```'..user..'```') end
-        local sn_info = API:GetUser(user.id)
-        if not sn_info.ID then return message:reply('```No data with StrafesNET is associated with that user.```') end
-        if sn_info.State==2 then return message:reply('```This user is currently blacklisted```') end
-        print(user.name,user.id,API.GAMES[game],API.STYLES[style]:lower())
-        _G.locked = true
-        _G.current = {name=user.name,game=API.GAMES[game],style=API.STYLES[style]:lower()}
-        local times = {}
-        local res,rheaders = API:GetUserTimes(user.id,nil,style,game)
-        if #res~=0 then
-            local pages = tonumber(rheaders['Pagination-Count'])
-            for _,v in next,res do
-                table.insert(times,v)
-            end
-            if pages>1 then
-                for i=2,pages do
-                    print('getting times page',i)
-                    res,rheaders = API:GetUserTimes(user.id,nil,style,game,i)
-                    for _,v in next,res do
-                        table.insert(times,v)
-                    end
-                end
-            end
-            print('times:',#times)
-            t.message:reply('ETA: '..(math.floor(#times*3/100))..' minutes '..((#times*3)%60)..' seconds (found '..#times..' times out of '..API.MAPS[game].count..' maps)')
-            local test_a,test_b = 0,0
-            for _,time in next,times do
-                local rank = API:GetTimeRank(time.ID).Rank
-                local count = tonumber(API:GetMapCompletionCount(time.Map,style))
-                if not rank or not count then
-                    print('NO RANK OR COUNT')
-                    print(rank,count)
-                    rank = 1
-                    count = 1
-                end
-                time.Points = API.CalculatePoint(rank,count)
-                time.Rank = rank
-                time.MapCompletionCount = count
-                time.SkillRaw = rank == 1 and 1 or (count-rank)/(count-1)
-                time.Skill = API.FormatSkill(time.SkillRaw)
-                test_a=test_a+(count-rank)
-                test_b=test_b+(count-1)
-            end
-            table.sort(times,sort:find('skill') and function(t1,t2)
-                return t1.SkillRaw<t2.SkillRaw
-            end or sort:find('point') and function(t1,t2)
-                return t1.Points<t2.Points
-            end)
-            local points = 0
-            for _,time in next,times do
-                points = points+time.Points
-            end
-            local skillFinal = (test_a)/(test_b-1)
-            local msg = 'Average Skill: '..API.FormatSkill(math.clamp(skillFinal,0,1))..'\n'..
-                        'Points: '..points..'\n'..
-                        pad('Map',50)..' | '..pad('Points')..' | '..pad('Skill',7)..' | '.. pad('Placement',14)..' | Time\n\n'
-                        
-            for _,time in next,times do
-                -- msg = msg..'['..time.Rank..'/'..time.MapCompletionCount..'] '..time.Map..' ('..time.Skill..')\n'
-                local mapStr = API.MAPS[game][time.Map].DisplayName..' ('..time.Map..')'
-                local skill = time.Skill
-                local point = time.Points
-                local rankStr = time.Rank..'/'..time.MapCompletionCount
-                local timeStr = API.FormatTime(time.Time)
-                msg = msg.. pad(mapStr,50)..' | '..pad(point)..' | '..pad(skill,7)..' | '.. pad(rankStr,14)..' | '..timeStr..'\n'
-            end
-            local txt = './skill-'..API.GAMES[game]..'-'..API.STYLES[style]:lower()..'-'..user.name..'.txt'
-            local file=io.open(txt,'w+')
-            file:write(msg)
-            file:close()
-            message:reply({
-                file=txt,
-                reference={
-                    message=message,
-                    mention=true
-                }
-            })
-            os.remove(txt)
-            _G.locked = false
-        else
-            message:reply('```No times found for that user.```')
-            _G.locked = false
-        end
-    else
-        --_G.current = {name=user.name,game=API.GAMES[game],style=API.STYLES[style]:lower()}
-        message:reply('Bot is currently in use, please try again later ('.._G.current.name..' for '.._G.current.game..' in '.._G.current.style..')')
-    end
-end)
-
-commands:Add('compare',{},'compare n1 n2', function(t)
-    local args=t.args
-    local message=t.message
-    local n1 = args[1]
-    local n2 = args[2]
-    local compared = API.CalculateDifference(n1,n2)
-    local compared_percent = API.CalculateDifferencePercent(n1,n2)
-    message:reply(tostring(compared)..' ('..compared_percent..')')
-end)
-
-commands:Add('calc',{},nil,function(t)
-    local args=t.args
-    local message=t.message
-    local rank = args[1]
-    local count = args[2]
-    local points=API.CalculatePoint(rank,count)
-    local skill=API.FormatSkill(rank == 1 and 1 or (count-rank)/(count-1))
-    message:reply('```Points: '..points..'\nSkill: '..skill..'```')
-end )
\ No newline at end of file
diff --git a/src/modules/commands/user.lua b/src/modules/commands/user.lua
deleted file mode 100644
index 514da35..0000000
--- a/src/modules/commands/user.lua
+++ /dev/null
@@ -1,167 +0,0 @@
-local discordia=require('discordia')
-local date = discordia.Date
-local API=require('./../strafes_net.lua')
-local commands=require('./../commands.lua')
---[[
-    {
-  "description": "string",
-  "created": "2022-08-22T02:55:01.607Z",
-  "isBanned": true,
-  "externalAppDisplayName": "string",
-  "hasVerifiedBadge": true,
-  "id": 0,
-  "name": "string",
-  "displayName": "string"
-}]]
---[[
-{"GameId":null,
-"IsOnline":false,
-"LastLocation":"Offline",
-"LastOnline":"2022-08-21T22:32:23.4-05:00",
-"LocationType":2,
-"PlaceId":null,
-"VisitorId":1455906620,
-"PresenceType":0,
-"UniverseId":null,
-"Visibility":0}
-]]
-Badges = {
-    '275640532', --Bhop, pre-group
-    '363928432', --Surf, pre-group
-    '2124614454', --Bhop, post-group
-    '2124615096', --Surf, post-group
-}
-BadgesToName = {
-    [275640532]='old bhop',
-    [363928432]='old surf',
-    [2124614454]='new bhop',
-    [2124615096]='new surf',
-}
-local function round(x,n)
-    return string.format('%.'..(n or 0)..'f',x)
-end
-
-local function FromYMD(ymd)
-    return date.fromISO(ymd.."T00:00:00")[1]
-end
-local function leftpad(s,n,p)
-    return string.rep(p,n-#tostring(s))..s
-end
-local function ToYMD(seconds)
-    return "<t:"..seconds..":R>"
-end
-local IDToDate = { --Terrible ranges but it's all we have
-    -- {1000000000, FromYMD("2006-01-01")}, --I guess?
-    -- {1864564055, FromYMD("2008-08-04")},
-    {3800920136, FromYMD("2016-04-16")},
-    {9855616205, FromYMD("2017-04-02")},
-    {30361018662, FromYMD("2018-11-14")},
-    {32665806459, FromYMD("2019-01-07")},
-    {34758058773, FromYMD("2019-02-24")},
-    {65918261258, FromYMD("2020-06-05")},
-    {171994717435, FromYMD("2023-03-24")},
-    {173210319088, FromYMD("2023-04-14")},
-    {206368884641, FromYMD("2023-07-16")},
-    {229093879745, FromYMD("2024-01-02")},
-    {232802028144, FromYMD("2024-04-08")},
-    {234886704167, FromYMD("2024-06-28")}
-}
---We assume linear interpolation since anything more complex I can't process
-local function linterp(i1, i2, m)
-    return math.floor(i1 + (i2-i1)*m)
-end
-local function GuessDateFromAssetID(AssetID)
-    for i = #IDToDate, 1, -1 do --Newest to oldest
-        local ID,Time = unpack(IDToDate[i])
-        if ID < AssetID then
-            if not IDToDate[i+1] then
-                return "After "..ToYMD(Time)
-            end
-            local ParentID, ParentTime = unpack(IDToDate[i+1])
-            return "Around "..ToYMD(linterp(Time, ParentTime, (AssetID-ID)/(ParentID-ID)))
-        end
-    end
-    return "Before "..ToYMD(IDToDate[1][2])
-end
-
-discordia.extensions()
-commands:Add('user',{},'user <username|mention|"me">', function(t)
-    local args=t.args
-    local message=t.message
-    local user=args[1] or 'me'
-    local user_info=API:GetUserFromAny(user,message)
-    if type(user_info)=='string' then return message:reply('```'..user_info..'```') end
-
-    local description = user_info.description=='' and 'This user has no description' or user_info.description
-    -- table.foreach(user_info,print)
-    local created = tostring(date.fromISO(user_info.created):toSeconds())
-    local current = date():toSeconds()
-    local accountAge = round((current-created)/86400)
-    local isBanned = user_info.isBanned
-    local id = user_info.id
-    local name = user_info.name
-    local displayName = user_info.displayName
-
-    local usernameHistory = API:GetUserUsernameHistory(id).data or {}
-    local usernameHistoryTable = {}
-    for index,usernameObj in next,usernameHistory do
-        table.insert(usernameHistoryTable,usernameObj.name)
-    end
-    local usernameHistoryString = table.concat(usernameHistoryTable,', ')
-
-    local onlineStatus_info = API:GetUserOnlineStatus(id) or {lastLocation="Unknown", lastOnline=0, userPresenceType=-1}
-    -- table.foreach(onlineStatus_info,print)
-
-    local LastLocation = onlineStatus_info.lastLocation
-    if onlineStatus_info.userPresenceType==2 then LastLocation="Ingame" end
-    local LastOnline = date.fromISO(onlineStatus_info.lastOnline):toSeconds()
-
-    local verificationAssetId = API:GetVerificationItemID(id)
-    local verificationDate = "Not verified"
-    if verificationAssetId.errors then
-        verificationDate = "Failed to fetch"
-    elseif verificationAssetId.data[1] then
-        verificationDate = GuessDateFromAssetID(verificationAssetId.data[1].instanceId)
-    end
-
-    local badgeRequest = API:GetBadgesAwardedDates(id,Badges)
-    local badgeData = badgeRequest.data
-
-    -- local badgesDates = {}
-
-    local firstBadge,firstBadgeDate = 0,math.huge
-    for _,badge in next,badgeData do
-        local badgeId = badge.badgeId
-        local awardedDate = tonumber(date.fromISO(badge.awardedDate):toSeconds())
-        if firstBadgeDate>awardedDate then
-            firstBadge=badgeId
-            firstBadgeDate=awardedDate
-        end
-        -- badgesDates[badgeId]=awardedDate
-    end
-    local userThumbnail = API:GetUserThumbnail(id).data[1]
-
-    local embed = {
-        title = displayName..' (@'..name..')',
-        url = 'https://roblox.com/users/'..id..'/profile',
-        thumbnail = {
-            url = userThumbnail.imageUrl,
-        },
-        fields = {
-            {name='ID',value=id,inline=true},
-            {name='Account Age',value=accountAge..' days',inline=true},
-            {name='Created',value='<t:'..round(created)..':R>',inline=true},
-            {name='Verified Email',value=verificationDate,inline=true},
-            {name='Last Online',value='<t:'..round(LastOnline)..':R>',inline=true},
-            {name='Last Location',value=LastLocation,inline=true},
-            {name='Banned',value=isBanned,inline=true},
-            {name='Description',value=description,inline=false},
-            {name='Username History ('..#usernameHistoryTable..(#usernameHistoryTable==50 and '*' or '')..')',value=usernameHistoryString,inline=false},
-        }
-    }
-    if firstBadge and firstBadgeDate~=math.huge then
-        table.insert(embed.fields,{name='FQG',value=BadgesToName[firstBadge],inline=true})
-        table.insert(embed.fields,{name='Joined',value='<t:'..round(firstBadgeDate)..':R>',inline=true})
-    end
-    message:reply({embed=embed})
-end)
\ No newline at end of file
diff --git a/src/modules/commands/wrmap.lua b/src/modules/commands/wrmap.lua
deleted file mode 100644
index e9b32ac..0000000
--- a/src/modules/commands/wrmap.lua
+++ /dev/null
@@ -1,50 +0,0 @@
-local discordia=require('discordia')
-local API=require('./../strafes_net.lua')
-local commands=require('./../commands.lua')
-local pad = API.Pad
-
-discordia.extensions()
-
--- args: game, style, map
-commands:Add('wr', {}, 'get map wr', function(t)
-    local args = t.args
-    local message = t.message
-
-    if #args < 3 then return message:reply('invalid arguments') end
-    local game = API.GAMES[args[1]]
-    local style = API.STYLES[args[2]]
-    local map = API.MAPS[game][table.concat(args,' ',3)]
-
-    if not game then return message:reply('invalid game') end
-    if not style then return message:reply('invalid style') end
-    if not map then return message:reply('invalid map') end
-
-    local time = API:GetMapWr(map.ID,style)
-
-    if not time then return message:reply('No time was found') end
-
-    local user = API:GetRobloxInfoFromUserId(time.User)
-    local username = user.name
-
-    local time_formatted = API.FormatTime(time.Time)
-    local date = os.date("%x", time.Date)
-    local count = tonumber(API:GetMapCompletionCount(time.Map, style))
-    local points = tostring(API.CalculatePoint(1, count))
-
-    -- Username:           | Time:     | Points:      | Date:
-    local n_n,t_n,p_n = 20,#time_formatted,#points
-    
-    local first_line = 'WR Time for map: '..map.DisplayName..' ( 1/'..count..' ) ['..API.GAMES[game]..', '..API.STYLES_LIST[style]..']'
-
-    local second_line = pad('Username:', n_n + 1) .. '| '
-                    .. pad('Time:', t_n + 1) .. '| '
-                    .. pad('Points:',p_n + 1).. '| '
-                    .. 'Date:'
-
-    local third_line = pad(username, n_n + 1) .. '| '
-                     .. pad(time_formatted, t_n + 1) .. '| '
-                     .. pad(points, p_n + 1) .. '| '
-                     .. date
-
-    return message:reply('```' .. first_line .. '\n' .. second_line .. '\n' .. third_line .. '```')
-end)
\ No newline at end of file

From 319c00f90b2374a34f80e628017cce77e2b2cef6 Mon Sep 17 00:00:00 2001
From: tommy <thefamousdoge@hotmail.com>
Date: Wed, 10 Jul 2024 15:52:08 -0400
Subject: [PATCH 3/4] Create new command loader and publisher (manager)

---
 src/modules/CommandCollector.lua | 73 ++++++++++++++++++++++++++++++++
 1 file changed, 73 insertions(+)
 create mode 100644 src/modules/CommandCollector.lua

diff --git a/src/modules/CommandCollector.lua b/src/modules/CommandCollector.lua
new file mode 100644
index 0000000..b80550c
--- /dev/null
+++ b/src/modules/CommandCollector.lua
@@ -0,0 +1,73 @@
+local RELATIVE_PATH_TO_COMMANDS = '../' --this is because the require function will call to a path relative to this current file :D
+local IGNORE_STARTING_FILE_NAME = '_'
+
+local CommandCollector = {}
+CommandCollector.__index = CommandCollector
+
+function CommandCollector.new(Prefix)
+	local self = setmetatable({}, CommandCollector)
+
+	self.Prefix = Prefix
+	self.Collected = false
+	self.Collection = {}
+
+	return self
+end
+
+function CommandCollector:Get(CommandName)
+	for CommandIndex, CommandData in next, self.Collection do
+		if CommandName == CommandData.Command.name then
+			return CommandData
+		end
+	end
+end
+
+function CommandCollector:Collect()
+	if self.Collected then
+		print('Command collector for', self.Prefix, 'commands was already collected')
+		return
+	end
+
+	local CommandsContainerPath = self.Prefix..'Commands/'
+
+	for File in io.popen('dir "./src/'..CommandsContainerPath..'" /b'):lines() do
+		if File:sub(1, 1) ~= IGNORE_STARTING_FILE_NAME then
+			local Success, Return = pcall(require, RELATIVE_PATH_TO_COMMANDS..CommandsContainerPath..File)
+			if Success then
+				if not Return.Command or not Return.Callback then
+					print('Malformed command data in', CommandsContainerPath..File, 'Reason: returned command data table is missing a Command or Callback field')
+					return
+				end
+				print('Loaded', CommandsContainerPath..File)
+				table.insert(self.Collection, {Command = Return.Command, Callback = Return.Callback})
+			else
+				print('Error loading', CommandsContainerPath..File, 'Error:', Return)
+			end
+		end
+	end
+
+	print('Loaded a total of '..#self.Collection..' '..self.Prefix..' command'..(#self.Collection ~= 1 and 's' or ''))
+
+	self.Collected = true
+	return self
+end
+
+function CommandCollector:Publish(Client)
+	if not Client.createGlobalApplicationCommand then
+		print('Client does not have the method \'createGlobalApplicationCommand\'')
+		return
+	end
+
+	for CommandIndex, CommandData in next, self.Collection do
+		local Success, Return = pcall(Client.createGlobalApplicationCommand, Client, CommandData.Command)
+		if Success then
+			print('Published command', CommandData.Command.name)
+		else
+			print('Failed to publish command', CommandData.Command.name, 'Error:', Return)
+		end
+	end
+
+	return self
+end
+
+return CommandCollector
\ No newline at end of file

From dc90da36dff69c0cac7df045f506cfc7b3b4ed74 Mon Sep 17 00:00:00 2001
From: tommy <thefamousdoge@hotmail.com>
Date: Wed, 10 Jul 2024 15:53:13 -0400
Subject: [PATCH 4/4] Make new commands and examples

---
 src/MessageCommands/_Example.lua       |  15 +++
 src/SlashCommands/Minecraft.lua        | 139 ++++++++++++++++++++
 src/SlashCommands/User.lua             | 175 +++++++++++++++++++++++++
 src/SlashCommands/_Example.lua         |  17 +++
 src/UserCommands/GetProfilePicture.lua |  15 +++
 5 files changed, 361 insertions(+)
 create mode 100644 src/MessageCommands/_Example.lua
 create mode 100644 src/SlashCommands/Minecraft.lua
 create mode 100644 src/SlashCommands/User.lua
 create mode 100644 src/SlashCommands/_Example.lua
 create mode 100644 src/UserCommands/GetProfilePicture.lua

diff --git a/src/MessageCommands/_Example.lua b/src/MessageCommands/_Example.lua
new file mode 100644
index 0000000..0c51af1
--- /dev/null
+++ b/src/MessageCommands/_Example.lua
@@ -0,0 +1,15 @@
+local SlashCommandTools = require('discordia-slash').util.tools()
+
+local GetProfilePictureCommand = SlashCommandTools.messageCommand('Copy message', 'Says the same exact message (text only)')
+
+local function Callback(Interaction, Command, Message)
+	local MessageContent = Message.content
+	if MessageContent then
+		return Interaction:reply(MessageContent)
+	end
+end
+
+return {
+	Command = GetProfilePictureCommand,
+	Callback = Callback
+}
\ No newline at end of file
diff --git a/src/SlashCommands/Minecraft.lua b/src/SlashCommands/Minecraft.lua
new file mode 100644
index 0000000..08910b8
--- /dev/null
+++ b/src/SlashCommands/Minecraft.lua
@@ -0,0 +1,139 @@
+local Discordia = require('discordia')
+local json = require('json')
+local http_request = require('../Modules/http.lua')
+Discordia.extensions()
+
+local ApplicationCommandOptionTypes = Discordia.enums.appCommandOptionType
+
+local SlashCommandTools = require('discordia-slash').util.tools()
+
+local MinecraftMainCommand = SlashCommandTools.slashCommand('minecraft', 'Minecraft server related commands')
+
+local MinecraftStatusSubCommand = SlashCommandTools.subCommand('status', '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 MinecraftSetIpOptions = SlashCommandTools.string('ip', 'The IP address of the server')
+MinecraftSetIpOptions:setRequired(true)
+MinecraftSetIpSubCommand:addOption(MinecraftSetIpOptions)
+
+MinecraftMainCommand:addOption(MinecraftSetIpSubCommand)
+MinecraftMainCommand:addOption(MinecraftStatusSubCommand)
+
+local COLOURS = {
+	GREEN = 0x00ff00,
+	RED = 0xff0000
+}
+
+--initialize minecraft ip data
+local MinecraftDataFile = io.open('minecraft_data.json', 'r')
+if not MinecraftDataFile or (MinecraftDataFile and MinecraftDataFile:read('*a') == '') then
+	print('no such file exists! so make it')
+	io.open('minecraft_data.json', 'w+'):write(json.encode({})):close()
+end
+if MinecraftDataFile then
+	MinecraftDataFile:close()
+end
+
+local SubCommandCallbacks = {}
+local function Status(Interaction, Command, Args)
+	local GuildId = Interaction.guild and Interaction.guild.id
+	if not GuildId then
+		return Interaction:reply('You cannot use this command outside of a Discord server', true)
+	end
+
+	local GlobalMinecraftData = json.decode(io.open('minecraft_data.json', 'r'):read('*a'))
+	if not GlobalMinecraftData then
+		return Interaction:reply('Could not read server data', true)
+	end
+
+	local ServerMinecraftData = GlobalMinecraftData[GuildId]
+	if not ServerMinecraftData then
+		return Interaction:reply('There is no data for this Discord server', true)
+	end
+
+	local ServerIPStr = ServerMinecraftData.IP..':'..ServerMinecraftData.PORT
+	local Response, Headers = http_request('GET', ('https://api.mcsrvstat.us/3/%s'):format(ServerIPStr))
+
+	local IsOnline = Response.online
+	local EmbedData
+	if IsOnline then
+		local MaxPlayers = Response.players.max
+		local OnlinePlayers = Response.players.online
+		local AnonymousPlayers = OnlinePlayers
+		local Players = {}
+		if OnlinePlayers>0 then
+			for PlayerIndex, PlayerData in next, Response.players.list do
+				table.insert(Players, PlayerData.name)
+				AnonymousPlayers = AnonymousPlayers-1
+			end
+		else
+			table.insert(Players, 'No players online')
+		end
+		if AnonymousPlayers>0 then
+			for AnonymousPlayerIndex = 1, AnonymousPlayers do
+				table.insert(Players, 'Anonymous Player')
+			end
+		end
+		EmbedData = {
+	        title = 'Server Status for '..ServerIPStr,
+	        description = Response.motd.clean[1]..' ('..Response.version..')',
+	        fields = {
+	        	{name = 'Players', value = OnlinePlayers..'/'..MaxPlayers, inline = true},
+	        	{name = 'List of players', value = table.concat(Players, '\n'), inline = true}
+	        },
+	        color = COLOURS.GREEN
+	    }
+	else
+		EmbedData = {
+			title = 'Server Status for '..ServerIPStr,
+			description = 'Server is offline',
+			color = COLOURS.RED
+		}
+	end
+	return Interaction:reply({embed = EmbedData})
+end
+
+local function SetIp(Interaction, Command, Args)
+	local ServerIPStr = Args.ip
+
+	local GuildId = Interaction.guild and Interaction.guild.id
+	if not GuildId then
+		return Interaction:reply('You cannot use this command outside of a Discord server')
+	end
+
+	local ServerIP = ServerIPStr:match("(%d+%.%d+%.%d+%.%d+)") or ServerIPStr:match("(%w*%.?%w+%.%w+)")
+	if not ServerIP then
+		return Interaction:reply('Invalid server IP')
+	end
+	local ServerPort = ServerIPStr:match(ServerIP..':(%d+)') or 25565
+
+	local GuildMinecraftData = {IP = ServerIP, PORT = ServerPort}
+
+	local GlobalMinecraftData = json.decode(io.open('minecraft_data.json','r'):read('*a'))
+	GlobalMinecraftData[GuildId] = GuildMinecraftData
+	io.open('minecraft_data.json','w+'):write(json.encode(GlobalMinecraftData)):close()
+
+	return Interaction:reply('Successfully added `'..ServerIP..':'..ServerPort..'` for ServerId='..GuildId)
+end
+SubCommandCallbacks.status = Status
+SubCommandCallbacks.setip = SetIp
+
+
+
+local function Callback(Interaction, Command, Args)
+	local SubCommandOption = Command.options[1]
+	if SubCommandOption.type == ApplicationCommandOptionTypes.subCommand then
+		local SubCommandName = SubCommandOption.name
+		local SubCommandCallback = SubCommandCallbacks[SubCommandName]
+		local SubCommandArgs = Args[SubCommandName]
+		if SubCommandCallback then
+			SubCommandCallback(Interaction, Command, SubCommandArgs)
+		end
+	end
+
+end
+
+return {
+	Command = MinecraftMainCommand,
+	Callback = Callback
+}
\ No newline at end of file
diff --git a/src/SlashCommands/User.lua b/src/SlashCommands/User.lua
new file mode 100644
index 0000000..7f4081b
--- /dev/null
+++ b/src/SlashCommands/User.lua
@@ -0,0 +1,175 @@
+local SlashCommandTools = require('discordia-slash').util.tools()
+
+local Discordia = require('discordia')
+local Date = Discordia.Date
+Discordia.extensions()
+
+local API = require('../Modules/strafes_net.lua')
+
+local UserCommand = SlashCommandTools.slashCommand('user', 'Looks up specified user on Roblox')
+
+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')
+
+UserCommand:addOption(UsernameOption)
+UserCommand:addOption(UserIdOption)
+UserCommand:addOption(MemberOption)
+
+Badges = {
+    '275640532', --Bhop, pre-group
+    '363928432', --Surf, pre-group
+    '2124614454', --Bhop, post-group
+    '2124615096', --Surf, post-group
+}
+BadgesToName = {
+    [275640532]='old bhop',
+    [363928432]='old surf',
+    [2124614454]='new bhop',
+    [2124615096]='new surf',
+}
+local function round(x,n)
+    return string.format('%.'..(n or 0)..'f',x)
+end
+
+local function FromYMD(ymd)
+    return Date.fromISO(ymd.."T00:00:00")[1]
+end
+local function leftpad(s,n,p)
+    return string.rep(p,n-#tostring(s))..s
+end
+local function ToYMD(seconds)
+    return "<t:"..seconds..":R>"
+end
+local IDToDate = { --Terrible ranges but it's all we have
+    -- {1000000000, FromYMD("2006-01-01")}, --I guess?
+    -- {1864564055, FromYMD("2008-08-04")},
+    {3800920136, FromYMD("2016-04-16")},
+    {9855616205, FromYMD("2017-04-02")},
+    {30361018662, FromYMD("2018-11-14")},
+    {32665806459, FromYMD("2019-01-07")},
+    {34758058773, FromYMD("2019-02-24")},
+    {65918261258, FromYMD("2020-06-05")},
+    {171994717435, FromYMD("2023-03-24")},
+    {173210319088, FromYMD("2023-04-14")},
+    {206368884641, FromYMD("2023-07-16")},
+    {229093879745, FromYMD("2024-01-02")},
+    {232802028144, FromYMD("2024-04-08")},
+    {234886704167, FromYMD("2024-06-28")}
+}
+--We assume linear interpolation since anything more complex I can't process
+local function linterp(i1, i2, m)
+    return math.floor(i1 + (i2-i1)*m)
+end
+local function GuessDateFromAssetID(AssetID)
+    for i = #IDToDate, 1, -1 do --Newest to oldest
+        local ID,Time = unpack(IDToDate[i])
+        if ID < AssetID then
+            if not IDToDate[i+1] then
+                return "After "..ToYMD(Time)
+            end
+            local ParentID, ParentTime = unpack(IDToDate[i+1])
+            return "Around "..ToYMD(linterp(Time, ParentTime, (AssetID-ID)/(ParentID-ID)))
+        end
+    end
+    return "Before "..ToYMD(IDToDate[1][2])
+end
+
+local function Callback(Interaction, Command, Args)
+	local user_info
+	if Args then
+		local username = Args.username
+		local user_id = Args.user_id
+		local member = Args.member
+		if username then
+			user_info = API:GetRobloxInfoFromUsername(username)
+		elseif user_id then
+			user_info = API:GetRobloxInfoFromUserId(user_id)
+		elseif member then
+			user_info = API:GetRobloxInfoFromDiscordId(member.id)
+		end
+	else
+		local user = Interaction.member or Interaction.user
+		if user then
+			user_info = API:GetRobloxInfoFromDiscordId(user.id)
+		end
+	end
+
+    local description = user_info.description=='' and 'This user has no description' or user_info.description
+    -- table.foreach(user_info,print)
+    local created = tostring(Date.fromISO(user_info.created):toSeconds())
+    local current = Date():toSeconds()
+    local accountAge = round((current-created)/86400)
+    local isBanned = user_info.isBanned
+    local id = user_info.id
+    local name = user_info.name
+    local displayName = user_info.displayName
+
+    local usernameHistory = API:GetUserUsernameHistory(id).data or {}
+    local usernameHistoryTable = {}
+    for index,usernameObj in next,usernameHistory do
+        table.insert(usernameHistoryTable,usernameObj.name)
+    end
+    local usernameHistoryString = table.concat(usernameHistoryTable,', ')
+
+    local onlineStatus_info = API:GetUserOnlineStatus(id) or {lastLocation="Unknown", lastOnline=0, userPresenceType=-1}
+    -- table.foreach(onlineStatus_info,print)
+
+    local LastLocation = onlineStatus_info.lastLocation
+    if onlineStatus_info.userPresenceType==2 then LastLocation="Ingame" end
+    local LastOnline = Date.fromISO(onlineStatus_info.lastOnline):toSeconds()
+
+    local verificationAssetId = API:GetVerificationItemID(id)
+    local verificationDate = "Not verified"
+    if verificationAssetId.errors then
+        verificationDate = "Failed to fetch"
+    elseif verificationAssetId.data[1] then
+        verificationDate = GuessDateFromAssetID(verificationAssetId.data[1].instanceId)
+    end
+
+    local badgeRequest = API:GetBadgesAwardedDates(id,Badges)
+    local badgeData = badgeRequest.data
+
+    -- local badgesDates = {}
+
+    local firstBadge,firstBadgeDate = 0,math.huge
+    for _,badge in next,badgeData do
+        local badgeId = badge.badgeId
+        local awardedDate = tonumber(Date.fromISO(badge.awardedDate):toSeconds())
+        if firstBadgeDate>awardedDate then
+            firstBadge=badgeId
+            firstBadgeDate=awardedDate
+        end
+        -- badgesDates[badgeId]=awardedDate
+    end
+    local userThumbnail = API:GetUserThumbnail(id).data[1]
+
+    local embed = {
+        title = displayName..' (@'..name..')',
+        url = 'https://roblox.com/users/'..id..'/profile',
+        thumbnail = {
+            url = userThumbnail.imageUrl,
+        },
+        fields = {
+            {name='ID',value=id,inline=true},
+            {name='Account Age',value=accountAge..' days',inline=true},
+            {name='Created',value='<t:'..round(created)..':R>',inline=true},
+            {name='Verified Email',value=verificationDate,inline=true},
+            {name='Last Online',value='<t:'..round(LastOnline)..':R>',inline=true},
+            {name='Last Location',value=LastLocation,inline=true},
+            {name='Banned',value=isBanned,inline=true},
+            {name='Description',value=description,inline=false},
+            {name='Username History ('..#usernameHistoryTable..(#usernameHistoryTable==50 and '*' or '')..')',value=usernameHistoryString,inline=false},
+        }
+    }
+    if firstBadge and firstBadgeDate~=math.huge then
+        table.insert(embed.fields,{name='FQG',value=BadgesToName[firstBadge],inline=true})
+        table.insert(embed.fields,{name='Joined',value='<t:'..round(firstBadgeDate)..':R>',inline=true})
+    end
+    Interaction:reply({embed=embed})
+end
+
+return {
+	Command = UserCommand,
+	Callback = Callback
+}
\ No newline at end of file
diff --git a/src/SlashCommands/_Example.lua b/src/SlashCommands/_Example.lua
new file mode 100644
index 0000000..0732a66
--- /dev/null
+++ b/src/SlashCommands/_Example.lua
@@ -0,0 +1,17 @@
+local SlashCommandTools = require('discordia-slash').util.tools()
+
+local PongCommand = SlashCommandTools.slashCommand('ping', 'Replies with pong')
+
+local MessageOption = SlashCommandTools.string('message', 'What the bot will append to the message')
+
+PongCommand:addOption(MessageOption)
+
+local function Callback(Interaction, Command, Args)
+	local Message = Args.message
+	return Interaction:reply('Pong! '..Message)
+end
+
+return {
+	Command = PongCommand,
+	Callback = Callback
+}
\ No newline at end of file
diff --git a/src/UserCommands/GetProfilePicture.lua b/src/UserCommands/GetProfilePicture.lua
new file mode 100644
index 0000000..0898ed1
--- /dev/null
+++ b/src/UserCommands/GetProfilePicture.lua
@@ -0,0 +1,15 @@
+local SlashCommandTools = require('discordia-slash').util.tools()
+
+local GetProfilePictureCommand = SlashCommandTools.userCommand('Get profile picture', 'Gets user avatar')
+
+local function Callback(Interaction, Command, Member)
+	local AvatarURL = Member:getAvatarURL(1024)
+	if AvatarURL then
+		return Interaction:reply(AvatarURL, true)
+	end
+end
+
+return {
+	Command = GetProfilePictureCommand,
+	Callback = Callback
+}
\ No newline at end of file