discord-bot-game/Bot/InviteTracker.fs

593 lines
24 KiB
Forth
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, 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 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 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 guildInviteEmbed =
let rewardMsg =
$"**Your Mission:**\nCLICK THE BUTTON below, then share your **UNIQUE LINK** with any Degenz you want to invite into the Server.\n\n" +
$"**Your Reward:**\n`Earn {InviteRewardAmount} $GBT` 💰 for every Degen you invite into the server, that **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", $"GET MY UNIQUE LINK") :> DiscordComponent
builder.AddComponents [| button |]
let private showInviteMessage (ctx : IDiscordContext) origin =
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**!
"""
do! Analytics.recruitCommand origin player.DiscordId (ctx.GetDiscordMember().Username) (ctx.GetChannel().Id) (ctx.GetChannel().Name)
| 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! total = getInvitedUserCount (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:** `{total} Degenz`\n**Total Earned:** `{total * 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 user = ctx.GetDiscordMember()
let channel = ctx.GetChannel()
do! Analytics.recruitedCommand total user.Id user.Username channel.Id channel.Name
}
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 (ctx : IDiscordContext) (invitedPlayer : PlayerData) =
task {
match! checkInviteAccepted invitedPlayer.DiscordId with
| false ->
let! _ = markInvitedAccepted invitedPlayer.DiscordId |> Async.Ignore
try
let! invite = getInviteFromInvitedUser invitedPlayer.DiscordId
let! player = DbService.tryFindPlayer invite.Inviter
match player with
| Some player ->
do! DbService.updatePlayerCurrency (int InviteRewardAmount) player |> Async.Ignore
let builder = DiscordMessageBuilder()
builder.WithContent($"{invitedPlayer.Name} was recruited and is now a Degen. <@{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
do! Analytics.invitedUserAccepted invite.Code player.DiscordId invitedPlayer.DiscordId player.Name invitedPlayer.Name
| None -> return ()
with _ -> ()
| true -> return ()
} :> Task
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.Color <- DiscordColor.White
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 Training Dojo\n\n{e.Message}"
} |> Async.RunSynchronously
type WhitelistResult =
| NotInGame
| NotAHacker
| 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
let hackerRole = ctx.GetGuild().GetRole(GuildEnvironment.roleHacker)
if Seq.contains hackerRole user.Roles then
if int player.Bank >= WhitelistPrice then
return Granted player
else
return NotEnoughGBT (int player.Bank)
else
return NotAHacker
else
return NotInGame
| None -> return NotInGame
}
let notAHackerMsg = $"""
Woah slow down buddy… Youre not even a hacker yet! To get Whitelisted you need to buy it with **$GBT** by playing the game.
Go to <#{GuildEnvironment.channelTraining}> NOW to finish training and become a **HACKER**!**
"""
let notInGameMsg = $"""
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 bunk, and **JOIN THE GAME!**
"""
let alreadyWhitelistedMsg = $"""
Youre **ALREADY** Whitelisted! Save some for other Degenz…
**Remember:**
Earn `100 $GBT` 💰 for every Degen you recruit into the game!
Just type `/recruit` anywhere, or press the button below...
**Commands:**
`/recruit` - Invite Degenz into the server.
`/recruited` - Check how many Degenz youve invited.
"""
let notEnoughMoneyMsg total = $"""
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 by recruiting other Degenz into the server.
Earn `{InviteRewardAmount} $GBT` 💰 for every Degen you recruit into the game!
Just type `/recruit` anywhere, anytime... Or just press the button below!
"""
let canBuyWhitelistMsg = $"""
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.
"""
let handleGimmeWhitelist (ctx : IDiscordContext) =
task {
let builder = DiscordInteractionResponseBuilder().AsEphemeral(true)
do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder)
let whitelistEmbed = DiscordEmbedBuilder()
whitelistEmbed.Title <- "1x Degenz Game Whitelist "
let includeInfo () =
whitelistEmbed.ImageUrl <- "https://s7.gifyu.com/images/whitelist-item-mock-banner18.png"
whitelistEmbed.AddField("Item", "1x Whitelist", true) |> ignore
whitelistEmbed.AddField("Available", "750", true) |> ignore
whitelistEmbed.AddField("Price 💰", $"{WhitelistPrice} $GBT", true) |> ignore
whitelistEmbed.Color <- DiscordColor.Red
let buyBtn = DiscordButtonComponent(ButtonStyle.Success, $"BuyWhitelist", $"Buy Now", true) :> DiscordComponent
let buyActiveBtn = DiscordButtonComponent(ButtonStyle.Success, $"BuyWhitelist", $"Buy Now") :> DiscordComponent
let recruitBtn = DiscordButtonComponent(ButtonStyle.Danger, $"ShowRecruitmentEmbed", $"Recruit Now") :> DiscordComponent
let builder = DiscordFollowupMessageBuilder().AsEphemeral(true)
let! availability = tryGrantWhitelist ctx
match availability with
| NotAHacker -> whitelistEmbed.Description <- notAHackerMsg
| NotInGame -> whitelistEmbed.Description <- notInGameMsg
| AlreadyWhitelisted ->
builder.AddComponents([ recruitBtn ]) |> ignore
whitelistEmbed.Color <- DiscordColor.Green
whitelistEmbed.Color <- DiscordColor.Green
whitelistEmbed.Description <- alreadyWhitelistedMsg
| NotEnoughGBT total ->
includeInfo()
builder.AddComponents([ buyBtn ; recruitBtn ]) |> ignore
whitelistEmbed.Description <- notEnoughMoneyMsg total
| Granted _ ->
includeInfo()
whitelistEmbed.Color <- DiscordColor.Green
whitelistEmbed.Color <- DiscordColor.Green
builder.AddComponents([ buyActiveBtn ]) |> ignore
whitelistEmbed.Description <- canBuyWhitelistMsg
builder.AddEmbed(whitelistEmbed) |> ignore
do! ctx.FollowUp(builder)
let availabilityStr =
match availability with
| NotEnoughGBT _ -> "NotEnoughGBT"
| Granted _ -> "Granted"
| _ -> string availability
let user = ctx.GetDiscordMember()
do! Analytics.whiteListButton availabilityStr user.Id user.Username
} :> Task
let buyWhitelistMsg = $"""
🎉 Congratulations youve been **WHITELISTED!**
**Remember:**
Earn `100 $GBT` 💰 for every Degen you recruit into the game!
Just type `/recruit` anywhere, or press the button below...
**Commands:**
`/recruit` - Invite Degenz into the server.
`/recruited` - Check how many Degenz youve invited.
"""
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
| NotAHacker ->
builder.Content <- $"You are somehow not a hacker anymore, what exactly are you doing?"
do! ctx.FollowUp(builder)
| NotInGame ->
builder.Content <- $"You somehow have left the game, what exactly are you doing?"
do! ctx.FollowUp(builder)
| AlreadyWhitelisted ->
builder.Content <- "🎉 You're already WHITELISTED!"
do! ctx.FollowUp(builder)
| NotEnoughGBT _ ->
builder.Content <- $"You somehow do not have enough $GBT, what exactly are you doing?"
do! ctx.FollowUp(builder)
| Granted player ->
let embed = DiscordEmbedBuilder()
embed.Description <- buyWhitelistMsg
embed.Color <- DiscordColor.Green
let recruitBtn = DiscordButtonComponent(ButtonStyle.Danger, $"ShowRecruitmentEmbed", $"Recruit Now") :> DiscordComponent
builder.AddComponents ([ recruitBtn ]) |> ignore
let role = ctx.GetGuild().GetRole(GuildEnvironment.roleWhitelist)
do! ctx.GetDiscordMember().GrantRoleAsync(role)
let! _ = DbService.updatePlayerCurrency -WhitelistPrice player
builder.AddEmbed(embed) |> ignore
do! ctx.FollowUp(builder)
// Send message to hall of privacy
let builder = DiscordMessageBuilder()
builder.WithContent($"{player.Name} just purchased WHITELIST!") |> ignore
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
let user = ctx.GetDiscordMember()
do! Analytics.whiteListPurchased WhitelistPrice user.Id user.Username
} :> 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)
do! Analytics.recruitLinkButton code user.Id user.Username (ctx.GetChannel().Id) (ctx.GetChannel().Name)
} :> 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 "RecruitButton"
| _ ->
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) "RecruitCommand"
[<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)