From 1fcf78d9d1ea4263750c9cb952fb066cd87c21e4 Mon Sep 17 00:00:00 2001 From: Alec LaLonde Date: Wed, 19 Jan 2022 22:35:26 -0700 Subject: [PATCH] Added cron trigger to sync guild memberships from Discord --- hasura/metadata/cron_triggers.yaml | 5 + hasura/metadata/tables.yaml | 11 - .../backend/src/handlers/actions/routes.ts | 5 + .../backend/src/handlers/graphql/queries.ts | 8 + .../triggers/syncDiscordGuildMembers.ts | 243 ++++++++++-------- yarn.lock | 9 + 6 files changed, 168 insertions(+), 113 deletions(-) diff --git a/hasura/metadata/cron_triggers.yaml b/hasura/metadata/cron_triggers.yaml index 69a60487..3134dd75 100644 --- a/hasura/metadata/cron_triggers.yaml +++ b/hasura/metadata/cron_triggers.yaml @@ -21,3 +21,8 @@ retry_interval_seconds: 10 comment: Checks for cache entries more than four days old and queues them to be recached. +- name: syncAllGuildDiscordMembers + webhook: '{{ACTION_BASE_ENDPOINT}}/syncAllGuildDiscordMembers' + schedule: 31 5 * * * + include_in_metadata: true + payload: {} diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index e1f85446..a4960023 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -128,14 +128,6 @@ - name: GuildType using: foreign_key_constraint_on: type - - name: metadata - using: - manual_configuration: - remote_table: - schema: public - name: guild_metadata - column_mapping: - id: guild_id array_relationships: - name: guild_players using: @@ -401,7 +393,6 @@ - ethereum_address - id - player_type_id - - profile_layout - pronouns - rank - role @@ -418,7 +409,6 @@ - color_mask - ethereum_address - id - - profile_layout - pronouns - rank - role @@ -435,7 +425,6 @@ - availability_hours - color_mask - player_type_id - - profile_layout - pronouns - role - timezone diff --git a/packages/backend/src/handlers/actions/routes.ts b/packages/backend/src/handlers/actions/routes.ts index 0a2c0b4d..9460f753 100644 --- a/packages/backend/src/handlers/actions/routes.ts +++ b/packages/backend/src/handlers/actions/routes.ts @@ -2,6 +2,7 @@ import express from 'express'; import multer from 'multer'; import { asyncHandlerWrapper } from '../../lib/apiHelpers'; +import { syncAllGuildDiscordMembers } from '../triggers/syncDiscordGuildMembers'; import { guildRoutes } from './guild/routes'; import { cacheRoutes } from './idxCache/routes'; import { migrateSourceCredAccounts } from './migrateSourceCredAccounts/handler'; @@ -18,6 +19,10 @@ actionRoutes.post( '/migrateSourceCredAccounts', asyncHandlerWrapper(migrateSourceCredAccounts), ); +actionRoutes.post( + '/syncAllGuildDiscordMembers', + asyncHandlerWrapper(syncAllGuildDiscordMembers), +); actionRoutes.use('/quests', questsRoutes); diff --git a/packages/backend/src/handlers/graphql/queries.ts b/packages/backend/src/handlers/graphql/queries.ts index 814cff22..0b50f86b 100644 --- a/packages/backend/src/handlers/graphql/queries.ts +++ b/packages/backend/src/handlers/graphql/queries.ts @@ -88,6 +88,8 @@ export const GuildFragment = gql` type website_url discord_id + status + membership_through_discord } `; @@ -113,6 +115,12 @@ gql` } } + query GetGuilds($status: GuildStatus_enum) { + guild(where: { status: { _eq: $status } }) { + ...GuildFragment + } + } + query GetGuildMetadataById($id: uuid!) { guild_metadata(where: { guild_id: { _eq: $id } }) { guild_id diff --git a/packages/backend/src/handlers/triggers/syncDiscordGuildMembers.ts b/packages/backend/src/handlers/triggers/syncDiscordGuildMembers.ts index 7227ba4f..9dc6f4f7 100644 --- a/packages/backend/src/handlers/triggers/syncDiscordGuildMembers.ts +++ b/packages/backend/src/handlers/triggers/syncDiscordGuildMembers.ts @@ -3,10 +3,12 @@ import { createDiscordClient, GuildDiscordMetadata, } from '@metafam/discord-bot'; +import { Request, Response } from 'express'; import { Guild, Guild_Player_Insert_Input, + GuildFragmentFragment, GuildStatus_Enum, SyncGuildMembersMutation, } from '../../lib/autogen/hasura-sdk'; @@ -18,111 +20,148 @@ export const syncDiscordGuildMembers = async ( ) => { const { new: guild } = payload.event.data; - if (guild?.discord_id == null) return; - - console.log(`Updating guild members for ${guild?.name} from Discord...`); - try { - const getMetadataResponse = await client.GetGuildMetadataById({ - id: guild.id, - }); - const guildMetadata = getMetadataResponse.guild_metadata[0]; - if ( - guildMetadata == null || - guildMetadata.discord_metadata == null || - guild.membership_through_discord === false - ) - return; - - // at least one membership role must be defined - const discordServerMembershipRoles = (guildMetadata.discord_metadata as GuildDiscordMetadata) - .membershipRoleIds; - if ( - discordServerMembershipRoles == null || - discordServerMembershipRoles?.length === 0 - ) { - return; - } - - // only sync on ACTIVE guilds. For all others, remove all guild_players - if (guild.status !== GuildStatus_Enum.Active) { - const removeResponse = await client.RemoveAllGuildMembers({ - guildId: guild.id, - }); - const numDeleted = removeResponse.delete_guild_player?.affected_rows; - if (numDeleted != null && numDeleted > 0) { - console.log(`Removed ${numDeleted} players from ${guild.status} guild`); - } - return; - } - - const discordClient = await createDiscordClient(); - const discordGuild = await discordClient.guilds.fetch(guild.discord_id); - - if (discordGuild == null) - throw new Error(`Discord server ${guild.discord_id} does not exist!`); - - const getGuildMembersResponse = await client.GetGuildMembers({ - id: guild.id, - }); - const guildMemberDiscordIds = getGuildMembersResponse.guild[0].guild_players - .filter((p) => p.Player.discord_id != null) - .map((p) => p.Player.discord_id) as string[]; - - await discordGuild.members.fetch(); - - // gather all discord server members who have at least one of the "membership" roles - // as defined by this guild - const discordGuildMembers = discordGuild.members.cache.filter( - (discordMember) => - discordMember.roles.cache.some((role) => - discordServerMembershipRoles.includes(role.id), - ), - ); - - // gather discord server members who are not already members of this guild - const discordServerMemberIds: string[] = []; - const playerDiscordIdsToAdd: string[] = []; - discordGuildMembers.forEach((discordMember) => { - discordServerMemberIds.push(discordMember.user.id); - if (!guildMemberDiscordIds.includes(discordMember.user.id)) { - playerDiscordIdsToAdd.push(discordMember.user.id); - } - }); - - // gather current members of this guild who are not in the list of discord server members - const playersToRemove = guildMemberDiscordIds.filter( - (discordId) => !discordServerMemberIds.includes(discordId), - ); - - const getPlayerIdsResponse = await client.GetPlayersByDiscordId({ - discordIds: playerDiscordIdsToAdd, - }); - - const playersToAdd: Guild_Player_Insert_Input[] = getPlayerIdsResponse.player.map( - (player) => ({ - guild_id: guild.id, - player_id: player.id, - }), - ); - - const syncResponse: SyncGuildMembersMutation = await client.SyncGuildMembers( - { - memberDiscordIdsToRemove: playersToRemove, - membersToAdd: playersToAdd, - }, - ); - - const numDeleted = syncResponse.delete_guild_player?.affected_rows; - const numInserted = syncResponse.insert_guild_player?.affected_rows; - - if (numDeleted != null && numDeleted > 0) { - console.log(`Removed ${numDeleted} players`); - } - if (numInserted != null && numInserted > 0) { - console.log(`Added ${numInserted} players`); + if (guild != null) { + await syncGuildMembers(guild); } } catch (e) { console.error(e); } }; + +export const syncAllGuildDiscordMembers = async ( + _req: Request, + res: Response, +): Promise => { + try { + const { guild: guilds } = await client.GetGuilds({ + status: GuildStatus_Enum.Active, + }); + + await Promise.all( + guilds + .filter((guild) => guild.membership_through_discord === true) + .map((guild) => syncGuildMembers(guild)), + ); + + res.sendStatus(200); + } catch (e) { + const msg = (e as Error).message; + console.warn('Error syncing guild memberships from discord', msg); + console.error(e); + + res.sendStatus(500); + } +}; + +const syncGuildMembers = async (guild: GuildFragmentFragment) => { + if (guild?.discord_id == null) return; + + const getMetadataResponse = await client.GetGuildMetadataById({ + id: guild.id, + }); + const guildMetadata = getMetadataResponse.guild_metadata[0]; + if ( + guildMetadata == null || + guildMetadata.discord_metadata == null || + guild.membership_through_discord === false + ) + return; + + // at least one membership role must be defined + const discordServerMembershipRoles = (guildMetadata.discord_metadata as GuildDiscordMetadata) + .membershipRoleIds; + if ( + discordServerMembershipRoles == null || + discordServerMembershipRoles?.length === 0 + ) { + return; + } + + // only sync on ACTIVE guilds. For all others, remove all guild_players + if (guild.status !== GuildStatus_Enum.Active) { + const removeResponse = await client.RemoveAllGuildMembers({ + guildId: guild.id, + }); + const numDeleted = removeResponse.delete_guild_player?.affected_rows; + if (numDeleted != null && numDeleted > 0) { + console.log(`Removed ${numDeleted} players from ${guild.status} guild`); + } + return; + } + + const discordClient = await createDiscordClient(); + const discordGuild = await discordClient.guilds.fetch(guild.discord_id); + + if (discordGuild == null) + throw new Error(`Discord server ${guild.discord_id} does not exist!`); + + const getGuildMembersResponse = await client.GetGuildMembers({ + id: guild.id, + }); + const guildMemberDiscordIds = getGuildMembersResponse.guild[0].guild_players + .filter((p) => p.Player.discord_id != null) + .map((p) => p.Player.discord_id) as string[]; + + await discordGuild.members.fetch(); + + // gather all discord server members who have at least one of the "membership" roles + // as defined by this guild + const discordGuildMembers = discordGuild.members.cache.filter( + (discordMember) => + discordMember.roles.cache.some((role) => + discordServerMembershipRoles.includes(role.id), + ), + ); + + // gather discord server members who are not already members of this guild + const discordServerMemberIds: string[] = []; + const playerDiscordIdsToAdd: string[] = []; + discordGuildMembers.forEach((discordMember) => { + discordServerMemberIds.push(discordMember.user.id); + if (!guildMemberDiscordIds.includes(discordMember.user.id)) { + playerDiscordIdsToAdd.push(discordMember.user.id); + } + }); + + // gather current members of this guild who are not in the list of discord server members + const playersToRemove = guildMemberDiscordIds.filter( + (discordId) => !discordServerMemberIds.includes(discordId), + ); + + const getPlayerIdsResponse = await client.GetPlayersByDiscordId({ + discordIds: playerDiscordIdsToAdd, + }); + + const playersToAdd: Guild_Player_Insert_Input[] = getPlayerIdsResponse.player.map( + (player) => ({ + guild_id: guild.id, + player_id: player.id, + }), + ); + + const syncResponse: SyncGuildMembersMutation = await client.SyncGuildMembers({ + memberDiscordIdsToRemove: playersToRemove, + membersToAdd: playersToAdd, + }); + + const numDeleted = syncResponse.delete_guild_player?.affected_rows; + const numInserted = syncResponse.insert_guild_player?.affected_rows; + + let logStr = ''; + + if (numInserted != null && numInserted > 0) { + logStr = `Added ${numInserted} players`; + } + if (numDeleted != null && numDeleted > 0) { + logStr += `${ + logStr.length > 0 ? ' and removed' : 'Removed' + } ${numDeleted} players`; + } + + if (logStr.length > 0) { + console.log( + `Updated guild members for ${guild?.name} from Discord. ${logStr}`, + ); + } +}; diff --git a/yarn.lock b/yarn.lock index da774dfb..6bfc7bc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19156,6 +19156,15 @@ loader-utils@^1.1.0, loader-utils@^1.4.0: emojis-list "^3.0.0" json5 "^1.0.1" +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + loady@~0.0.1: version "0.0.5" resolved "https://registry.yarnpkg.com/loady/-/loady-0.0.5.tgz#b17adb52d2fb7e743f107b0928ba0b591da5d881"