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 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 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()) | 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 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, use the `/recruit` command to get the recruitment link" DiscordFollowupMessageBuilder() .AsEphemeral(true) .WithContent(str) do! ctx.FollowUp(msg) let user = ctx.GetDiscordMember() do! Analytics.recruitedCommand 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 (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 InviteRewardAmount player |> Async.Ignore do! match GuildEnvironment.botClientRecruit with | Some recruitBot -> async { 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! recruitBot.SendMessageAsync(channel, builder) |> Async.AwaitTask |> Async.Ignore } | None -> async.Return() do! Analytics.invitedUserAccepted invite.Code player.DiscordId invitedPlayer.DiscordId player.Name invitedPlayer.Name | None -> return () with _ -> () | true -> return () } :> Task let sendInitialEmbed (ctx : IDiscordContext) = async { try let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelWhitelist) 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 may 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! 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 type WhitelistResult = | NotInGame | NotAHacker | NotEnoughGBT of currentAmount : int | NotEnoughStock | Granted of PlayerData | AlreadyWhitelisted let tryGrantWhitelist (ctx : IDiscordContext) stock price = task { let user = ctx.GetDiscordMember() match! DbService.tryFindPlayer user.Id with | Some player -> let hasWhitelist = Seq.contains (ctx.GetGuild().GetRole(GuildEnvironment.roleWhitelist)) user.Roles let isHacker = Seq.contains (ctx.GetGuild().GetRole(GuildEnvironment.roleHacker)) user.Roles match hasWhitelist , player.Active , isHacker , stock > 0 , player.Bank >= price with | true , _ , _ , _ , _ -> return AlreadyWhitelisted | _ , false , _ , _ , _ -> return NotInGame | _ , _ , false , _ , _ -> return NotAHacker | _ , _ , _ , false , _ -> return NotEnoughStock | _ , _ , _ , _ , false -> return NotEnoughGBT player.Bank | _ , _ , _ , _ , _ -> return Granted player | None -> return NotInGame } let notAHackerMsg = $""" Woah slow down buddy… You’re 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… You’re 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 = $""" ✅ You’re **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 you’ve invited. """ let notEnoughMoneyMsg price total = $""" Oh no! You don't have enough **$GBT** to buy a WHITELIST spot! Come back when you have `{price - 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 Brother’s 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 stock price = whitelistEmbed.ImageUrl <- "https://s7.gifyu.com/images/whitelist-item-mock-banner18.png" whitelistEmbed.AddField("Item", "`1x Whitelist`", true) |> ignore whitelistEmbed.AddField("Available", $"`{stock}`", true) |> ignore whitelistEmbed.AddField("Price 💰", $"`{price} $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! wlItem = DbService.getWhitelistItem () let! availability = tryGrantWhitelist ctx wlItem.Stock wlItem.Price 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 | NotEnoughStock -> whitelistEmbed.Description <- "Oh no! We do not have any whitelist spots available for now. Check back later or go bother Kitty and ask him why the fuck you can't whitelist" | NotEnoughGBT total -> includeInfo wlItem.Stock wlItem.Price builder.AddComponents([ buyBtn ; recruitBtn ]) |> ignore whitelistEmbed.Description <- notEnoughMoneyMsg wlItem.Price total | Granted _ -> includeInfo wlItem.Stock wlItem.Price 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 you’ve 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 you’ve invited. """ let handleBuyWhitelist (ctx : IDiscordContext) = task { let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) let! wlItem = DbService.getWhitelistItem () let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) match! tryGrantWhitelist ctx wlItem.Stock wlItem.Price 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) | NotEnoughStock -> builder.Content <- $"We just ran out of stock, tough shit" do! ctx.FollowUp(builder) | Granted player -> match! DbService.updateWhitelistStock () with | true -> 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 -wlItem.Price 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 wlItem.Price wlItem.Stock user.Id user.Username | false -> let embed = DiscordEmbedBuilder() embed.Description <- "Oh no! Looks like the last Whitelist spot was taken. Don't worry you weren't charged..." embed.Color <- DiscordColor.Red builder.AddEmbed(embed) |> ignore 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 ( result , channel ) = ctx.GetGuild().Channels.TryGetValue(GuildEnvironment.channelWelcome) if result then 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 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 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 let rec setWhitelistStock amount (ctx : IDiscordContext) = task { do! Messaging.defer ctx let! result = DbService.setWhitelistStock amount if result then do! Messaging.sendFollowUpMessage ctx $"Set Whitelist stock to {amount}" else do! Messaging.sendFollowUpMessage ctx $"Error setting WL to {amount}, make sure it's greater than 0" } :> Task type Inviter() = inherit ApplicationCommandModule () [] member this.CreateInvite (ctx : InteractionContext) = showInviteMessage (DiscordInteractionContext ctx) "RecruitCommand" [] member this.ListInvitedPeople (ctx : InteractionContext) = let ictx = DiscordInteractionContext ctx :> IDiscordContext getInvitedUsersForId (ictx.GetDiscordMember()) ictx // [] member this.ListServerInvites (ctx : InteractionContext) = listServerInvites (DiscordInteractionContext ctx) // [] member this.ClearInvites (ctx : InteractionContext) = clearInvites (DiscordInteractionContext ctx)