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; 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 { return [RpcMessageType.Pong, []]; } async function userOffers(): Promise { return [RpcMessageType.Offers, await offers.userOffers(user)]; } async function userWants(): Promise { return [RpcMessageType.Wants, await pg.userWants(user)]; } async function offerStatusSet(params: OfferStatusSetParams): Promise { 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 { 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 { 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 { 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 { assert(isAdmin(user), 'not authorised'); return [RpcMessageType.AcpOffers, await offers.acpOffers()]; } async function acpListingsSync(): Promise { assert(isAdmin(user), 'not authorised'); events.emit(Events.Sync); return [RpcMessageType.AcpListingsSync, []]; } async function receive(message: any): Promise { 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, } }