discord-bot-game/Bot/InviteTracker.fs

510 lines
20 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;
"""
|> Sql.executeAsync (fun read -> read.string "discord_id" |> uint64)
|> Async.AwaitTask
let guildInviteEmbed showWhitelistReward =
let rewardMsg =
if showWhitelistReward then
$"**⏀ | Your Mission:**\nUse the link to **recruit** Degenz into Beautopia©.\n\n" +
$"**⌼ | Your Reward:**\nGet awarded **Whitelist**."
else
$"**⏀ | Your Mission:**\nUse the link to share it with Degenz you want to `/recruit`.\n\n" +
$"**⌼ | Your Reward:**\n`Earn {InviteRewardAmount} $GBT` 💰 for every Degen you've `/recruited`."
let embed =
DiscordEmbedBuilder()
.WithDescription(rewardMsg)
.WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500")
.WithTitle("Recruitment")
let builder =
DiscordFollowupMessageBuilder()
.AddEmbed(embed)
.AsEphemeral(true)
let button = DiscordButtonComponent(ButtonStyle.Success, $"CreateGuildInvite", $"Give it to me") :> DiscordComponent
builder.AddComponents [| button |]
let private showInviteMessage (ctx : IDiscordContext) showWhitelistReward =
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 showWhitelistReward)
| 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 $"You still haven't completed training! 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 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 ->
builder.Content <- $"""
🎉 You're already WHITELISTED!
Come hang with all the other VIP Degenz in the <#{GuildEnvironment.channelElite}>
"""
| NotEnoughGBT total ->
builder.AddComponents([ DiscordButtonComponent(ButtonStyle.Success, $"BuyWhitelist", $"Buy Now", true) :> DiscordComponent ]) |> ignore
builder.Content <- $"""
You don't have enough **$GBT** to buy a WHITELIST spot!
WHITELIST aint cheap, and looks like you're `{WhitelistPrice - total} $GBT` short.
Go earn more $GBT, and come back when you have `{WhitelistPrice} $GBT`!
`/recruit` other Degenz and get `{InviteRewardAmount} 💰 $GBT` for every Degen you recruit!
`/hack` other Degenz in <#{GuildEnvironment.channelBattle}> to steal their **$GBT**
`/toss` against <@{GuildEnvironment.botIdTosserTed}> in <#{GuildEnvironment.channelTosserTed}> to try double up!
Good luck Degen
"""
| Granted _ ->
builder.AddComponents([ DiscordButtonComponent(ButtonStyle.Success, $"BuyWhitelist", $"Buy Now") :> DiscordComponent ]) |> ignore
builder.Content <- """
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.
"""
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!
Come hang with all the other VIP Degenz in the <#{GuildEnvironment.channelElite}>
`/recruit` other Degenz and get 💰 $GBT {InviteRewardAmount} for every Degen you recruit!
"""
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
| _ ->
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) false
[<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)