Resource fixes!
[browser-rts.git] / src / api.ts
1 import { HttpServer } from './lib/server';
2 import * as config from './config';
3 import { AccountRepository } from './repository/accounts';
4 import { CityRepository } from './repository/city';
5 import { MailRepository } from './repository/mail';
6 import {BadInputError, ERROR_CODE, NotFoundError} from './errors';
7 import { renderKingomOverview } from './render/kingdom-overview';
8 import { renderLandDevelopment } from './render/land-development';
9 import { tick } from './tasks/tick';
10 import { construction } from './tasks/construction';
11 import { unitTraining } from './tasks/unit-training';
12 import { fight } from './tasks/fight';
13 import { renderUnitTraining } from './render/unit-training';
14 import { launchOffensive, listOperations, renderOverworldMap } from './render/map';
15 import { createBullBoard } from '@bull-board/api';
16 import { BullAdapter } from '@bull-board/api/bullAdapter';
17 import _ from 'lodash';
18 import { renderCost } from './render/costs';
19 import {renderMailroom, renderMessage} from './render/mail';
20 import {topbar} from './render/topbar';
21
22 const server = new HttpServer(config.API_PORT);
23
24 const accountRepo = new AccountRepository();
25 const cityRepo = new CityRepository();
26 const mailRepo = new MailRepository();
27
28 createBullBoard({
29         queues: [
30                 new BullAdapter(tick.queue),
31                 new BullAdapter(construction.queue),
32                 new BullAdapter(unitTraining.queue),
33                 new BullAdapter(fight.queue)
34         ],
35         serverAdapter: server.bullAdapter,
36 });
37
38 server.post<{
39         body: {username: string, password: string}}, 
40         string
41         >
42         ('/accounts', async (req) => {
43         const { username, password} = req.body;
44         if(!username || !password || username.length < 3 || password.length < 3) {
45                 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
46         }
47         const acct = await accountRepo.create(username, password);
48
49         // lets create the city!
50         await cityRepo.create(acct.id);
51
52         return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
53 });
54
55 server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
56         const { username, password} = req.body;
57         if(!username || !password || username.length < 3 || password.length < 3) {
58                 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
59         }
60         const {account, session} = await accountRepo.login(username, password);
61         if(!account) {
62                 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
63         }
64
65         res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
66
67 });
68
69 server.post<{body: {
70         soldiers: number,
71         attackers: number,
72         defenders: number,
73         sp_attackers: number,
74         sp_defenders: number
75   }}, string>('/attack-power', async req => {
76         const power = await cityRepo.power(req.body);
77
78         return power.toLocaleString();
79
80 });
81
82 server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
83         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
84         const yourCity = await cityRepo.getUsersCity(account.id);
85         const city = await cityRepo.findById(req.params.cityId);
86         const acct = await accountRepo.FindOne({id: city.owner});
87
88
89         return await launchOffensive(city, acct || {
90                 id: '-',
91                 username: 'Rebels',
92                 password: ''
93         }, yourCity, account);
94 });
95
96 server.get<{}, string>('/poll/overview', async req => {
97         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
98         const city = await cityRepo.getUsersCity(account.id);
99
100   const usage = {
101     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
102     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
103     energyUsagePerTick: 0,
104     energyProductionPerTick: 0
105   }
106
107         return renderKingomOverview({
108     ...city,
109     ...usage
110   }, account) + topbar({...city, ...usage});
111 });
112
113 server.get<{}, string>('/poll/construction', async req => {
114         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
115         const city = await cityRepo.getUsersCity(account.id);
116         const buildings = await cityRepo.buildingRepository.list();
117
118         const buildQueues = await cityRepo.getBuildQueues(account.id);
119   const usage = {
120     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
121     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
122     energyUsagePerTick: 0,
123     energyProductionPerTick: 0
124   }
125         return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage});
126 });
127
128 server.get<{}, string>('/poll/unit-training', async req => {
129         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
130         const city = await cityRepo.getUsersCity(account.id);
131
132         const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
133         const units = await cityRepo.unitRepository.list();
134   const usage = {
135     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
136     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
137     energyUsagePerTick: 0,
138     energyProductionPerTick: 0
139   }
140
141         return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
142     ...city,
143     ...usage
144   });
145 });
146
147 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
148         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
149         const city = await cityRepo.getUsersCity(account.id);
150
151   let sector = city.sector_id;
152   if(req.body.sector) {
153     try {
154       sector = parseInt(req.body.sector);
155     }
156     catch(e) {
157       sector = city.sector_id;
158     }
159   }
160
161   const usage = {
162     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
163     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
164     energyUsagePerTick: 0,
165     energyProductionPerTick: 0
166   }
167
168         return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
169     ...city,
170     ...usage
171   });
172 });
173
174 server.get<{}, string>('/poll/mailroom', async req => {
175         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
176         const city = await cityRepo.getUsersCity(account.id);
177
178   const usage = {
179     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
180     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
181     energyUsagePerTick: 0,
182     energyProductionPerTick: 0
183   }
184
185         return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
186     ...city,
187     ...usage
188   });
189 });
190
191
192 server.post<{
193         body: {
194                 amount: string,
195                 building_type: string
196         }
197 }, string>('/cost/construction', async req => {
198         const amount = parseInt(req.body.amount.trim(), 10);
199
200         if(isNaN(amount) || amount < 1) {
201                 return '';
202         }
203         const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
204
205         if(!building) {
206                 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
207         }
208
209   const cost = {
210                 credits: building.credits * amount,
211                 alloys: building.alloys * amount,
212                 energy: building.energy * amount,
213                 land: building.land * amount,
214                 time: building.time
215   };
216
217   return renderCost(cost);
218 });
219
220 server.post<{
221         body: {
222                 amount: string;
223                 type: string;
224         }
225 }, string>('/cost/training', async req => {
226         const amount = parseInt(req.body.amount, 10);
227
228         if(isNaN(amount) || amount < 1) {
229                 return '';
230         }
231
232         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
233         if(!unit) {
234                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
235         }
236
237         return renderCost({
238                 population: unit.population * amount,
239                 soldiers: unit.soldiers * amount,
240                 attackers: unit.attackers * amount,
241                 defenders: unit.defenders * amount,
242                 credits: unit.credits * amount,
243                 food: unit.food * amount,
244     time: unit.time * amount
245         });
246 });
247
248 server.post<{
249         body: {
250                 amount: string,
251                 building_type: string,
252         }
253 }, void>('/build', async req => {
254   const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
255   const city = await cityRepo.getUsersCity(account.id);
256
257   const amount = parseInt(req.body.amount, 10);
258   const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
259
260   if(!building) {
261     throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
262   }
263
264   const queueData = await cityRepo.buildBuilding(building, amount, city);
265
266         construction.trigger(queueData, { delay: queueData.due });
267 }, 'reload-construction-queue');
268
269 server.post<{
270                 body: {
271                         amount: string,
272                         type: string
273                 }
274         },
275         void
276         >('/units', async req => {
277         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
278         const city = await cityRepo.getUsersCity(acct.id);
279
280         const amount  = parseInt(req.body.amount, 10);
281         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
282
283         if(!unit) {
284                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
285         }
286
287         const queueData = await cityRepo.train(unit, amount, city);
288         unitTraining.trigger(queueData, { delay: queueData.due });
289
290 }, 'reload-unit-training');
291
292 server.post<{
293         body: {
294                 city: string,
295                 soldiers: string,
296                 attackers: string,
297                 defenders: string,
298                 sp_attackers: string,
299                 sp_defenders: string
300         }
301         }, 
302         void
303         >('/attack', async req => {
304                 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
305                 const city = await cityRepo.getUsersCity(acct.id);
306                 const attackedCity = await cityRepo.findById(req.body.city);
307
308                 const army = {
309                         soldiers: parseInt(req.body.soldiers),
310                         attackers: parseInt(req.body.attackers),
311                         defenders: parseInt(req.body.defenders),
312                         sp_attackers: parseInt(req.body.sp_attackers),
313                         sp_defenders: parseInt(req.body.sp_defenders)
314                 };
315
316                 const armyQueue = await cityRepo.attack(city, attackedCity, army);
317
318                 fight.trigger(armyQueue, {
319                         delay: armyQueue.due - Date.now()
320                 });
321         }, 'reload-outgoing-attacks');
322
323 server.get<void, string>('/messages', async req => {
324         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
325         const msgs = await mailRepo.listReceivedMessages(acct.id);
326
327         return JSON.stringify(msgs);
328 });
329
330 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
331         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
332         const msg = await mailRepo.getMessage(req.params.id, acct.id);
333
334         if(!msg) {
335                 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
336         }
337
338         await mailRepo.markAsRead(msg.id, msg.to_account);
339
340         return renderMessage(msg);
341 });
342
343 server.get<void, string>('/attacks/outgoing', async req => {
344         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
345         const city = await cityRepo.getUsersCity(acct.id);
346         const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
347
348         return listOperations(attacks);
349 });
350
351
352 server.start();