bug: things were scheduling out to 2074
[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 import {renderPublicChatMessage} from './render/chat-message';
22
23
24 const server = new HttpServer(config.API_PORT);
25 const accountRepo = new AccountRepository();
26 const cityRepo = new CityRepository();
27 const mailRepo = new MailRepository();
28
29 const msgBuffer: string[] = [];
30
31 createBullBoard({
32         queues: [
33                 new BullAdapter(tick.queue),
34                 new BullAdapter(construction.queue),
35                 new BullAdapter(unitTraining.queue),
36                 new BullAdapter(fight.queue)
37         ],
38         serverAdapter: server.bullAdapter,
39 });
40
41 server.post<{
42         body: {username: string, password: string}}, 
43         string
44         >
45         ('/accounts', async (req) => {
46         const { username, password} = req.body;
47         if(!username || !password || username.length < 3 || password.length < 3) {
48                 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
49         }
50         const acct = await accountRepo.create(username, password);
51
52         // lets create the city!
53         await cityRepo.create(acct.id);
54
55         return `<div class="alert success">You are all signed up! You can go ahead and log in</div>`;
56 });
57
58 server.post<{body: {username: string, password: string}}, void>('/login', async (req, raw, res) => {
59         const { username, password} = req.body;
60         if(!username || !password || username.length < 3 || password.length < 3) {
61                 throw new BadInputError('Invalid username or password', ERROR_CODE.INVALID_USERNAME);
62         }
63         const {account, session} = await accountRepo.login(username, password);
64         if(!account) {
65                 throw new NotFoundError('Invalid username or password', ERROR_CODE.USER_NOT_FOUND);
66         }
67
68         res.setHeader('hx-redirect', `/game.html?token=${session.id}&id=${session.account_id}`);
69
70 });
71
72 server.post<{body: {
73         soldiers: number,
74         attackers: number,
75         defenders: number,
76         sp_attackers: number,
77         sp_defenders: number
78   }}, string>('/attack-power', async req => {
79         const power = await cityRepo.power(req.body);
80
81         return power.toLocaleString();
82
83 });
84
85 server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
86         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
87         const yourCity = await cityRepo.getUsersCity(account.id);
88         const city = await cityRepo.findById(req.params.cityId);
89         const acct = await accountRepo.FindOne({id: city.owner});
90
91
92         return await launchOffensive(city, acct || {
93                 id: '-',
94                 username: 'Rebels',
95                 password: ''
96         }, yourCity, account);
97 });
98
99 server.get<{}, string>('/poll/overview', async req => {
100         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
101         const city = await cityRepo.getUsersCity(account.id);
102
103   const usage = {
104     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
105     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
106     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
107     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
108   }
109
110         return renderKingomOverview({
111     ...city,
112     ...usage
113   }, account) + topbar({...city, ...usage});
114 });
115
116 server.get<{}, string>('/poll/construction', async req => {
117         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
118         const city = await cityRepo.getUsersCity(account.id);
119         const buildings = await cityRepo.buildingRepository.list();
120
121         const buildQueues = await cityRepo.getBuildQueues(account.id);
122   const usage = {
123     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
124     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
125     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
126     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
127   }
128         return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage});
129 });
130
131 server.get<{}, string>('/poll/unit-training', async req => {
132         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
133         const city = await cityRepo.getUsersCity(account.id);
134
135         const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
136         const units = await cityRepo.unitRepository.list();
137   const usage = {
138     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
139     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
140     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
141     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
142   }
143
144         return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
145     ...city,
146     ...usage
147   });
148 });
149
150 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
151         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
152         const city = await cityRepo.getUsersCity(account.id);
153
154   let sector = city.sector_id;
155   if(req.body.sector) {
156     try {
157       sector = parseInt(req.body.sector);
158     }
159     catch(e) {
160       sector = city.sector_id;
161     }
162   }
163
164   const usage = {
165     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
166     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
167     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
168     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
169   }
170
171         return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
172     ...city,
173     ...usage
174   });
175 });
176
177 server.get<{}, string>('/poll/mailroom', async req => {
178         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
179         const city = await cityRepo.getUsersCity(account.id);
180
181   const usage = {
182     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
183     foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
184     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
185     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
186   }
187
188         return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
189     ...city,
190     ...usage
191   });
192 });
193
194
195 server.post<{
196         body: {
197                 amount: string,
198                 building_type: string
199         }
200 }, string>('/cost/construction', async req => {
201         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
202         const city = await cityRepo.getUsersCity(account.id);
203
204         const amount = parseInt(req.body.amount.trim(), 10);
205
206         if(isNaN(amount) || amount < 1) {
207                 return '';
208         }
209         const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
210
211         if(!building) {
212                 throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
213         }
214
215   const cost = {
216                 credits: building.credits * amount,
217                 alloys: building.alloys * amount,
218                 energy: building.energy * amount,
219                 land: building.land * amount,
220                 time: building.time
221   };
222
223   return renderCost(cost, city);
224 });
225
226 server.post<{
227         body: {
228                 amount: string;
229                 type: string;
230         }
231 }, string>('/cost/training', async req => {
232         const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
233         const city = await cityRepo.getUsersCity(account.id);
234         const amount = parseInt(req.body.amount, 10);
235
236         if(isNaN(amount) || amount < 1) {
237                 return '';
238         }
239
240         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
241         if(!unit) {
242                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
243         }
244
245         return renderCost({
246                 population: unit.population * amount,
247                 soldiers: unit.soldiers * amount,
248                 attackers: unit.attackers * amount,
249                 defenders: unit.defenders * amount,
250                 credits: unit.credits * amount,
251                 food: unit.food * amount,
252     time: unit.time * amount
253         }, city);
254 });
255
256 server.post<{
257         body: {
258                 amount: string,
259                 building_type: string,
260         }
261 }, void>('/build', async req => {
262   const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
263   const city = await cityRepo.getUsersCity(account.id);
264
265   const amount = parseInt(req.body.amount, 10);
266   if(amount < 1) {
267     throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
268   }
269   const building = await cityRepo.buildingRepository.findBySlug(req.body.building_type);
270
271   if(!building) {
272     throw new NotFoundError(`Invalid building type ${req.body.building_type}`, ERROR_CODE.INVALID_BUILDING);
273   }
274
275   const queueData = await cityRepo.buildBuilding(building, amount, city);
276
277         construction.trigger(queueData, { delay: queueData.due - Date.now() });
278 }, 'reload-construction-queue');
279
280 server.post<{
281                 body: {
282                         amount: string,
283                         type: string
284                 }
285         },
286         void
287         >('/units', async req => {
288         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
289         const city = await cityRepo.getUsersCity(acct.id);
290
291         const amount  = parseInt(req.body.amount, 10) || 0;
292   console.log('request amount?!', amount);
293   if(amount < 1) {
294     throw new BadInputError('Please specify an amount > 0', ERROR_CODE.INVALID_AMOUNT);
295   }
296         const unit = await cityRepo.unitRepository.findBySlug(req.body.type);
297
298         if(!unit) {
299                 throw new NotFoundError(`Invalid unit type ${req.body.type}`, ERROR_CODE.INVALID_UNIT);
300         }
301
302         const queueData = await cityRepo.train(unit, amount, city);
303         unitTraining.trigger(queueData, { delay: queueData.due - Date.now() });
304
305 }, 'reload-unit-training');
306
307 server.post<{
308         body: {
309                 city: string,
310                 soldiers: string,
311                 attackers: string,
312                 defenders: string,
313                 sp_attackers: string,
314                 sp_defenders: string
315         }
316         }, 
317         void
318         >('/attack', async req => {
319                 const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
320                 const city = await cityRepo.getUsersCity(acct.id);
321                 const attackedCity = await cityRepo.findById(req.body.city);
322
323                 const army = {
324                         soldiers: parseInt(req.body.soldiers),
325                         attackers: parseInt(req.body.attackers),
326                         defenders: parseInt(req.body.defenders),
327                         sp_attackers: parseInt(req.body.sp_attackers),
328                         sp_defenders: parseInt(req.body.sp_defenders)
329                 };
330
331                 const armyQueue = await cityRepo.attack(city, attackedCity, army);
332
333                 fight.trigger(armyQueue, {
334                         delay: armyQueue.due - Date.now()
335                 });
336         }, 'reload-outgoing-attacks');
337
338 server.get<void, string>('/messages', async req => {
339         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
340         const msgs = await mailRepo.listReceivedMessages(acct.id);
341
342         return JSON.stringify(msgs);
343 });
344
345 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
346         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
347         const msg = await mailRepo.getMessage(req.params.id, acct.id);
348
349         if(!msg) {
350                 throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
351         }
352
353         await mailRepo.markAsRead(msg.id, msg.to_account);
354
355         return renderMessage(msg);
356 });
357
358 server.get<void, string>('/attacks/outgoing', async req => {
359         const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
360         const city = await cityRepo.getUsersCity(acct.id);
361         const attacks = await cityRepo.armyRepository.listOutgoing(city.id);
362
363         return listOperations(attacks);
364 });
365
366 server.post<{body: {message: string}}, void>('/chat', async req => {
367   const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
368   const now = Date.now();
369
370   if(!_.isEmpty(req.body.message)) {
371     const msg = renderPublicChatMessage(acct.username, req.body.message);
372     server.ws.emit('/chat-message', msg);
373     msgBuffer.unshift(msg);
374     while(msgBuffer.length > 30) {
375       msgBuffer.pop();
376     }
377   }
378
379   return;
380 });
381
382 server.get<void, string>('/server-stats', async req => {
383   const date = new Date();
384   return `
385   <div class="text-right">
386     <span class="success-text">${(await server.ws.allSockets()).size} Online</span><br>
387     <span>
388     Server Time: ${date.getHours()}:${date.getMinutes()}
389     </span>
390   </div>`;
391 });
392
393 server.ws.on('connection', async socket => {
394   const auth = server.authFromUrl(socket.request.headers['referer']);
395   const acct = await accountRepo.validate(auth.authInfo.accountId, auth.authInfo.token);
396
397   server.ws.emit('/chat-message', msgBuffer.join("\n"));
398
399   server.ws.emit('/chat-message', renderPublicChatMessage('Server', `${acct.username} logged in`));
400
401 });
402
403 server.start();
404
405 tick.trigger({
406   lastTickAt: 0,
407   lastTick: 0
408 }, {
409   repeat: {
410     cron: '0 * * * *'
411   }
412 });