chore: add backend-y bits of heartbeats

This commit is contained in:
41666 2021-03-12 20:45:35 -05:00
parent f65779f925
commit a34aebabe3
12 changed files with 174 additions and 2 deletions

View file

@ -10,4 +10,5 @@ declare global {
const KV_SESSIONS: KVNamespace;
const KV_GUILDS: KVNamespace;
const KV_GUILD_DATA: KVNamespace;
const KV_INFRASTRUCTURE: KVNamespace;
}

View file

@ -0,0 +1,23 @@
import { BotHeartbeatData } from 'packages/types';
import { getSharedKey, respond } from '../utils/api-tools';
import { botHeartbeatToken } from '../utils/config';
import { Infrastructure } from '../utils/kv';
const denied = () => respond({ error: 'robots only, human.' }, { status: 403 });
export const BotHeartbeat = async (request: Request): Promise<Response> => {
const { id } = getSharedKey(request) || {};
if (!id || id !== botHeartbeatToken) {
return denied();
}
const heartbeat: BotHeartbeatData = await request.json();
await Infrastructure.put<BotHeartbeatData>(`shard_${heartbeat.shard}`, {
lastHeartbeat: Date.now(),
state: heartbeat.state,
shard: heartbeat.shard,
});
return respond({ status: 'ok' });
};

View file

@ -0,0 +1,85 @@
import { BotHeartbeatData } from 'packages/types';
import { respond } from '../utils/api-tools';
import { Infrastructure } from '../utils/kv';
export const GetInfrastructureStatus = async (request: Request): Promise<Response> => {
const cachedStatus = await Infrastructure.get('cached_status');
if (cachedStatus) {
return respond({ status: cachedStatus });
}
const [shardStatus, discord, cloudflare] = await Promise.all([
getAllShardStatus(),
getDiscordStatus(),
getCloudflareStatus(),
]);
const status = {
shards: shardStatus,
discord,
cloudflare,
};
await Infrastructure.put('cached_status', status, 60 * 5);
return respond({
status,
});
};
const getAllShardStatus = async (): Promise<BotHeartbeatData[]> => {
const shards: BotHeartbeatData[] = [];
let currentShard = 0;
do {
const thisShard = await getShardStatus(currentShard);
if (!thisShard) {
break;
}
shards.push(thisShard);
currentShard++;
} while (currentShard < 64); // If we hit 64, there's bigger issues.
return shards;
};
const getShardStatus = async (shardNumber: number): Promise<BotHeartbeatData | null> => {
const shardHeartbeat = await Infrastructure.get<BotHeartbeatData>(
`shard_${shardNumber}`
);
if (!shardHeartbeat) {
return null;
}
return shardHeartbeat;
};
const getDiscordStatus = async () => {
const req = await fetch('https://srhpyqt94yxb.statuspage.io/api/v2/summary.json');
const status = await req.json();
const api =
status.components.find(
(component: { id: string }) => component.id === 'rhznvxg4v7yh'
)?.status !== 'operational';
return { api };
};
const getCloudflareStatus = async () => {
const req = await fetch('https://yh6f0r4529hb.statuspage.io/api/v2/summary.json');
const status = await req.json();
const workers =
status.components.find(
(component: { id: string }) => component.id === '57srcl8zcn7c'
)?.status !== 'operational';
const storage =
status.components.find(
(component: { id: string }) => component.id === 'tmh50tx2nprs'
)?.status !== 'operational';
return { workers, storage };
};

View file

@ -1,5 +1,7 @@
import { BotHeartbeat } from './handlers/bot-heartbeat';
import { BotJoin } from './handlers/bot-join';
import { CreateRoleypolyData } from './handlers/create-roleypoly-data';
import { GetInfrastructureStatus } from './handlers/get-infrastructure-status';
import { GetPickerData } from './handlers/get-picker-data';
import { GetSession } from './handlers/get-session';
import { GetSlug } from './handlers/get-slug';
@ -13,6 +15,9 @@ import { uiPublicURI } from './utils/config';
const router = new Router();
// Public
router.add('GET', 'get-infrastructure-status', GetInfrastructureStatus);
// OAuth
router.add('GET', 'bot-join', BotJoin);
router.add('GET', 'login-bounce', LoginBounce);
@ -30,6 +35,9 @@ router.add('PATCH', 'update-roles', UpdateRoles);
// Root users only
router.add('GET', 'x-create-roleypoly-data', CreateRoleypolyData);
// Internal routes
router.add('PUT', 'bot-heartbeat', BotHeartbeat);
// Tester Routes
router.add('GET', 'x-headers', (request) => {
const headers: { [x: string]: string } = {};

View file

@ -69,6 +69,20 @@ export const getSessionID = (request: Request): { type: string; id: string } | n
return { type, id };
};
export const getSharedKey = (request: Request): { type: string; id: string } | null => {
const sharedKey = request.headers.get('authorization');
if (!sharedKey) {
return null;
}
const [type, id] = sharedKey.split(' ');
if (type !== 'Shared') {
return null;
}
return { type, id };
};
export const userAgent =
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)';

View file

@ -8,6 +8,7 @@ const list = (x: string) => x.split(',');
export const botClientID = env('BOT_CLIENT_ID');
export const botClientSecret = env('BOT_CLIENT_SECRET');
export const botToken = env('BOT_TOKEN');
export const botHeartbeatToken = env('BOT_HEARTBEAT_TOKEN');
export const uiPublicURI = safeURI(env('UI_PUBLIC_URI'));
export const apiPublicURI = safeURI(env('API_PUBLIC_URI'));
export const rootUsers = list(env('ROOT_USERS'));

View file

@ -88,3 +88,6 @@ const self = (global as any) as Record<string, any>;
export const Sessions = new WrappedKVNamespace(kvOrLocal(self.KV_SESSIONS ?? null));
export const GuildData = new WrappedKVNamespace(kvOrLocal(self.KV_GUILD_DATA ?? null));
export const Guilds = new WrappedKVNamespace(kvOrLocal(self.KV_GUILDS ?? null));
export const Infrastructure = new WrappedKVNamespace(
kvOrLocal(self.KV_INFRASTRUCTURE ?? null)
);

View file

@ -7,9 +7,10 @@ module.exports = {
'BOT_CLIENT_ID',
'BOT_CLIENT_SECRET',
'BOT_TOKEN',
'BOT_HEARTBEAT_TOKEN',
'UI_PUBLIC_URI',
'API_PUBLIC_URI',
'ROOT_USERS',
]),
kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA'],
kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA', 'KV_INFRASTRUCTURE'],
};