mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Move HTTP server functions to a separate class
This commit is contained in:
parent
9cb4ced59f
commit
516349e5df
|
|
@ -17,6 +17,7 @@ import { AuthPolicy, AuthToken, ZncPresenceEventStreamEvent } from '../../api/zn
|
|||
import { addCliFeatureUserAgent } from '../../util/useragent.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
import Users, { CoralUser } from '../../common/users.js';
|
||||
import { EventStreamResponse, HttpServer, ResponseError } from '../util/http-server.js';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
|
|
@ -90,7 +91,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
|
||||
const FRIEND_CODE = /^\d{4}-\d{4}-\d{4}$/;
|
||||
|
||||
class Server {
|
||||
class Server extends HttpServer {
|
||||
require_token = true;
|
||||
update_interval = 30 * 1000;
|
||||
|
||||
|
|
@ -104,6 +105,8 @@ class Server {
|
|||
readonly storage: persist.LocalStorage,
|
||||
readonly users: Users<CoralUser>,
|
||||
) {
|
||||
super();
|
||||
|
||||
const app = this.app = express();
|
||||
|
||||
app.use('/api/znc', (req, res, next) => {
|
||||
|
|
@ -118,64 +121,64 @@ class Server {
|
|||
next();
|
||||
});
|
||||
|
||||
app.get('/api/znc/auth', this.createApiRequestHandler(r => this.handleAuthRequest(r), true));
|
||||
app.get('/api/znc/auth', this.createProxyRequestHandler(r => this.handleAuthRequest(r), true));
|
||||
|
||||
app.get('/api/znc/token', this.authTokenMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleTokenRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleTokenRequest(r)));
|
||||
app.delete('/api/znc/token', this.authTokenMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleDeleteTokenRequest(r)));
|
||||
app.get('/api/znc/tokens', this.createApiRequestHandler(r => this.handleTokensRequest(r), true));
|
||||
this.createProxyRequestHandler(r => this.handleDeleteTokenRequest(r)));
|
||||
app.get('/api/znc/tokens', this.createProxyRequestHandler(r => this.handleTokensRequest(r), true));
|
||||
app.post('/api/znc/tokens', bodyParser.json(),
|
||||
this.createApiRequestHandler(r => this.handleCreateTokenRequest(r), true));
|
||||
this.createProxyRequestHandler(r => this.handleCreateTokenRequest(r), true));
|
||||
|
||||
app.get('/api/znc/announcements', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleCreateTokenRequest(r), true));
|
||||
this.createProxyRequestHandler(r => this.handleCreateTokenRequest(r), true));
|
||||
|
||||
app.get('/api/znc/user', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleCurrentUserRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleCurrentUserRequest(r)));
|
||||
app.get('/api/znc/user/presence', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleUserPresenceRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleUserPresenceRequest(r)));
|
||||
|
||||
app.get('/api/znc/friends', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFriendsRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleFriendsRequest(r)));
|
||||
app.get('/api/znc/friends/favourites', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFavouriteFriendsRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleFavouriteFriendsRequest(r)));
|
||||
app.get('/api/znc/friends/presence', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFriendsPresenceRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleFriendsPresenceRequest(r)));
|
||||
app.get('/api/znc/friends/favourites/presence', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFavouriteFriendsPresenceRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleFavouriteFriendsPresenceRequest(r)));
|
||||
|
||||
app.get('/api/znc/friend/:nsaid', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFriendRequest(r, r.req.params.nsaid)));
|
||||
this.createProxyRequestHandler(r => this.handleFriendRequest(r, r.req.params.nsaid)));
|
||||
app.post('/api/znc/friend/:nsaid', bodyParser.json(),
|
||||
this.createApiRequestHandler(r => this.handleUpdateFriendRequest(r, r.req.params.nsaid), true));
|
||||
this.createProxyRequestHandler(r => this.handleUpdateFriendRequest(r, r.req.params.nsaid), true));
|
||||
app.get('/api/znc/friend/:nsaid/presence', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFriendPresenceRequest(r, r.req.params.nsaid)));
|
||||
this.createProxyRequestHandler(r => this.handleFriendPresenceRequest(r, r.req.params.nsaid)));
|
||||
|
||||
app.get('/api/znc/webservices', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleWebServicesRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleWebServicesRequest(r)));
|
||||
app.get('/api/znc/webservice/:id/token',
|
||||
this.createApiRequestHandler(r => this.handleWebServiceTokenRequest(r, r.req.params.id), true));
|
||||
this.createProxyRequestHandler(r => this.handleWebServiceTokenRequest(r, r.req.params.id), true));
|
||||
app.get('/api/znc/activeevent', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleActiveEventRequest(r)));
|
||||
this.createProxyRequestHandler(r => this.handleActiveEventRequest(r)));
|
||||
|
||||
app.get('/api/znc/event/:id',
|
||||
this.createApiRequestHandler(r => this.handleEventRequest(r, r.req.params.id), true));
|
||||
this.createProxyRequestHandler(r => this.handleEventRequest(r, r.req.params.id), true));
|
||||
app.get('/api/znc/user/:id',
|
||||
this.createApiRequestHandler(r => this.handleUserRequest(r, r.req.params.id), true));
|
||||
this.createProxyRequestHandler(r => this.handleUserRequest(r, r.req.params.id), true));
|
||||
|
||||
app.get('/api/znc/friendcode/:friendcode', this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFriendCodeRequest(r, r.req.params.friendcode), true));
|
||||
this.createProxyRequestHandler(r => this.handleFriendCodeRequest(r, r.req.params.friendcode), true));
|
||||
app.get('/api/znc/friendcode', this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handleFriendCodeUrlRequest(r), true));
|
||||
this.createProxyRequestHandler(r => this.handleFriendCodeUrlRequest(r), true));
|
||||
|
||||
app.get('/api/znc/presence/events', this.localAuthMiddleware,
|
||||
this.createApiRequestHandler(r => this.handlePresenceEventStreamRequest(r), true));
|
||||
this.createProxyRequestHandler(r => this.handlePresenceEventStreamRequest(r), true));
|
||||
}
|
||||
|
||||
protected createApiRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth: true): RequestHandler
|
||||
protected createApiRequestHandler(callback: (data: RequestData) => Promise<{} | void>, auth?: boolean): RequestHandler
|
||||
protected createApiRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth = false) {
|
||||
protected createProxyRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth: true): RequestHandler
|
||||
protected createProxyRequestHandler(callback: (data: RequestData) => Promise<{} | void>, auth?: boolean): RequestHandler
|
||||
protected createProxyRequestHandler(callback: (data: RequestDataWithUser) => Promise<{} | void>, auth = false) {
|
||||
return async (req: Request, res: Response) => {
|
||||
try {
|
||||
const user = req.coralUser ?? auth ? await this.getCoralUser(req) : undefined;
|
||||
|
|
@ -202,34 +205,6 @@ class Server {
|
|||
};
|
||||
}
|
||||
|
||||
protected createApiMiddleware(
|
||||
callback: (req: Request, res: Response) => Promise<void>
|
||||
): RequestHandler {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await callback.call(null, req, res);
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
if (err instanceof ResponseError) {
|
||||
err.sendResponse(req, res);
|
||||
} else {
|
||||
this.sendJsonResponse(res, {
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected sendJsonResponse(res: Response, data: {}, status?: number) {
|
||||
if (status) res.statusCode = status;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(res.req.headers['accept']?.match(/\/html\b/i) ?
|
||||
JSON.stringify(data, null, 4) : JSON.stringify(data));
|
||||
}
|
||||
|
||||
protected async _cache<T>(
|
||||
id: string, callback: () => Promise<T>,
|
||||
promises: Map<string, Promise<[number, T]>>,
|
||||
|
|
@ -765,9 +740,6 @@ class Server {
|
|||
//
|
||||
|
||||
async handlePresenceEventStreamRequest({req, res, user}: RequestDataWithUser) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
|
||||
const na_session_token = req.headers['authorization']!.substr(3);
|
||||
const i = new ZncNotifications(this.storage, na_session_token, user.nso, user.data, user);
|
||||
|
||||
|
|
@ -775,7 +747,8 @@ class Server {
|
|||
i.friend_notifications = true;
|
||||
i.update_interval = this.update_interval / 1000;
|
||||
|
||||
const es = i.notifications = new EventStreamNotificationManager(req, res);
|
||||
const stream = new EventStreamResponse(req, res);
|
||||
i.notifications = new EventStreamNotificationManager(stream);
|
||||
|
||||
try {
|
||||
await i.loop(true);
|
||||
|
|
@ -786,7 +759,7 @@ class Server {
|
|||
this.resetAuthTimeout(na_session_token, () => user.data.user.id);
|
||||
}
|
||||
} catch (err) {
|
||||
es.sendEvent('error', {
|
||||
stream.sendEvent('error', {
|
||||
error: (err as Error).name,
|
||||
error_message: (err as Error).message,
|
||||
});
|
||||
|
|
@ -794,71 +767,44 @@ class Server {
|
|||
}
|
||||
}
|
||||
|
||||
class ResponseError extends Error {
|
||||
constructor(readonly status: number, readonly code: string, message?: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
sendResponse(req: Request, res: Response) {
|
||||
const data = {
|
||||
error: this.code,
|
||||
error_message: this.message,
|
||||
};
|
||||
|
||||
res.statusCode = this.status;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(req.headers['accept']?.match(/\/html\b/i) ?
|
||||
JSON.stringify(data, null, 4) : JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
function cacheMaxAge(updated_timestamp_ms: number, update_interval_ms: number) {
|
||||
return Math.floor(((updated_timestamp_ms + update_interval_ms) - Date.now()) / 1000);
|
||||
}
|
||||
|
||||
class EventStreamNotificationManager extends NotificationManager {
|
||||
constructor(
|
||||
public req: Request,
|
||||
public res: Response
|
||||
) {
|
||||
constructor(readonly stream: EventStreamResponse) {
|
||||
super();
|
||||
}
|
||||
|
||||
sendEvent(event: string | null, ...data: unknown[]) {
|
||||
if (event) this.res.write('event: ' + event + '\n');
|
||||
for (const d of data) this.res.write('data: ' + JSON.stringify(d) + '\n');
|
||||
this.res.write('\n');
|
||||
}
|
||||
|
||||
onPresenceUpdated(
|
||||
friend: CurrentUser | Friend, prev?: CurrentUser | Friend, type?: PresenceEvent,
|
||||
naid?: string, ir?: boolean
|
||||
) {
|
||||
this.sendEvent(ZncPresenceEventStreamEvent.PRESENCE_UPDATED, {
|
||||
this.stream.sendEvent(ZncPresenceEventStreamEvent.PRESENCE_UPDATED, {
|
||||
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
|
||||
});
|
||||
}
|
||||
|
||||
onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
|
||||
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_ONLINE, {
|
||||
this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_ONLINE, {
|
||||
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
|
||||
});
|
||||
}
|
||||
|
||||
onFriendOffline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
|
||||
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_OFFLINE, {
|
||||
this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_OFFLINE, {
|
||||
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
|
||||
});
|
||||
}
|
||||
|
||||
onFriendPlayingChangeTitle(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
|
||||
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_CHANGE, {
|
||||
this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_CHANGE, {
|
||||
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
|
||||
});
|
||||
}
|
||||
|
||||
onFriendTitleStateChange(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
|
||||
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_STATECHANGE, {
|
||||
this.stream.sendEvent(ZncPresenceEventStreamEvent.FRIEND_TITLE_STATECHANGE, {
|
||||
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import Users, { CoralUser } from '../common/users.js';
|
|||
import { Friend } from '../api/coral-types.js';
|
||||
import { getBulletToken, SavedBulletToken } from '../common/auth/splatnet3.js';
|
||||
import SplatNet3Api from '../api/splatnet3.js';
|
||||
import { HttpServer, ResponseError } from './util/http-server.js';
|
||||
|
||||
const debug = createDebug('cli:presence-server');
|
||||
|
||||
|
|
@ -172,7 +173,7 @@ export class SplatNet3User {
|
|||
}
|
||||
}
|
||||
|
||||
class Server {
|
||||
class Server extends HttpServer {
|
||||
allow_all_users = false;
|
||||
update_interval = 30 * 1000;
|
||||
|
||||
|
|
@ -184,6 +185,8 @@ class Server {
|
|||
readonly splatnet3_users: Users<SplatNet3User> | null,
|
||||
readonly user_ids: string[],
|
||||
) {
|
||||
super();
|
||||
|
||||
const app = this.app = express();
|
||||
|
||||
app.use('/api/presence', (req, res, next) => {
|
||||
|
|
@ -204,32 +207,6 @@ class Server {
|
|||
this.handlePresenceRequest(req, res, req.params.user)));
|
||||
}
|
||||
|
||||
sendJsonResponse(res: Response, data: {}, status?: number) {
|
||||
if (status) res.statusCode = status;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(res.req.headers['accept']?.match(/\/html\b/i) ?
|
||||
JSON.stringify(data, replacer, 4) : JSON.stringify(data, replacer));
|
||||
}
|
||||
|
||||
createApiRequestHandler(callback: (req: Request, res: Response) => Promise<{} | void>) {
|
||||
return async (req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await callback.call(null, req, res);
|
||||
|
||||
if (result) this.sendJsonResponse(res, result);
|
||||
else res.end();
|
||||
} catch (err) {
|
||||
if (err instanceof ResponseError) {
|
||||
err.sendResponse(req, res);
|
||||
} else {
|
||||
this.sendJsonResponse(res, {
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async handleAllUsersRequest(req: Request, res: Response) {
|
||||
if (!this.allow_all_users) {
|
||||
|
|
@ -467,24 +444,6 @@ class Server {
|
|||
}
|
||||
}
|
||||
|
||||
class ResponseError extends Error {
|
||||
constructor(readonly status: number, readonly code: string, message?: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
sendResponse(req: Request, res: Response) {
|
||||
const data = {
|
||||
error: this.code,
|
||||
error_message: this.message,
|
||||
};
|
||||
|
||||
res.statusCode = this.status;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(req.headers['accept']?.match(/\/html\b/i) ?
|
||||
JSON.stringify(data, null, 4) : JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
function createScheduleFest(
|
||||
fest: Fest_schedule, vote_team?: string, state?: FestVoteState | null
|
||||
): Fest_schedule {
|
||||
|
|
|
|||
89
src/cli/util/http-server.ts
Normal file
89
src/cli/util/http-server.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import createDebug from 'debug';
|
||||
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
|
||||
const debug = createDebug('cli:util:http-server');
|
||||
|
||||
export class HttpServer {
|
||||
protected createApiRequestHandler(callback: (req: Request, res: Response) => Promise<{} | void>, auth = false) {
|
||||
return async (req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await callback.call(null, req, res);
|
||||
|
||||
if (result) this.sendJsonResponse(res, result);
|
||||
else res.end();
|
||||
} catch (err) {
|
||||
if (err instanceof ResponseError) {
|
||||
err.sendResponse(req, res);
|
||||
} else {
|
||||
this.sendJsonResponse(res, {
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected createApiMiddleware(
|
||||
callback: (req: Request, res: Response) => Promise<void>
|
||||
): RequestHandler {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await callback.call(null, req, res);
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
if (err instanceof ResponseError) {
|
||||
err.sendResponse(req, res);
|
||||
} else {
|
||||
this.sendJsonResponse(res, {
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected sendJsonResponse(res: Response, data: {}, status?: number) {
|
||||
if (status) res.statusCode = status;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(res.req.headers['accept']?.match(/\/html\b/i) ?
|
||||
JSON.stringify(data, null, 4) : JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseError extends Error {
|
||||
constructor(readonly status: number, readonly code: string, message?: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
sendResponse(req: Request, res: Response) {
|
||||
const data = {
|
||||
error: this.code,
|
||||
error_message: this.message,
|
||||
};
|
||||
|
||||
res.statusCode = this.status;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(req.headers['accept']?.match(/\/html\b/i) ?
|
||||
JSON.stringify(data, null, 4) : JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
export class EventStreamResponse {
|
||||
constructor(
|
||||
readonly req: Request,
|
||||
readonly res: Response,
|
||||
) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
}
|
||||
|
||||
sendEvent(event: string | null, ...data: unknown[]) {
|
||||
if (event) this.res.write('event: ' + event + '\n');
|
||||
for (const d of data) this.res.write('data: ' + JSON.stringify(d) + '\n');
|
||||
this.res.write('\n');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user