diff --git a/Bot/Bot.fs b/Bot/Bot.fs index 9d9a3d3..f75e40d 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -4,9 +4,6 @@ open System.Threading.Tasks open DSharpPlus open DSharpPlus.SlashCommands open Degenz -open Degenz.HackerBattle -open Degenz.Store -open Degenz.Thief open Emzi0767.Utilities //open Degenz.SlotMachine @@ -17,13 +14,11 @@ let guild = GuildEnvironment.guildId let hackerBattleConfig = DiscordConfiguration() let storeConfig = DiscordConfiguration() let stealConfig = DiscordConfiguration() +let inviterConfig = DiscordConfiguration() //let slotMachineConfig = DiscordConfiguration() //hackerBattleConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace //storeConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace -//let configs = [| hackerBattleConfig ; storeConfig ; slotMachineConfig ; |] -let configs = [ hackerBattleConfig ; storeConfig ; stealConfig ] - hackerBattleConfig.TokenType <- TokenType.Bot hackerBattleConfig.Intents <- DiscordIntents.All @@ -33,39 +28,40 @@ storeConfig.Intents <- DiscordIntents.All stealConfig.TokenType <- TokenType.Bot stealConfig.Intents <- DiscordIntents.All +inviterConfig.TokenType <- TokenType.Bot +inviterConfig.Intents <- DiscordIntents.All + hackerBattleConfig.Token <- GuildEnvironment.tokenHackerBattle storeConfig.Token <- GuildEnvironment.tokenStore stealConfig.Token <- GuildEnvironment.tokenSteal +inviterConfig.Token <- GuildEnvironment.tokenInviter //slotMachineConfig.Token <- Environment.GetEnvironmentVariable("BOT_SLOT_MACHINE") let hackerBattleBot = new DiscordClient(hackerBattleConfig) let storeBot = new DiscordClient(storeConfig) let stealBot = new DiscordClient(stealConfig) +let inviterBot = new DiscordClient(inviterConfig) //let slotMachineBot = new DiscordClient(slotMachineConfig) //let clients = [| hackerBattleBot ; storeBot ; slotMachineBot |] let hackerCommands = hackerBattleBot.UseSlashCommands() let storeCommands = storeBot.UseSlashCommands() let stealCommands = stealBot.UseSlashCommands() +let inviterCommands = inviterBot.UseSlashCommands() //let sc3 = slotMachineBot.UseSlashCommands() -hackerCommands.RegisterCommands(guild); -storeCommands.RegisterCommands(guild); -stealCommands.RegisterCommands(guild); +hackerCommands.RegisterCommands(guild); +storeCommands.RegisterCommands(guild); +stealCommands.RegisterCommands(guild); +inviterCommands.RegisterCommands(guild); //hackerCommands.RegisterCommands(guild); //sc3.RegisterCommands(guild); hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent)) storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents)) stealBot.add_ComponentInteractionCreated(AsyncEventHandler(Thief.handleStealButton)) -hackerBattleBot.add_GuildMemberAdded(AsyncEventHandler(fun client ea -> - task { - let! guildInvites = ea.Guild.GetInvitesAsync() - let! cachedInvites = InviteTracker.getInvites() - for invite in guildInvites do - if invite.Uses < (snd cachedInvites.[invite.Code]) then - do! InviteTracker.addInvitedUser ea.Member.Id invite.Code |> Async.Ignore - })) +inviterBot.add_GuildMemberAdded(AsyncEventHandler(InviteTracker.handleGuildMemberAdded)) +inviterBot.add_GuildMemberRemoved(AsyncEventHandler(InviteTracker.handleGuildMemberRemoved)) let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) = async { @@ -94,6 +90,8 @@ GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously + //let channel = hackerBattleBot.GetChannelAsync(1234uL) |> Async.AwaitTask |> Async.RunSynchronously //channel.invi diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 56784b0..413045c 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -243,93 +243,6 @@ let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEve do! eventCtx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } -let createInvite (ctx : IDiscordContext) = - task { - let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] - let! invite = channel.CreateInviteAsync(max_age = 259200) - do! InviteTracker.createInvite (ctx.GetDiscordMember().Id) invite.Code |> Async.Ignore - - let embed = - DiscordEmbedBuilder() - .WithDescription($"Send this invite to your friend, when they join, they can type the `/enter-code` slash command\n\n - ```https://discord.gg/{invite.Code}```") - .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") - .WithTitle("Invite Code") - - 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 = InviteTracker.getInviteAttributions(userId) - let msg = - DiscordInteractionResponseBuilder() - .AsEphemeral(true) - .WithContent($"<@{userId}> has invited {total} people") - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) -} - -let getInvitedUsers (ctx : IDiscordContext) userId = task { - let! users = InviteTracker.getInvitedUsers(userId) - let sb = StringBuilder() - for user in users do - sb.AppendLine($"<@{user}>") |> ignore - let msg = - DiscordInteractionResponseBuilder() - .AsEphemeral(true) - .WithContent($"<@{userId}> 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 invite (ctx : IDiscordContext) = -// task { -// let code = Guid.NewGuid().ToString().Substring(0, 7) -// -//// let embed1 = -//// DiscordEmbedBuilder() -//// .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") -// let embed2 = -// DiscordEmbedBuilder() -// .WithDescription($"Send this invite to your friend, when they join, type the `/enter-code` slash command\n\n```{code}```") -// .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") -// .WithTitle("Invite Code") -// -// let msg = -// DiscordInteractionResponseBuilder() -// .AsEphemeral(true) -//// .AddEmbed(embed1) -// .AddEmbed(embed2) -// -// do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) -// } type HackerGame() = inherit ApplicationCommandModule () @@ -353,26 +266,6 @@ type HackerGame() = do! Messaging.sendSimpleResponse ctx msg } - [] - member this.CreateInvite (ctx : InteractionContext) = - createInvite (DiscordInteractionContext ctx) - - [] - member this.ListServerInvites (ctx : InteractionContext) = - listServerInvites (DiscordInteractionContext ctx) - - [] - member this.getAttributions (ctx : InteractionContext, [] user : DiscordUser) = - getAttributions (DiscordInteractionContext ctx) user.Id - - [] - member this.ListInvitedPeople (ctx : InteractionContext, [] user : DiscordUser) = - getInvitedUsers (DiscordInteractionContext ctx) user.Id - - [] - member this.ClearInvites (ctx : InteractionContext) = - clearInvites (DiscordInteractionContext ctx) - [] member this.Arsenal (ctx : InteractionContext) = enforceChannels (DiscordInteractionContext ctx) (Trainer.handleArsenal) arsenal diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index 4dbc87a..25e4073 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -18,6 +18,7 @@ let tokenPlayerInteractions = getVar "TOKEN_PLAYER_INTERACTIONS" let tokenSteal = getVar "TOKEN_STEAL" let tokenHackerBattle = getVar "TOKEN_HACKER_BATTLE" let tokenStore = getVar "TOKEN_STORE" +let tokenInviter = getVar "TOKEN_INVITER" let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE" let channelTraining = getId "CHANNEL_TRAINING" let channelArmory = getId "CHANNEL_ARMORY" @@ -30,6 +31,7 @@ let channelWelcome = getId "CHANNEL_WELCOME" //let channelThievery = getId "CHANNEL_THIEVERY" let botIdHackerBattle = getId "BOT_HACKER_BATTLE" let botIdArmory = getId "BOT_ARMORY" +let botInviter = getId "BOT_INVITER" let roleTrainee = getId "ROLE_TRAINEE" let rolePrisoner = getId "ROLE_PRISONER" diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 2f219a0..03fb575 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -1,7 +1,13 @@ module Degenz.InviteTracker -open System +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 @@ -18,7 +24,7 @@ let getInvites () = async { |> Sql.connect |> Sql.query """ SELECT code, inviter, count FROM invite - WHERE created_at > (current_timestamp at time zone 'utc') - interval '3 day' + WHERE created_at > (current_timestamp at time zone 'utc') - interval '1 day' """ |> Sql.executeAsync (fun read -> { Code = read.string "code" @@ -33,34 +39,50 @@ let getInvites () = async { } 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 + 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 + |> Async.Ignore -let addInvitedUser did code = +let addInvitedUser did code count = try connStr |> Sql.connect |> Sql.executeTransactionAsync [ """ - WITH invite AS (SELECT id FROM invite WHERE code = @code) - INSERT INTO invited_user (discord_id, invite_id) SELECT @discord_id, invite.id FROM invite; - """ , [ [ "@discord_id" , Sql.string (string did) ] ; [ "@code" , Sql.string code ] ] - "UPDATE invite SET count = count + 1 WHERE code = @code" , [ [ "@code" , Sql.string code ] ] + 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 sum(count) AS total FROM invite WHERE inviter = @did" - |> Sql.executeRowAsync (fun read -> read.int "total") + |> 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 = @@ -73,3 +95,119 @@ let getInvitedUsers userId = """ |> 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) + + // 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 + do! createInvite (ctx.GetDiscordMember().Id) invite.Code + with _ -> () + + let embed = + DiscordEmbedBuilder() + .WithDescription($"Use this invite link to earn invite points for future rewards.\nExpires in 1 day. + ```https://discord.gg/{invite.Code}```") + .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") + .WithTitle("Invite Link") + + 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) userId = task { + let! users = getInvitedUsers(userId) + let sb = StringBuilder() + for user in users do + sb.AppendLine($"<@{user}>") |> ignore + let msg = + DiscordInteractionResponseBuilder() + .AsEphemeral(true) + .WithContent($"<@{userId}> 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 { +// let! guildInvites = eventArgs.Guild.GetInvitesAsync() +// let! cachedInvites = getInvites() +// for invite in guildInvites do +// if invite.Uses < (snd cachedInvites.[invite.Code]) then +// do! addInvitedUser eventArgs.Member.Id invite.Code |> Async.Ignore + return () + } :> Task + +type Inviter() = + inherit ApplicationCommandModule () + + [] + member this.CreateInvite (ctx : InteractionContext) = + createGuildInvite (DiscordInteractionContext ctx) + + [] + member this.ListServerInvites (ctx : InteractionContext) = + listServerInvites (DiscordInteractionContext ctx) + + [] + member this.getAttributions (ctx : InteractionContext, [] user : DiscordUser) = + getAttributions (DiscordInteractionContext ctx) user.Id + + [] + member this.ListInvitedPeople (ctx : InteractionContext, [] user : DiscordUser) = + getInvitedUsersForId (DiscordInteractionContext ctx) user.Id + + [] + member this.ClearInvites (ctx : InteractionContext) = + clearInvites (DiscordInteractionContext ctx) +