Added cron trigger to sync guild memberships from Discord

This commit is contained in:
Alec LaLonde
2022-01-19 22:35:26 -07:00
committed by Alec LaLonde
parent 2db3d575a3
commit 1fcf78d9d1
6 changed files with 168 additions and 113 deletions

View File

@@ -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: {}

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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<void> => {
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}`,
);
}
};

View File

@@ -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"