From a3691fa11214b32fb8b1f6325c16ac467eba1177 Mon Sep 17 00:00:00 2001 From: Katalina Okano Date: Sun, 30 Jan 2022 03:26:14 -0500 Subject: [PATCH] automatically import from legacy, or die trying. --- packages/api/src/guilds/getters.spec.ts | 93 ++++++++++++++++++++- packages/api/src/guilds/getters.ts | 30 +++++++ packages/api/src/index.ts | 17 +--- packages/api/src/routes/legacy/import.ts | 9 -- packages/api/src/routes/legacy/preflight.ts | 9 -- packages/api/src/utils/legacy.ts | 69 +++++++++++++++ packages/api/src/utils/testHelpers.ts | 1 + 7 files changed, 193 insertions(+), 35 deletions(-) delete mode 100644 packages/api/src/routes/legacy/import.ts delete mode 100644 packages/api/src/routes/legacy/preflight.ts create mode 100644 packages/api/src/utils/legacy.ts diff --git a/packages/api/src/guilds/getters.spec.ts b/packages/api/src/guilds/getters.spec.ts index b606b08..d0f231d 100644 --- a/packages/api/src/guilds/getters.spec.ts +++ b/packages/api/src/guilds/getters.spec.ts @@ -1,11 +1,19 @@ jest.mock('../utils/discord'); +jest.mock('../utils/legacy'); -import { Features, GuildData } from '@roleypoly/types'; +import { CategoryType, Features, GuildData } from '@roleypoly/types'; import { APIGuild, discordFetch } from '../utils/discord'; +import { + fetchLegacyServer, + LegacyGuildData, + transformLegacyGuild, +} from '../utils/legacy'; import { configContext } from '../utils/testHelpers'; import { getGuild, getGuildData, getGuildMember } from './getters'; const mockDiscordFetch = discordFetch as jest.Mock; +const mockFetchLegacyServer = fetchLegacyServer as jest.Mock; +const mockTransformLegacyGuild = transformLegacyGuild as jest.Mock; beforeEach(() => { mockDiscordFetch.mockReset(); @@ -91,6 +99,89 @@ describe('getGuildData', () => { accessControl: expect.any(Object), }); }); + + describe('automatic legacy import', () => { + beforeEach(() => { + mockFetchLegacyServer.mockReset(); + mockTransformLegacyGuild.mockImplementation( + jest.requireActual('../utils/legacy').transformLegacyGuild + ); + }); + + it('attempts to import guild data from the legacy server', async () => { + const [config] = configContext(); + + const legacyGuildData: LegacyGuildData = { + id: '123', + message: 'Hello world!', + categories: [ + { + id: '123', + name: 'test', + position: 0, + roles: ['role-1', 'role-2'], + hidden: false, + type: 'multi', + }, + ], + }; + + mockFetchLegacyServer.mockReturnValue(legacyGuildData); + + const expectedGuildData: GuildData = { + id: '123', + message: legacyGuildData.message, + auditLogWebhook: null, + accessControl: { + allowList: [], + blockList: [], + blockPending: true, + }, + features: Features.LegacyGuild, + categories: [ + { + id: expect.any(String), + name: 'test', + position: 0, + roles: ['role-1', 'role-2'], + hidden: false, + type: CategoryType.Multi, + }, + ], + }; + + const currentGuildData = await getGuildData(config, '123'); + expect(currentGuildData).toMatchObject(expectedGuildData); + + const storedGuildData = await config.kv.guildData.get('123'); + expect(storedGuildData).toMatchObject(expectedGuildData); + }); + + it('fails an import and saves new guild data instead', async () => { + const [config] = configContext(); + + mockFetchLegacyServer.mockReturnValue(null); + + const expectedGuildData: GuildData = { + id: '123', + message: '', + auditLogWebhook: null, + accessControl: { + allowList: [], + blockList: [], + blockPending: true, + }, + features: Features.None, + categories: [], + }; + + const currentGuildData = await getGuildData(config, '123'); + expect(currentGuildData).toMatchObject(expectedGuildData); + + const storedGuildData = await config.kv.guildData.get('123'); + expect(storedGuildData).toMatchObject(expectedGuildData); + }); + }); }); describe('getGuildMember', () => { diff --git a/packages/api/src/guilds/getters.ts b/packages/api/src/guilds/getters.ts index 39f8ee5..2fbcef4 100644 --- a/packages/api/src/guilds/getters.ts +++ b/packages/api/src/guilds/getters.ts @@ -7,6 +7,7 @@ import { discordFetch, getHighestRole, } from '@roleypoly/api/src/utils/discord'; +import { fetchLegacyServer, transformLegacyGuild } from '@roleypoly/api/src/utils/legacy'; import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission'; import { Features, @@ -86,6 +87,19 @@ export const getGuildData = async (config: Config, id: string): Promise => { + const legacyGuildData = await fetchLegacyServer(config, id); + if (!legacyGuildData) { + // Means there is no legacy data. + return null; + } + + const transformed = transformLegacyGuild(legacyGuildData); + + await config.kv.guildData.put(id, transformed); + return transformed; +}; + export const getGuildMember = async ( config: Config, serverID: string, diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index bc24978..6cb5183 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -19,7 +19,7 @@ import { Router } from 'itty-router'; import { authBounce } from './routes/auth/bounce'; import { Environment, parseEnvironment } from './utils/config'; import { Context, RoleypolyHandler } from './utils/context'; -import { json, notFound, notImplemented, serverError } from './utils/response'; +import { json, notFound, serverError } from './utils/response'; const router = Router(); @@ -47,21 +47,6 @@ router.get('/guilds/slug/:guildId', injectParams, guildsSlug); router.post('/interactions', handleInteraction); -router.get( - '/legacy/preflight/:guildId', - injectParams, - withSession, - requireSession, - notImplemented -); -router.put( - '/legacy/import/:guildId', - injectParams, - withSession, - requireSession, - notImplemented -); - router.get('/', ((request: Request, { config }: Context) => json({ __warning: '🦊', diff --git a/packages/api/src/routes/legacy/import.ts b/packages/api/src/routes/legacy/import.ts deleted file mode 100644 index c350599..0000000 --- a/packages/api/src/routes/legacy/import.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RoleypolyHandler } from '@roleypoly/api/src/utils/context'; -import { notImplemented } from '@roleypoly/api/src/utils/response'; - -/** - * Full import flow for legacy config. - */ -export const legacyImport: RoleypolyHandler = () => { - return notImplemented(); -}; diff --git a/packages/api/src/routes/legacy/preflight.ts b/packages/api/src/routes/legacy/preflight.ts deleted file mode 100644 index 3487d4e..0000000 --- a/packages/api/src/routes/legacy/preflight.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RoleypolyHandler } from '@roleypoly/api/src/utils/context'; -import { notImplemented } from '@roleypoly/api/src/utils/response'; - -/** - * Fetch setup from beta.roleypoly.com to show the admin. - */ -export const legacyPreflight: RoleypolyHandler = () => { - return notImplemented(); -}; diff --git a/packages/api/src/utils/legacy.ts b/packages/api/src/utils/legacy.ts new file mode 100644 index 0000000..8fce631 --- /dev/null +++ b/packages/api/src/utils/legacy.ts @@ -0,0 +1,69 @@ +import { Config } from '@roleypoly/api/src/utils/config'; +import { getID } from '@roleypoly/api/src/utils/id'; +import { sortBy } from '@roleypoly/misc-utils/sortBy'; +import { CategoryType, Features, GuildData } from '@roleypoly/types'; + +export type LegacyCategory = { + id: string; + name: string; + roles: string[]; + hidden: boolean; + type: 'single' | 'multi'; + position: number; +}; + +export type LegacyGuildData = { + id: string; + categories: LegacyCategory[]; + message: string; +}; + +export const fetchLegacyServer = async ( + config: Config, + id: string +): Promise => { + if (!config.interactionsSharedKey) { + return null; + } + + const guildDataResponse = await fetch( + `https://beta.roleypoly.com/x/import-to-next/${id}`, + { + headers: { + authorization: `Shared ${config.importSharedKey}`, + }, + } + ); + + if (guildDataResponse.status === 404) { + return null; + } + + if (guildDataResponse.status !== 200) { + throw new Error('Guild data fetch failed'); + } + + return await guildDataResponse.json(); +}; + +export const transformLegacyGuild = (guild: LegacyGuildData): GuildData => { + return { + id: guild.id, + message: guild.message, + features: Features.LegacyGuild, + auditLogWebhook: null, + accessControl: { + allowList: [], + blockList: [], + blockPending: true, + }, + categories: sortBy(Object.values(guild.categories), 'position').map( + (category, idx) => ({ + ...category, + id: getID(), + position: idx, // Reset positions by index. May have side-effects but oh well. + type: category.type === 'multi' ? CategoryType.Multi : CategoryType.Single, + }) + ), + }; +}; diff --git a/packages/api/src/utils/testHelpers.ts b/packages/api/src/utils/testHelpers.ts index bf992f5..5ac4f91 100644 --- a/packages/api/src/utils/testHelpers.ts +++ b/packages/api/src/utils/testHelpers.ts @@ -85,6 +85,7 @@ export const configContext = (): [Config, Context] => { BOT_CLIENT_SECRET: 'test-client-secret', BOT_CLIENT_ID: 'test-client-id', BOT_TOKEN: 'test-bot-token', + INTERACTIONS_SHARED_KEY: '', // IMPORTANT: setting this properly can have unexpected results. }); const context: Context = { config,