521 lines
21 KiB
Forth
521 lines
21 KiB
Forth
module Degenz.InviteTracker
|
|
|
|
open System.Text
|
|
open System.Threading.Tasks
|
|
open DSharpPlus
|
|
open DSharpPlus.Entities
|
|
open DSharpPlus.EventArgs
|
|
open DSharpPlus.SlashCommands
|
|
open Degenz.Messaging
|
|
open Npgsql.FSharp
|
|
open Solnet.Wallet
|
|
|
|
let connStr = GuildEnvironment.connectionString
|
|
let InviteRewardAmount = 100<GBT>
|
|
let InviteLinkButtonText = "Get My Invite Link"
|
|
|
|
type Invite = {
|
|
Code : string
|
|
Inviter : uint64
|
|
Count : int
|
|
}
|
|
|
|
let private mapInvite (reader : RowReader) = {
|
|
Code = reader.string "code"
|
|
Inviter = reader.string "inviter" |> uint64
|
|
Count = reader.int "count"
|
|
}
|
|
|
|
let private getInvites () = async {
|
|
let! invites =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.query "SELECT code, inviter, count FROM invite"
|
|
|> Sql.executeAsync mapInvite
|
|
|> Async.AwaitTask
|
|
return
|
|
invites
|
|
|> List.map (fun inv -> (inv.Code , (inv.Inviter , inv.Count)))
|
|
|> Map.ofList
|
|
}
|
|
|
|
let private getInvitesFromUser discordId = async {
|
|
let! invites =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string discordId) ]
|
|
|> Sql.query """
|
|
SELECT code, count FROM invite
|
|
WHERE inviter = @did AND invite.created_at > NOW() at time zone 'utc' - INTERVAL '72 HOURS'
|
|
"""
|
|
|> Sql.executeAsync (fun read -> {
|
|
Code = read.string "code"
|
|
Inviter = discordId
|
|
Count = read.int "count"
|
|
})
|
|
|> Async.AwaitTask
|
|
return
|
|
invites
|
|
|> List.map (fun inv -> (inv.Code , (inv.Inviter , inv.Count)))
|
|
|> Map.ofList
|
|
}
|
|
|
|
let private createInvite inviter code =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "code" , Sql.string code ; "inviter" , Sql.string (string inviter) ]
|
|
|> Sql.query "INSERT INTO invite (code, inviter) VALUES (@code, @inviter)"
|
|
|> Sql.executeNonQueryAsync
|
|
|> Async.AwaitTask
|
|
|
|
let private addInvitedUser did code count =
|
|
try
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.executeTransactionAsync [
|
|
"""
|
|
INSERT INTO invited_user (discord_id, invite_id)
|
|
VALUES (@did, (SELECT id FROM invite WHERE code = @code));
|
|
""" , [ [ "@code" , Sql.string code ; "@did" , Sql.string (string did) ] ]
|
|
"UPDATE invite SET count = @count WHERE code = @code" , [ [ "count" , Sql.int count ; "code" , Sql.string code ] ]
|
|
]
|
|
|> Async.AwaitTask
|
|
|> Async.Ignore
|
|
with _ -> async.Zero ()
|
|
|
|
let private markInvitedAccepted did =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string did) ]
|
|
|> Sql.query "UPDATE invited_user SET accepted = true, updated_at = timezone('utc'::text, now()) WHERE discord_id = @did"
|
|
|> Sql.executeNonQueryAsync
|
|
|> Async.AwaitTask
|
|
|
|
let private getInviteFromInvitedUser invitedUser =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string invitedUser) ]
|
|
|> Sql.query """
|
|
SELECT code, inviter, count FROM invite
|
|
JOIN invited_user iu ON invite.id = iu.invite_id
|
|
WHERE iu.discord_id = @did
|
|
"""
|
|
|> Sql.executeRowAsync mapInvite
|
|
|> Async.AwaitTask
|
|
|
|
let private removeInvitedUser did =
|
|
try
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string did) ]
|
|
|> Sql.query "DELETE FROM invited_user WHERE discord_id = @did"
|
|
|> Sql.executeNonQueryAsync
|
|
|> Async.AwaitTask
|
|
|> Async.Ignore
|
|
with _ -> async.Zero ()
|
|
|
|
let private checkUserAlreadyInvited userId = async {
|
|
let! result =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string userId) ]
|
|
|> Sql.query """
|
|
SELECT id FROM invited_user WHERE discord_id = @did
|
|
"""
|
|
|> Sql.executeAsync (fun read -> read.int "id")
|
|
|> Async.AwaitTask
|
|
|
|
return List.isEmpty result |> not
|
|
}
|
|
|
|
let checkInviteAccepted (userId : uint64) = async {
|
|
try
|
|
let! result =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string userId) ]
|
|
|> Sql.query "SELECT accepted FROM invited_user WHERE discord_id = @did"
|
|
|> Sql.executeRowAsync (fun read -> read.bool "accepted")
|
|
|> Async.AwaitTask
|
|
return result
|
|
with ex ->
|
|
printfn "%s %u" ex.Message userId
|
|
return false
|
|
}
|
|
|
|
let private getInviteAttributions userId =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string userId) ]
|
|
|> Sql.query """
|
|
SELECT count(*) FROM invited_user
|
|
JOIN invite ON invite.id = invited_user.invite_id
|
|
WHERE invite.inviter = @did AND invited_user.accepted = true;
|
|
"""
|
|
|> Sql.executeRowAsync (fun read -> read.int "count")
|
|
|> Async.AwaitTask
|
|
|
|
let getInvitedUsers userId =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string userId) ]
|
|
|> Sql.query """
|
|
WITH invite AS (SELECT id FROM invite WHERE inviter = @did)
|
|
SELECT discord_id FROM invited_user, invite
|
|
WHERE invite.id = invited_user.invite_id AND invited_user.accepted = true
|
|
ORDER BY invited_user.updated_at DESC, invited_user.created_at DESC LIMIT 10
|
|
"""
|
|
|> Sql.executeAsync (fun read -> read.string "discord_id" |> uint64)
|
|
|> Async.AwaitTask
|
|
|
|
let getInvitedUserCount userId =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string userId) ]
|
|
|> Sql.query """
|
|
WITH invite AS (SELECT id FROM invite WHERE inviter = @did)
|
|
SELECT count(*) FROM invited_user, invite
|
|
WHERE invite.id = invited_user.invite_id AND invited_user.accepted = true
|
|
"""
|
|
|> Sql.executeRowAsync (fun read -> read.int "count")
|
|
|> Async.AwaitTask
|
|
|
|
let addWalletAddress (userId : uint64) address =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string userId) ; "address" , Sql.string address ]
|
|
|> Sql.query """
|
|
UPDATE "user" SET wallet_address = @address WHERE id = @did;
|
|
"""
|
|
|> Sql.executeNonQueryAsync
|
|
|> Async.AwaitTask
|
|
|> Async.Ignore
|
|
|
|
let getWalletAddress (userId : uint64) =
|
|
connStr
|
|
|> Sql.connect
|
|
|> Sql.parameters [ "did" , Sql.string (string userId) ]
|
|
|> Sql.query """
|
|
SELECT wallet_address FROM "user" WHERE id = @did;
|
|
"""
|
|
|> Sql.executeRowAsync (fun reader -> reader.stringOrNone "wallet_address")
|
|
|> Async.AwaitTask
|
|
|
|
let private listServerInvites (ctx : IDiscordContext) = task {
|
|
let! invites = ctx.GetGuild().GetInvitesAsync()
|
|
let sb = StringBuilder()
|
|
for invite in invites do
|
|
sb.AppendLine($"{invite.Inviter.Username} - {invite.Code}") |> ignore
|
|
let msg =
|
|
DiscordInteractionResponseBuilder()
|
|
.AsEphemeral(true)
|
|
.WithContent("Server Invites\n" + sb.ToString())
|
|
do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg)
|
|
}
|
|
|
|
let getAttributions userId (ctx : IDiscordContext) =
|
|
task {
|
|
let! total = getInviteAttributions(userId)
|
|
let msg =
|
|
DiscordInteractionResponseBuilder()
|
|
.AsEphemeral(true)
|
|
.WithContent($"<@{userId}> has invited {total} people")
|
|
do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg)
|
|
} :> Task
|
|
|
|
let getInvitedUsersForId (user : DiscordUser) (ctx : IDiscordContext) =
|
|
task {
|
|
do! Messaging.defer ctx
|
|
let! users = getInvitedUsers user.Id
|
|
let! total = getInvitedUserCount user.Id
|
|
let sb = StringBuilder()
|
|
let mutable count = 0
|
|
for user in users do
|
|
count <- count + 1
|
|
sb.AppendLine($"{count}.) <@{user}>") |> ignore
|
|
let msg =
|
|
let str =
|
|
if users.Length > 0 then
|
|
$"**Total Recruited:** `{total} Degenz`\n**Total Earned:** `{total * InviteRewardAmount} 💰$GBT`\n\n**Last 10 users recruited:**\n{sb}"
|
|
else
|
|
$"You haven't recruited anyone yet, click the `{InviteLinkButtonText}` button to get your invite link and start recruiting!"
|
|
DiscordFollowupMessageBuilder()
|
|
.AsEphemeral(true)
|
|
.WithContent(str)
|
|
do! ctx.FollowUp(msg)
|
|
let user = ctx.GetDiscordMember()
|
|
do! Analytics.recruitedButton total user.Id user.Username (ctx.GetChannel())
|
|
} :> Task
|
|
|
|
let clearInvites (ctx : IDiscordContext) = task {
|
|
let! invites = ctx.GetGuild().GetInvitesAsync()
|
|
do!
|
|
invites
|
|
|> Seq.map (fun invite -> invite.DeleteAsync() |> Async.AwaitTask)
|
|
|> Async.Sequential
|
|
|> Async.Ignore
|
|
}
|
|
|
|
// Discord doesn't have any way to tell you if the user came via an invite, the only way to tell is to compare the
|
|
// cached invites in the DB to the ones in the guild and see if any has been incremented
|
|
let private processNewUser (eventArgs : GuildMemberAddEventArgs) =
|
|
task {
|
|
let! guildInvites = eventArgs.Guild.GetInvitesAsync()
|
|
let! cachedInvites = getInvites ()
|
|
for invite in guildInvites do
|
|
let result = cachedInvites.TryFind(invite.Code)
|
|
match result with
|
|
| Some (_,count) ->
|
|
if invite.Uses > count then
|
|
do! addInvitedUser eventArgs.Member.Id invite.Code invite.Uses |> Async.Ignore
|
|
do! Analytics.invitedUserEntered invite.Code invite.Inviter.Id eventArgs.Member.Id invite.Inviter.Username eventArgs.Member.Username
|
|
| None -> ()
|
|
} :> Task
|
|
|
|
let acceptInvite (guild : DiscordGuild) discordId memberName =
|
|
task {
|
|
match! checkInviteAccepted discordId with
|
|
| false ->
|
|
let! _ = markInvitedAccepted discordId |> Async.Ignore
|
|
try
|
|
let! invite = getInviteFromInvitedUser discordId
|
|
let! player = DbService.tryFindPlayer invite.Inviter
|
|
match player with
|
|
| Some player ->
|
|
do! DbService.updatePlayerCurrency InviteRewardAmount player.DiscordId |> Async.Ignore
|
|
do! match GuildEnvironment.botClientRecruit with
|
|
| Some recruitBot -> async {
|
|
let builder = DiscordMessageBuilder()
|
|
builder.WithContent($"{memberName} was recruited and is now a Degen. <@{player.DiscordId}> just earned {InviteRewardAmount} 💰$GBT for their efforts!") |> ignore
|
|
let channel = guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)
|
|
do! recruitBot.SendMessageAsync(channel, builder)
|
|
|> Async.AwaitTask
|
|
|> Async.Ignore
|
|
}
|
|
| None -> async.Return()
|
|
do! Analytics.invitedUserAccepted invite.Code player.DiscordId discordId player.Name memberName
|
|
| None -> return ()
|
|
with _ -> ()
|
|
| true -> return ()
|
|
} :> Task
|
|
|
|
let sendInitialEmbed (ctx : IDiscordContext) =
|
|
async {
|
|
try
|
|
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelRecruitment)
|
|
let rewardMsg = $"""
|
|
**__Win $2,000:__**
|
|
🙋 1 invite = 1 entry everyday*
|
|
🎟 $100 daily raffles till mint
|
|
|
|
**__How To Invite:__**
|
|
1️⃣ Click the green button below
|
|
2️⃣ Share your unique link with Degenz
|
|
|
|
**__Bonus__**
|
|
💰 Earn an extra 100 $GBT for every invite!
|
|
|
|
**Every invite increases your chances of winning*
|
|
"""
|
|
let embed =
|
|
DiscordEmbedBuilder()
|
|
.WithColor(DiscordColor.CornflowerBlue)
|
|
.WithDescription(rewardMsg)
|
|
.WithImageUrl("https://s8.gifyu.com/images/invite-banner-usdc.png")
|
|
.WithTitle("Invite Degenz")
|
|
|
|
let builder = DiscordMessageBuilder().AddEmbed(embed)
|
|
let btn1 = DiscordButtonComponent(ButtonStyle.Success, $"CreateGuildInvite", InviteLinkButtonText) :> DiscordComponent
|
|
let btn2 = DiscordButtonComponent(ButtonStyle.Primary, $"ShowRecruited", $"Check My Recruits") :> DiscordComponent
|
|
builder.AddComponents [| btn1 ; btn2 |] |> ignore
|
|
|
|
do! GuildEnvironment.botClientRecruit.Value.SendMessageAsync(channel, builder)
|
|
|> Async.AwaitTask
|
|
|> Async.Ignore
|
|
with e ->
|
|
printfn $"Error trying to get channel Whitelist\n\n{e.Message}"
|
|
} |> Async.RunSynchronously
|
|
|
|
let showWalletStatus (ctx : IDiscordContext) =
|
|
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
|
try
|
|
match! getWalletAddress player.DiscordId with
|
|
| Some address -> do! Messaging.sendFollowUpMessage ctx $"""
|
|
🚀 __Mint Date:__ 31st May, 18:00 UTC
|
|
✅ __Status:__ We have successfully received your wallet address: {address}"""
|
|
| None -> do! Messaging.sendFollowUpMessage ctx "You have no submitted your wallet yet. Type `/submit`, paste your **Solana Wallet Address**, then press enter"
|
|
with ex ->
|
|
printfn $"{ex.Message}"
|
|
do! Messaging.sendFollowUpMessage ctx "Something went wrong retrieving your wallet address"
|
|
})
|
|
|
|
let sendSubmitEmbed (ctx : IDiscordContext) =
|
|
async {
|
|
try
|
|
let rewardMsg = $"""
|
|
To confirm your **Whitelist** please submit it below:
|
|
|
|
1️⃣ Type `/submit`
|
|
2️⃣ Paste your **Wallet Address**
|
|
3️⃣ Press `Enter`
|
|
|
|
**Check status anytime to double check it worked*"""
|
|
let embed =
|
|
DiscordEmbedBuilder()
|
|
.WithColor(DiscordColor.White)
|
|
.WithDescription(rewardMsg)
|
|
.WithImageUrl("https://s8.gifyu.com/images/whitelist-submit-banner7.png")
|
|
.WithTitle("Submit Your Solana Wallet Address")
|
|
|
|
let builder = DiscordMessageBuilder().AddEmbed(embed)
|
|
let btn = DiscordButtonComponent(ButtonStyle.Success, "WalletStatus", "Check Status") :> DiscordComponent
|
|
builder.AddComponents [| btn |] |> ignore
|
|
|
|
let recruitBot = GuildEnvironment.botClientRecruit.Value
|
|
let! channel = recruitBot.GetChannelAsync(GuildEnvironment.channelSubmitWallet) |> Async.AwaitTask
|
|
let! msgs = channel.GetMessagesAsync() |> Async.AwaitTask
|
|
match msgs |> Seq.tryHead with
|
|
| Some msg ->
|
|
if msg.Author.Id = recruitBot.CurrentUser.Id then
|
|
do! msg.ModifyAsync(builder) |> Async.AwaitTask |> Async.Ignore
|
|
| None ->
|
|
do! recruitBot.SendMessageAsync(channel, builder)
|
|
|> Async.AwaitTask
|
|
|> Async.Ignore
|
|
with e ->
|
|
printfn $"Error trying to get channel Recruit thing\n\n{e.Message}"
|
|
} |> Async.RunSynchronously
|
|
|
|
let handleCreateInvite (ctx : IDiscordContext) =
|
|
task {
|
|
let builder = DiscordInteractionResponseBuilder().AsEphemeral(true)
|
|
do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder)
|
|
|
|
let user = ctx.GetDiscordMember()
|
|
|
|
let! code =
|
|
task {
|
|
let! invites = getInvitesFromUser user.Id
|
|
match invites |> Map.toList with
|
|
| [] ->
|
|
let ( result , channel ) = ctx.GetGuild().Channels.TryGetValue(GuildEnvironment.channelWelcome)
|
|
if result then
|
|
let! invite = channel.CreateInviteAsync(max_age = 259200, unique = true)
|
|
|
|
try do! createInvite (ctx.GetDiscordMember().Id) invite.Code |> Async.Ignore
|
|
with ex -> printfn "%A" ex.Message
|
|
|
|
return invite.Code
|
|
else
|
|
printfn "Error finding Welcome channel"
|
|
return ""
|
|
| invite::_ ->
|
|
return invite |> fst
|
|
}
|
|
|
|
let msg =
|
|
DiscordFollowupMessageBuilder()
|
|
.WithContent($"https://discord.gg/{code}")
|
|
.AsEphemeral(true)
|
|
|
|
do! ctx.FollowUp(msg)
|
|
do! Analytics.recruitLinkButton code user.Id user.Username (ctx.GetChannel())
|
|
} :> Task
|
|
|
|
let handleMemberUpdated (client : DiscordClient) (event : GuildMemberUpdateEventArgs) =
|
|
let addedRole (rolesBefore : DiscordRole seq) (rolesAfter : DiscordRole seq) =
|
|
rolesAfter |> Seq.filter ((fun role -> rolesBefore |> Seq.exists (fun r -> role.Id = r.Id)) >> not)
|
|
task {
|
|
let symmetricDifference = addedRole event.RolesBefore event.RolesAfter |> Seq.toList
|
|
match symmetricDifference with
|
|
| [] -> ()
|
|
| role::_ ->
|
|
if role.Name = "Degen" then
|
|
let (_,guild) = client.Guilds.TryGetValue(GuildEnvironment.guildId)
|
|
do! acceptInvite guild event.Member.Id event.Member.Username |> Async.AwaitTask
|
|
return ()
|
|
} :> Task
|
|
|
|
let handleMessageCreated _ (event : MessageCreateEventArgs) =
|
|
task {
|
|
let bot = GuildEnvironment.botClientRecruit.Value
|
|
if event.Channel.Id = GuildEnvironment.channelSubmitWallet && event.Author.Id <> bot.CurrentUser.Id then
|
|
do! Async.Sleep 100
|
|
do! event.Message.DeleteAsync()
|
|
} :> Task
|
|
|
|
let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) =
|
|
task {
|
|
let! exists = checkUserAlreadyInvited eventArgs.Member.Id
|
|
if not exists then
|
|
do! processNewUser eventArgs
|
|
} :> Task
|
|
|
|
let submitWhitelist (address : string) (ctx : IDiscordContext) =
|
|
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
|
let pubkey = PublicKey(address)
|
|
try
|
|
if pubkey.IsValid() && pubkey.IsOnCurve() then
|
|
let! maybeAddress = getWalletAddress player.DiscordId
|
|
let msg =
|
|
match maybeAddress with
|
|
| Some storedAddress when storedAddress = address -> "You already provided this wallet address:"
|
|
| Some _ -> "We successfully updated your wallet address:"
|
|
| None -> "We have successfully received your wallet address"
|
|
do! addWalletAddress (ctx.GetDiscordMember().Id) address
|
|
do! Messaging.sendFollowUpMessage ctx $"""
|
|
🚀 __Mint Date:__ 31st May 18:00 UTC
|
|
✅ {msg} {address}
|
|
|
|
Keep an eye on <#{GuildEnvironment.channelAnnouncements}> for updates."""
|
|
|
|
let builder = DiscordMessageBuilder()
|
|
builder.WithContent($"{ctx.GetDiscordMember().Username} submitted their wallet address in <#{GuildEnvironment.channelSubmitWallet}> and confirmed whitelist") |> ignore
|
|
let channel = (ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle))
|
|
do! channel.SendMessageAsync(builder)
|
|
|> Async.AwaitTask
|
|
|> Async.Ignore
|
|
do! Analytics.walletSubmit (ctx.GetDiscordMember())
|
|
else
|
|
do! Messaging.sendFollowUpMessage ctx "⚠️ That's not a valid Solana address, please try again"
|
|
do! Analytics.invalidWalletSubmit (ctx.GetDiscordMember())
|
|
with _ ->
|
|
do! Messaging.sendFollowUpMessage ctx "⚠️ That's not a valid Solana address, please try again"
|
|
do! Analytics.invalidWalletSubmit (ctx.GetDiscordMember())
|
|
})
|
|
|
|
type Inviter() =
|
|
inherit ApplicationCommandModule ()
|
|
|
|
let enforceChannel (ctx : IDiscordContext) (fn : IDiscordContext -> Task) =
|
|
match ctx.GetChannel().Id with
|
|
| id when id = GuildEnvironment.channelSubmitWallet -> fn ctx
|
|
| _ ->
|
|
task {
|
|
let msg = $"You must go to <#{GuildEnvironment.channelSubmitWallet}> channel to submit your wallet"
|
|
do! Messaging.sendSimpleResponse ctx msg
|
|
}
|
|
|
|
[<SlashCommand("submit", "Submit your public wallet address")>]
|
|
member this.SubmitAddress (ctx : InteractionContext, [<Option("address", "Wallet address")>] address : string) =
|
|
if ctx.Member.Roles |> Seq.exists (fun role -> role.Id = GuildEnvironment.roleWhitelist || role.Id = GuildEnvironment.roleWhiteOG) then
|
|
enforceChannel (DiscordInteractionContext ctx) (submitWhitelist address)
|
|
else
|
|
let msg = $"You currently are not Whitelisted, go to <#{GuildEnvironment.channelWhitelist}> to purchase the role!"
|
|
Messaging.sendSimpleResponse (DiscordInteractionContext ctx) msg
|
|
|> Async.StartAsTask :> Task
|
|
|
|
// [<SlashCommand("recruited", "Get total invites from a specific user")>]
|
|
// member this.ListInvitedPeople (ctx : InteractionContext) =
|
|
// let ictx = DiscordInteractionContext ctx :> IDiscordContext
|
|
// getInvitedUsersForId (ictx.GetDiscordMember()) ictx
|
|
|
|
// [<SlashCommand("invites-list", "List all the invites")>]
|
|
// member this.ListServerInvites (ctx : InteractionContext) =
|
|
// listServerInvites (DiscordInteractionContext ctx)
|
|
|
|
// [<SlashCommand("invites-clear", "Get total invites from a specific user")>]
|
|
// member this.ClearInvites (ctx : InteractionContext) =
|
|
// clearInvites (DiscordInteractionContext ctx)
|
|
|