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 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 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 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 """ |> 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:** Recruit `10` people and gain access to Beautopia©" + $"```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() for user in users do sb.AppendLine($"<@{user}>") |> ignore let msg = DiscordInteractionResponseBuilder() .AsEphemeral(true) .WithContent($"<@{ctx.GetDiscordMember().Id}> has invited the following people:\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 handleGuildMemberAdded _ (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 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)