import http from 'http'; import koa from 'koa'; import koaWebsocket from 'koa-websocket'; import bodyParser from 'koa-body'; import session from 'koa-session'; import koaRouter from '@koa/router' import cors from '@koa/cors'; import { groupBy, sortBy } from 'lodash'; import Bluebird from 'bluebird'; import type Log from 'bunyan'; import type { Listing, User, Want, Prisma } from '@prisma/client'; import { Pg } from './pg'; import routerAuth from './routers/router.auth'; import { Events } from './events'; import createRpc, { RpcMessageType } from './rpc'; export default function createApi(log: Log, pg: Pg, discogs, offers, events) { const PORT = 41337; const WS_PORT = 40055; const SECRET = 'ohja4Eif9eidah3aj5ta2haa4iemooth'; const app = koaWebsocket(new koa()); app.use(async (ctx, next) => { try { await next(); } catch (err) { log.error(err); ctx.status = err.statusCode || err.status || 500; ctx.body = { error: err.message }; } }); const router = koaRouter(); function listen() { const server = app.listen(PORT); log.info({ PORT }, 'listening'); } app.use(cors({ credentials: true })); app.use(bodyParser()) app.keys = [SECRET]; app.use(session(app)); const { authRouter, passport } = routerAuth(log, pg); app.use(passport.initialize()); app.use(passport.session()) app.use(authRouter.routes()); router.get('/health', async function health(ctx) { ctx.status = 200; }); router.get('/acp/bundles', async function bundlesGet(ctx, next) { const bundles = await offers.bundles(); ctx.response.body = bundles; }) router.get('/acp/offers', async function acpOffers(ctx, next) { const offers = await pg.offersAdmin(); const grouped = groupBy(offers, o => o.listing.data.seller.id); const bundles = sortBy(Object.values(grouped).map(offers => { const shipping = offers[0]?.listing.data.seller.shipping; const bundle = { username: offers[0]?.listing.data.seller.username, seller: offers[0]?.listing.data.seller.id, shipsFrom: offers[0]?.listing.data.ships_from, shipping, offers, mentionsAustralia: shipping.toLowerCase().includes('australia'), mentionsWorldwide: shipping.toLowerCase().includes('world'), } return bundle; }), bundle => -bundle.offers.length); ctx.response.body = bundles; }) router.post('/offers', async function postOffers(ctx, next) { const { listingIds, shipping, baseShipping } = ctx.request.body; ctx.assert(listingIds.length, 400, 'listingIds required'); ctx.assert([listingIds, shipping, baseShipping].every(i => i), 400, 'listingIds, shipping, baseShipping required'); ctx.response.body = await offers.generate(listingIds, shipping, baseShipping); }) router.post('/offer/accept', async function postOffers(ctx, next) { ctx.assert(ctx.isAuthenticated(), 401, 'log in required'); ctx.response.body = await offers.accept(ctx); }) router.get('/user/offers', async function userOffersGet(ctx, next) { ctx.assert(ctx.isAuthenticated(), 401, 'log in required'); const user = ctx.state.user; const offers = await pg.userOffers(user); ctx.response.body = offers; }); router.post('/want', async function wantPost(ctx, next) { ctx.assert(ctx.isAuthenticated(), 401, 'log in required'); const { releaseId } = ctx.request.body; const user = await pg.userFindId(ctx.state.user.id); const release = await discogs.release(releaseId); const want = await pg.wantCreate(release, user); events.emit(Events.WantAdded, want); ctx.response.body = [want]; }); router.post('/want/discogs', async function wantListPost(ctx, next) { ctx.assert(ctx.isAuthenticated(), 401, 'log in required'); const { username } = ctx.request.body; const user = await pg.userFindId(ctx.state.user.id); const wants = await discogs.wantListSync(username, user); wants.forEach(w => events.emit(Events.WantAdded, w)); ctx.response.body = wants; }); router.get('/want/all', async function wantGet(ctx, next) { ctx.assert(ctx.isAuthenticated(), 401, 'log in required'); const user = ctx.state.user; const wants = await pg.wantList(user); ctx.response.body = wants; }); router.post('/want/search', async function wantPost(ctx, next) { const { artist, title } = ctx.request.body; const results = await discogs.search(artist, title); ctx.response.body = results; }); router.post('/auth/logout', async (ctx, next) => { ctx.assert(ctx.isAuthenticated(), 401, 'log in required'); ctx.logout(); ctx.status = 200; ctx.body = { success: true }; }); app.use(router.routes()); app.ws.use(passport.initialize()); app.ws.use(passport.session()); app.ws.use(async (ctx, next) => { log.info('new ws connection', { user: ctx.state.user }); // log.child... const ws = ctx.websocket; const rpc = createRpc(log, pg, ctx.state.user, ws, discogs, offers); if (ctx.state.user) events.emit(Events.Connection, { ws, id: ctx.state.user.id }); ws.on('message', async function incoming(message) { try { const parsed = JSON.parse(message.toString()); const response = await rpc.receive(parsed); rpc.send(response); } catch (e) { log.warn(e); log.warn(message.toString()); rpc.send([RpcMessageType.Error, e.message || e.msg]); } return true; }); ws.on('close', function onClose() { log.info('ws disconnected', { user: ctx.state.user }); }); }); return { app, listen, }; }