mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
automatically import from legacy, or die trying.
This commit is contained in:
parent
d407a015c9
commit
a3691fa112
7 changed files with 193 additions and 35 deletions
|
@ -1,11 +1,19 @@
|
||||||
jest.mock('../utils/discord');
|
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 { APIGuild, discordFetch } from '../utils/discord';
|
||||||
|
import {
|
||||||
|
fetchLegacyServer,
|
||||||
|
LegacyGuildData,
|
||||||
|
transformLegacyGuild,
|
||||||
|
} from '../utils/legacy';
|
||||||
import { configContext } from '../utils/testHelpers';
|
import { configContext } from '../utils/testHelpers';
|
||||||
import { getGuild, getGuildData, getGuildMember } from './getters';
|
import { getGuild, getGuildData, getGuildMember } from './getters';
|
||||||
|
|
||||||
const mockDiscordFetch = discordFetch as jest.Mock;
|
const mockDiscordFetch = discordFetch as jest.Mock;
|
||||||
|
const mockFetchLegacyServer = fetchLegacyServer as jest.Mock;
|
||||||
|
const mockTransformLegacyGuild = transformLegacyGuild as jest.Mock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockDiscordFetch.mockReset();
|
mockDiscordFetch.mockReset();
|
||||||
|
@ -91,6 +99,89 @@ describe('getGuildData', () => {
|
||||||
accessControl: expect.any(Object),
|
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', () => {
|
describe('getGuildMember', () => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
discordFetch,
|
discordFetch,
|
||||||
getHighestRole,
|
getHighestRole,
|
||||||
} from '@roleypoly/api/src/utils/discord';
|
} from '@roleypoly/api/src/utils/discord';
|
||||||
|
import { fetchLegacyServer, transformLegacyGuild } from '@roleypoly/api/src/utils/legacy';
|
||||||
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||||
import {
|
import {
|
||||||
Features,
|
Features,
|
||||||
|
@ -86,6 +87,19 @@ export const getGuildData = async (config: Config, id: string): Promise<GuildDat
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!guildData) {
|
if (!guildData) {
|
||||||
|
// It's rare for no guild data to exist while also having a guild.
|
||||||
|
// It's either an actually new guild... or could be imported.
|
||||||
|
// Let's attempt the import...
|
||||||
|
const legacyData = await attemptLegacyImport(config, id);
|
||||||
|
if (legacyData) {
|
||||||
|
return {
|
||||||
|
...empty,
|
||||||
|
...legacyData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// So we don't try again, let's set the data.
|
||||||
|
await config.kv.guildData.put(id, empty);
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +109,22 @@ export const getGuildData = async (config: Config, id: string): Promise<GuildDat
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const attemptLegacyImport = async (
|
||||||
|
config: Config,
|
||||||
|
id: string
|
||||||
|
): Promise<GuildData | null> => {
|
||||||
|
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 (
|
export const getGuildMember = async (
|
||||||
config: Config,
|
config: Config,
|
||||||
serverID: string,
|
serverID: string,
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { Router } from 'itty-router';
|
||||||
import { authBounce } from './routes/auth/bounce';
|
import { authBounce } from './routes/auth/bounce';
|
||||||
import { Environment, parseEnvironment } from './utils/config';
|
import { Environment, parseEnvironment } from './utils/config';
|
||||||
import { Context, RoleypolyHandler } from './utils/context';
|
import { Context, RoleypolyHandler } from './utils/context';
|
||||||
import { json, notFound, notImplemented, serverError } from './utils/response';
|
import { json, notFound, serverError } from './utils/response';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
@ -47,21 +47,6 @@ router.get('/guilds/slug/:guildId', injectParams, guildsSlug);
|
||||||
|
|
||||||
router.post('/interactions', handleInteraction);
|
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) =>
|
router.get('/', ((request: Request, { config }: Context) =>
|
||||||
json({
|
json({
|
||||||
__warning: '🦊',
|
__warning: '🦊',
|
||||||
|
|
|
@ -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();
|
|
||||||
};
|
|
|
@ -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();
|
|
||||||
};
|
|
69
packages/api/src/utils/legacy.ts
Normal file
69
packages/api/src/utils/legacy.ts
Normal file
|
@ -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<LegacyGuildData | null> => {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
|
@ -85,6 +85,7 @@ export const configContext = (): [Config, Context] => {
|
||||||
BOT_CLIENT_SECRET: 'test-client-secret',
|
BOT_CLIENT_SECRET: 'test-client-secret',
|
||||||
BOT_CLIENT_ID: 'test-client-id',
|
BOT_CLIENT_ID: 'test-client-id',
|
||||||
BOT_TOKEN: 'test-bot-token',
|
BOT_TOKEN: 'test-bot-token',
|
||||||
|
INTERACTIONS_SHARED_KEY: '', // IMPORTANT: setting this properly can have unexpected results.
|
||||||
});
|
});
|
||||||
const context: Context = {
|
const context: Context = {
|
||||||
config,
|
config,
|
||||||
|
|
Loading…
Add table
Reference in a new issue