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 = 10 type Invite = { Code : string Inviter : uint64 Count : int } let getInvites () = async { let! invites = connStr |> Sql.connect |> Sql.query """ SELECT code, inviter, count FROM invite WHERE created_at > (current_timestamp at time zone 'utc') - interval '1 day' """ |> Sql.executeAsync (fun read -> { Code = read.string "code" Inviter = read.string "inviter" |> uint64 Count = read.int "count" }) |> Async.AwaitTask return invites |> List.map (fun inv -> (inv.Code , (inv.Inviter , inv.Count))) |> Map.ofList } let 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 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 acceptInvite 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 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 checkUserInvited 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 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; """ |> Sql.executeAsync (fun read -> read.string "discord_id" |> uint64) |> Async.AwaitTask let createGuildInvite (ctx : IDiscordContext) = task { let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] let! invite = channel.CreateInviteAsync(max_age = 86400, unique = true) // When a player generates an invite code but it hasn't expired, it generates the same code, creating a duplicate entry // so catch the exception thrown because the code column is unique try let! _ = createInvite (ctx.GetDiscordMember().Id) invite.Code return () with ex -> printfn "%A" ex.Message () let embed = DiscordEmbedBuilder() .WithDescription($"Copy this link and share it with any Degenz you want to recruit to the Degenz Army.\n\n" + "**YOUR REWARD:** Eartn 10 💰$GBT for every person you recruit that comes into the server" + $"```https://discord.gg/{invite.Code}```") .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") .WithTitle("Your mission") let msg = DiscordInteractionResponseBuilder() .AddEmbed(embed) .AsEphemeral(true) // .WithContent($"https://discord.gg/{invite.Code}") do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) } let 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 (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 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 = DiscordInteractionResponseBuilder() .AsEphemeral(true) .WithContent($"**Total Earned:** `{users.Length * InviteRewardAmount} 💰$GBT `\n\n**Last 10 users recruited:**\n{sb}") 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.Parallel |> Async.Ignore } let 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 (inviter,count) -> if invite.Uses > count then do! addInvitedUser eventArgs.Member.Id invite.Code invite.Uses |> Async.Ignore let! _ = acceptInvite eventArgs.Member.Id let! player = DbService.tryFindPlayer inviter match player with | Some player -> do! DbService.updatePlayerCurrency (int InviteRewardAmount) player |> Async.Ignore let builder = DiscordMessageBuilder() builder.WithContent($"{eventArgs.Member.DisplayName} was recruited to the server. <@{player.DiscordId}> just earned {InviteRewardAmount} 💰$GBT for their efforts!") |> ignore let channel = eventArgs.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle) do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore | None -> return () | None -> () } :> Task let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) = task { let! exists = checkUserInvited eventArgs.Member.Id if not exists then do! processNewUser eventArgs } :> 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 type Inviter() = inherit ApplicationCommandModule () [] member this.CreateInvite (ctx : InteractionContext) = createGuildInvite (DiscordInteractionContext ctx) [] member this.ListInvitedPeople (ctx : InteractionContext) = getInvitedUsersForId (DiscordInteractionContext ctx) // [] member this.ListServerInvites (ctx : InteractionContext) = listServerInvites (DiscordInteractionContext ctx) // [] member this.getAttributions (ctx : InteractionContext, [] user : DiscordUser) = getAttributions (DiscordInteractionContext ctx) user.Id // [] member this.ClearInvites (ctx : InteractionContext) = clearInvites (DiscordInteractionContext ctx)