259 lines
7.1 KiB
TypeScript
259 lines
7.1 KiB
TypeScript
import assert from 'assert/strict';
|
|
|
|
import type Log from 'bunyan';
|
|
|
|
import type { Want, Offer, User } from '@prisma/client';
|
|
|
|
import type { Pg } from './pg';
|
|
import type { Discogs } from './discogs';
|
|
import type { Offers } from './offers';
|
|
|
|
import { Status } from './offers';
|
|
import { isAdmin } from './utils';
|
|
import { Events } from './events';
|
|
|
|
export type Rpc = ReturnType<typeof createRpc>;
|
|
|
|
export enum RpcMessageType {
|
|
Ping = 'Ping',
|
|
Pong = 'Pong',
|
|
|
|
// points out user is anon
|
|
WhoDis = 'WhoDis',
|
|
|
|
Bundles = 'Bundles',
|
|
Offers = 'Offers',
|
|
List = 'List',
|
|
Wants = 'Wants',
|
|
User = 'User',
|
|
|
|
Error = 'Error',
|
|
|
|
OfferSamples = 'OfferSamples',
|
|
OfferStatusSet = 'OfferStatusSet',
|
|
|
|
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
|
AcpOffers = 'AcpOffers',
|
|
AcpListingsSync = 'AcpListingsSync',
|
|
|
|
WantRm = 'WantRm',
|
|
WantAdd = 'WantAdd',
|
|
}
|
|
|
|
type ErrorMessage = [RpcMessageType.Error, string];
|
|
type PongMessage = [RpcMessageType.Pong, []];
|
|
type PingMessage = [RpcMessageType.Ping, []];
|
|
type WhoDisMessage = [RpcMessageType.WhoDis, []];
|
|
type UserMessage = [RpcMessageType.User, User];
|
|
type WantsMessage = [RpcMessageType.Wants, Want[]];
|
|
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
|
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
|
type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]];
|
|
|
|
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
|
|
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
|
|
type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []];
|
|
|
|
export type RpcMessage =
|
|
PingMessage |
|
|
PongMessage |
|
|
WhoDisMessage |
|
|
|
|
UserMessage |
|
|
|
|
WantsMessage |
|
|
|
|
OffersMessage |
|
|
OfferSamplesMessage |
|
|
OfferStatusSetMessage |
|
|
|
|
AcpOfferStatusSetMessage |
|
|
AcpOffersMessage |
|
|
AcpListingsSyncMessage |
|
|
|
|
ErrorMessage;
|
|
|
|
|
|
type OfferStatusSetParams = {
|
|
id: UUID,
|
|
status: Status,
|
|
};
|
|
|
|
type WantRmParams = {
|
|
id: UUID,
|
|
};
|
|
|
|
type WantAddParams = {
|
|
releaseId: number,
|
|
};
|
|
|
|
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
|
|
if (RpcMessageType[msgType]) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// rpc is created per connection from the api
|
|
export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers: Offers, events, user: User | null) {
|
|
async function ping(): Promise<PongMessage> {
|
|
return [RpcMessageType.Pong, []];
|
|
}
|
|
|
|
async function userOffers(): Promise<OffersMessage> {
|
|
return [RpcMessageType.Offers, await offers.userOffers(user)];
|
|
}
|
|
|
|
async function userWants(): Promise<WantsMessage> {
|
|
return [RpcMessageType.Wants, await pg.userWants(user)];
|
|
}
|
|
|
|
async function offerStatusSet(params: OfferStatusSetParams): Promise<OffersMessage> {
|
|
const { id, status } = params;
|
|
assert(id, 'no id supplied');
|
|
assert(Status[status], 'unknown status');
|
|
|
|
const offer = await offers.statusSet(user, id, status);
|
|
const userOffers = await offers.userOffers(user);
|
|
|
|
events.emit(Events.DiscordMessage, { msg: 'user offer event', user: user.email, offer })
|
|
|
|
return [RpcMessageType.Offers, userOffers]
|
|
}
|
|
|
|
async function wantRm(params: WantRmParams): Promise<WantsMessage> {
|
|
const { id } = params;
|
|
assert(id, 'no id supplied');
|
|
|
|
await pg.wantRm(id);
|
|
|
|
const wants = await pg.userWants(user);
|
|
return [RpcMessageType.Wants, wants];
|
|
}
|
|
|
|
async function wantAdd(params: WantAddParams): Promise<WantsMessage> {
|
|
const { releaseId } = params;
|
|
assert(releaseId, 'no id supplied');
|
|
|
|
const release = await discogs.release(releaseId);
|
|
const want = await pg.wantCreate(release, user);
|
|
events.emit(Events.WantsAdded, [want]);
|
|
|
|
const wants = await pg.userWants(user);
|
|
return [RpcMessageType.Wants, wants];
|
|
}
|
|
|
|
async function acpOfferStatusSet(params: OfferStatusSetParams): Promise<AcpOffersMessage> {
|
|
const { id, status } = params;
|
|
assert(isAdmin(user), 'not authorised');
|
|
assert(id, 'no id supplied');
|
|
assert(Status[status], 'unknown status');
|
|
|
|
const offer = await offers.statusSet(user, id, status);
|
|
|
|
return [RpcMessageType.AcpOffers, await offers.acpOffers()]
|
|
}
|
|
|
|
async function acpOffers(): Promise<AcpOffersMessage> {
|
|
assert(isAdmin(user), 'not authorised');
|
|
return [RpcMessageType.AcpOffers, await offers.acpOffers()];
|
|
}
|
|
|
|
async function acpListingsSync(): Promise<AcpListingsSyncMessage> {
|
|
assert(isAdmin(user), 'not authorised');
|
|
events.emit(Events.Sync);
|
|
return [RpcMessageType.AcpListingsSync, []];
|
|
}
|
|
|
|
async function receive(message: any): Promise<RpcMessage> {
|
|
const [msgType, params] = message;
|
|
|
|
if (!user) {
|
|
log.debug('anon rpc call');
|
|
return [RpcMessageType.WhoDis, []];
|
|
}
|
|
|
|
if (msgType !== RpcMessageType.Ping) log.info({ msgType, params }, 'rpc message');
|
|
|
|
// // this sucks but can't figure out how to convince typescript to agree yet
|
|
if (checkMsgType(msgType)) {
|
|
switch (msgType) {
|
|
case RpcMessageType.Ping:
|
|
return ping();
|
|
|
|
case RpcMessageType.Offers:
|
|
return userOffers();
|
|
case RpcMessageType.Wants:
|
|
return userWants();
|
|
case RpcMessageType.OfferStatusSet:
|
|
return offerStatusSet(params);
|
|
|
|
case RpcMessageType.WantRm:
|
|
return wantRm(params);
|
|
case RpcMessageType.WantAdd:
|
|
return wantAdd(params);
|
|
|
|
case RpcMessageType.AcpOfferStatusSet:
|
|
return acpOfferStatusSet(params);
|
|
case RpcMessageType.AcpOffers:
|
|
return acpOffers();
|
|
case RpcMessageType.AcpListingsSync:
|
|
return acpListingsSync();
|
|
|
|
default:
|
|
throw new Error(`Message type ${msgType} is known but unhandled`);
|
|
}
|
|
} else {
|
|
throw new Error(`Unknown message type ${msgType}`);
|
|
}
|
|
|
|
throw new Error(`Invalid parameters for message type ${msgType}: ${JSON.stringify(message)}`);
|
|
}
|
|
|
|
async function anonConnection() {
|
|
|
|
}
|
|
|
|
async function adminConnection() {
|
|
// don't need it, requested on route
|
|
// send(await acpOffers());
|
|
}
|
|
|
|
async function onConnection() {
|
|
if (!user) return anonConnection();
|
|
|
|
const userMsg: UserMessage = [RpcMessageType.User, user];
|
|
send(userMsg);
|
|
|
|
await pushUserOffers();
|
|
await pushUserWants();
|
|
|
|
if (isAdmin(user)) return adminConnection();
|
|
}
|
|
|
|
function send(msg: RpcMessage) {
|
|
ws.send(JSON.stringify(msg, (key, value) =>
|
|
typeof value === 'bigint'
|
|
? value.toString()
|
|
: value // return everything else unchanged
|
|
))
|
|
}
|
|
|
|
async function pushUserOffers() {
|
|
send(await userOffers());
|
|
}
|
|
|
|
async function pushUserWants() {
|
|
send(await userWants());
|
|
}
|
|
|
|
return {
|
|
receive,
|
|
send,
|
|
onConnection,
|
|
|
|
pushUserOffers,
|
|
pushUserWants,
|
|
}
|
|
}
|