discord-bot-game/Bot/InviteTracker.fs
2022-03-28 11:54:58 +07:00

542 lines
22 KiB
Forth
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
let connStr = GuildEnvironment.connectionString
let InviteRewardAmount = 100<GBT>
let WhitelistPrice = 1000
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
"""
|> 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 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 private 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 created_at DESC LIMIT 10
"""
|> Sql.executeAsync (fun read -> read.string "discord_id" |> uint64)
|> Async.AwaitTask
let guildInviteEmbed =
let rewardMsg =
$"**Your Mission:**\nCLICK THE BUTTON below, then share your **UNIQUE LINK**\nwith any Degenz you want to invite into the Server.\n\n" +
$"**Your Reward:**\n`Earn {InviteRewardAmount} $GBT` 💰 for every Degen you invite into\nthe server, the **COMPLETES** hacker training.\n\n" +
$"**Commands:**\n`/recruit` - Invite Degenz into the server.\n`/recruited` - Check how many Degenz you've invited."
let embed =
DiscordEmbedBuilder()
.WithColor(DiscordColor.Green)
.WithDescription(rewardMsg)
.WithImageUrl("https://s1.gifyu.com/images/whitelist-image-banner-3.gif")
.WithTitle("Recruitment")
let builder =
DiscordFollowupMessageBuilder()
.AddEmbed(embed)
.AsEphemeral(true)
let button = DiscordButtonComponent(ButtonStyle.Success, $"CreateGuildInvite", $"Give Me the Link") :> DiscordComponent
builder.AddComponents [| button |]
let private showInviteMessage (ctx : IDiscordContext) =
task {
let builder = DiscordInteractionResponseBuilder().AsEphemeral(true)
do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder)
match! DbService.tryFindPlayer (ctx.GetDiscordMember().Id) with
| Some player ->
let ( result , hackerRole ) = ctx.GetGuild().Roles.TryGetValue(GuildEnvironment.roleHacker)
match player.Active , result && Seq.contains hackerRole (ctx.GetDiscordMember().Roles) with
| true , true -> do! ctx.FollowUp(guildInviteEmbed)
| false , _ -> do! sendFollowUpMessage ctx $"You're not in the game! Go to <#{GuildEnvironment.channelShelters}> NOW to get assigned a private bunk, and **JOIN THE GAME!**"
| _ , false ->
do! sendFollowUpMessage ctx $"""
⚠️ Only Degen Hackers can `/recuit` others to the Degenz Army.
You must **COMPLETE YOUR TRAINING FIRST!** Then you can `/recruit`...
Go to {GuildEnvironment.channelTraining} now to become a **HACKER**!
"""
| None ->
do! sendFollowUpMessage ctx $"You're not in the game! Go to <#{GuildEnvironment.channelShelters}> NOW to get assigned a private bunk, and **JOIN THE GAME!**"
} :> Task
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 private getAttributions (ctx : IDiscordContext) userId = task {
let! total = getInviteAttributions(userId)
let msg =
DiscordInteractionResponseBuilder()
.AsEphemeral(true)
.WithContent($"<@{userId}> has invited {total} people")
do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg)
}
let private getInvitedUsersForId (ctx : IDiscordContext) = task {
let! users = getInvitedUsers (ctx.GetDiscordMember().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:** `{users.Length} Degenz`\n**Total Earned:** `{users.Length * InviteRewardAmount} 💰$GBT`\n\n**Last 10 users recruited:**\n{sb}"
else
$"You haven't recruited anyone yet, use the `/recruit` command to get the recruitment link"
DiscordInteractionResponseBuilder()
.AsEphemeral(true)
.WithContent(str)
do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg)
}
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
| None -> ()
} :> Task
let acceptInvite (ctx : IDiscordContext) (invitedPlayer : PlayerData) =
task {
match! checkInviteAccepted invitedPlayer.DiscordId with
| false ->
let! _ = markInvitedAccepted invitedPlayer.DiscordId |> Async.Ignore
try
let! inviter = getInviteFromInvitedUser invitedPlayer.DiscordId
let! player = DbService.tryFindPlayer inviter.Inviter
match player with
| Some player ->
do! DbService.updatePlayerCurrency (int InviteRewardAmount) player |> Async.Ignore
let builder = DiscordMessageBuilder()
builder.WithContent($"{invitedPlayer.Name} was recruited to the server. <@{player.DiscordId}> just earned {InviteRewardAmount} 💰$GBT for their efforts!") |> ignore
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
| None -> return ()
with _ -> ()
| true -> return ()
} :> Task
// If we do it like this then there's an obvious exploit where the user can come and go as many times and it will keep
// rewarding GBT.
//let handleGuildMemberRemoved _ (eventArgs : GuildMemberRemoveEventArgs) =
// task {
// do! removeInvitedUser eventArgs.Member.Id
// } :> Task
//Degenz Game
//Mint Date: April 2022
//Supply: 3,333
//Price: 1.984 $SOL
//Your NFT will be your In-Game Character that provides you with unique traits, and abilities in game.
let sendInitialEmbed (client : DiscordClient) =
async {
try
let! channel = client.GetChannelAsync(GuildEnvironment.channelWhitelist) |> Async.AwaitTask
let builder = DiscordMessageBuilder()
let embed = DiscordEmbedBuilder()
embed.ImageUrl <- "https://s1.gifyu.com/images/whitelist-image-2.gif"
embed.Title <- "Degenz Game"
embed.Description <- """
Mint Date: **April 2022**
Supply: **3,333**
Price: **1.984 $SOL**
Your NFT will be your In-Game Character that provides you with unique traits, and abilities in game.
"""
builder.AddEmbed embed |> ignore
let button = DiscordButtonComponent(ButtonStyle.Success, $"GimmeWhitelist", $"Give Me Whitelist") :> DiscordComponent
builder.AddComponents [| button |] |> ignore
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
with e ->
printfn $"Error trying to get channel {GuildEnvironment.channelTraining}\n\n{e.Message}"
} |> Async.RunSynchronously
type WhitelistResult =
| NotInGame
| NotEnoughGBT of currentAmount : int
| Granted of PlayerData
| AlreadyWhitelisted
let tryGrantWhitelist (ctx : IDiscordContext) =
task {
let user = ctx.GetDiscordMember()
match! DbService.tryFindPlayer user.Id with
| Some player ->
let role = ctx.GetGuild().GetRole(GuildEnvironment.roleWhitelist)
if Seq.contains role user.Roles then
return AlreadyWhitelisted
elif player.Active then
if int player.Bank >= WhitelistPrice then
return Granted player
else
return NotEnoughGBT (int player.Bank)
else
return NotInGame
| None -> return NotInGame
}
let handleGimmeWhitelist (ctx : IDiscordContext) =
task {
let builder = DiscordInteractionResponseBuilder().AsEphemeral(true)
do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder)
let whitelistEmbed = DiscordEmbedBuilder()
let thumb = DiscordEmbedBuilder.EmbedThumbnail()
thumb.Url <- "https://s7.gifyu.com/images/Card-5x-rotate.png"
whitelistEmbed.Thumbnail <- thumb
whitelistEmbed.Title <- "1x Degenz Game Whitelist"
whitelistEmbed.AddField("Mint April 2022 | ", "Join us on the moon", true) |> ignore
whitelistEmbed.AddField("Price 💰", $"{WhitelistPrice} $GBT", true) |> ignore
let builder = DiscordFollowupMessageBuilder().AsEphemeral(true)
match! tryGrantWhitelist ctx with
| NotInGame ->
builder.Content <- $"""
Woah slow down buddy… Youre not even in the game yet!
To get Whitelisted you need to buy it with **$GBT** by playing the game.
Go to <#{GuildEnvironment.channelShelters}> NOW to get assigned a private bunk, and **JOIN THE GAME!**
"""
| AlreadyWhitelisted ->
let buyBtn = DiscordButtonComponent(ButtonStyle.Success, $"BuyWhitelist", $"Buy Now", true) :> DiscordComponent
let recruitBtn = DiscordButtonComponent(ButtonStyle.Danger, $"ShowRecruitmentEmbed", $"Recruit Now") :> DiscordComponent
builder.AddComponents([ buyBtn ; recruitBtn ]) |> ignore
builder.Content <- $"""
Youre **ALREADY** Whitelisted! Save some for other Degenz…
**Remember:**
Earn `{InviteRewardAmount} $GBT` 💰 for every Degen you `/recruit` into the game!
**Commands:**
`/recruit` - Invite Degenz into the server.
`/recruited` - Check how many Degenz youve invited.
"""
| NotEnoughGBT total ->
whitelistEmbed.Color <- DiscordColor.Red
let buyBtn = DiscordButtonComponent(ButtonStyle.Success, $"BuyWhitelist", $"Buy Now", true) :> DiscordComponent
let recruitBtn = DiscordButtonComponent(ButtonStyle.Danger, $"ShowRecruitmentEmbed", $"Recruit Now") :> DiscordComponent
let msgEmbed = DiscordEmbedBuilder()
msgEmbed.Color <- DiscordColor.Red
msgEmbed.Description <- $"""
Oh no! You don't have enough **$GBT** to buy a WHITELIST spot!
Come back when you have `{WhitelistPrice - total}` more $GBT.
The quickest way to earn $GBT is to `/recruit`.
You WILL earn `{InviteRewardAmount} 💰 $GBT` for every Degen you `/recruit`!
"""
builder.AddComponents([ buyBtn ; recruitBtn ]) |> ignore
builder.AddEmbed(msgEmbed) |> ignore
builder.AddEmbed(whitelistEmbed) |> ignore
| Granted _ ->
whitelistEmbed.Color <- DiscordColor.Green
builder.AddComponents([ DiscordButtonComponent(ButtonStyle.Success, $"BuyWhitelist", $"Buy Now") :> DiscordComponent ]) |> ignore
let msgEmbed = DiscordEmbedBuilder()
msgEmbed.Title <- "Buy Whitelist"
msgEmbed.Color <- DiscordColor.Green
msgEmbed.Description <- $"""
Look at you Degen, you played Big Brothers games and made it out alive!
Now you can use your $GBT to pay for one of our coveted Whitelist spots.
Click buy now below and the role will be auto assigned to you.
"""
builder.AddEmbed(msgEmbed) |> ignore
builder.AddEmbed(whitelistEmbed) |> ignore
do! ctx.FollowUp(builder)
} :> Task
let handleBuyWhitelist (ctx : IDiscordContext) =
task {
let builder = DiscordInteractionResponseBuilder().AsEphemeral(true)
do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder)
let builder = DiscordFollowupMessageBuilder().AsEphemeral(true)
match! tryGrantWhitelist ctx with
| NotInGame -> builder.Content <- $"You somehow have left the game, what exactly are you doing?"
| AlreadyWhitelisted ->
builder.Content <- $"""
🎉 You're already WHITELISTED!
Come hang with all the other VIP Degenz in the <#{GuildEnvironment.channelElite}>
"""
| NotEnoughGBT _ -> builder.Content <- $"You somehow do not have enough $GBT, what exactly are you doing?"
| Granted player ->
let role = ctx.GetGuild().GetRole(GuildEnvironment.roleWhitelist)
do! ctx.GetDiscordMember().GrantRoleAsync(role)
let! _ = DbService.updatePlayerCurrency -WhitelistPrice player
builder.Content <- $"""
🎉 Congratulations youve been **WHITELISTED**!
Now type `/recruit` and share **YOUR UNIQUE LINK** with any Degenz
you want to invite into the game, and let them earn whitelist too!
**Remember:**
Earn `100 $GBT` 💰 for every Degen you `/recruit` into the game!
**Commands:**
`/recruit` - Invite Degenz into the server.
`/recruited` - Check how many Degenz youve invited.
"""
do! ctx.FollowUp(builder)
} :> Task
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 channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome]
let! invite = channel.CreateInviteAsync(max_age = 0, unique = true)
try do! createInvite (ctx.GetDiscordMember().Id) invite.Code |> Async.Ignore
with ex -> printfn "%A" ex.Message
return invite.Code
| invite::_ ->
return invite |> fst
}
let msg =
DiscordFollowupMessageBuilder()
.WithContent($"https://discord.gg/{code}")
.AsEphemeral(true)
do! ctx.FollowUp(msg)
} :> Task
let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) =
let eventCtx = DiscordEventContext event :> IDiscordContext
match event.Id with
| id when id.StartsWith("GimmeWhitelist") -> handleGimmeWhitelist eventCtx
| id when id.StartsWith("BuyWhitelist") -> handleBuyWhitelist eventCtx
| id when id.StartsWith("CreateGuildInvite") -> handleCreateInvite eventCtx
| id when id.StartsWith("ShowRecruitmentEmbed") -> showInviteMessage eventCtx
| _ ->
task {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Incorrect Action identifier {eventCtx.GetInteractionId()}"
do! eventCtx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask
}
let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) =
task {
let! exists = checkUserAlreadyInvited eventArgs.Member.Id
if not exists then
do! processNewUser eventArgs
} :> Task
type Inviter() =
inherit ApplicationCommandModule ()
[<SlashCommand("recruit", "Recruit another user to this discord and earn rewards")>]
member this.CreateInvite (ctx : InteractionContext) =
showInviteMessage (DiscordInteractionContext ctx)
[<SlashCommand("recruited", "Get total invites from a specific user")>]
member this.ListInvitedPeople (ctx : InteractionContext) =
getInvitedUsersForId (DiscordInteractionContext ctx)
// [<SlashCommand("invites-list", "List all the invites")>]
member this.ListServerInvites (ctx : InteractionContext) =
listServerInvites (DiscordInteractionContext ctx)
// [<SlashCommand("invites-attributions", "Get total invites from a specific user")>]
member this.getAttributions (ctx : InteractionContext, [<Option("player", "The player you want to check")>] user : DiscordUser) =
getAttributions (DiscordInteractionContext ctx) user.Id
// [<SlashCommand("invites-clear", "Get total invites from a specific user")>]
member this.ClearInvites (ctx : InteractionContext) =
clearInvites (DiscordInteractionContext ctx)