-
Notifications
You must be signed in to change notification settings - Fork 22
/
index.js
150 lines (123 loc) · 4.79 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
const cacheManager = require('cache-manager');
const express = require('express');
const fetch = require('node-fetch');
const fsStore = require('cache-manager-fs-binary');
const rateLimit = require("express-rate-limit");
const { CardCreator } = require('./create-card');
const port = process.env.PORT || 5000;
const xivApiKey = typeof process.env.XIV_API_KEY === 'string' && process.env.XIV_API_KEY !== '' ? process.env.XIV_API_KEY : undefined;
const supportedLanguages = ['en', 'ja', 'de', 'fr'];
const app = express();
const creator = new CardCreator(xivApiKey);
// Initialize caching on disk
const diskCache = cacheManager.caching({
store: fsStore,
options: {
reviveBuffers: true,
binaryAsStream: false,
ttl: 14400, // s = 4h
maxsize: 1000000000, // bytes = 1 GB
path: 'diskcache',
preventfill: true,
}
});
// Rate limit all requests that result in XIV API calls
const limiter = rateLimit({
windowMs: 1000, // ms = 1s
max: 20, // default XIV API request limit
keyGenerator: () => 'global',
});
async function getCharacterIdByName(world, name, retries = 1) {
if (retries === -1) return undefined;
const searchUrl = new URL('https://xivapi.com/character/search');
searchUrl.searchParams.set('name', name)
searchUrl.searchParams.set('server', world)
if (xivApiKey != null) searchUrl.searchParams.set('private_key', xivApiKey)
const response = await fetch(searchUrl.toString());
const data = await response.json();
if (data.Results[0] === undefined) return getCharacterIdByName(world, name, --retries);
return data.Results[0].ID;
}
async function cacheCreateCard(characterId, customImage, language) {
const cacheKey = `img:${characterId}:${customImage}:${language}`;
return diskCache.wrap(cacheKey, async () => {
await creator.ensureInit().catch(error => { throw new Error(`Init failed with: ${error}`) });
const image = await creator.createCard(characterId, customImage, language).catch(error => { throw new Error(`Create card failed with: ${error}`) });
return {
binary: {
image,
},
};
});
}
function getOriginalQueryString(req) {
const url = new URL(req.originalUrl, 'http://example.org');
return url.search;
}
app.get('/prepare/id/:characterId', limiter, (req, res, next) => {
const language = typeof req.query.lang === 'string' && supportedLanguages.includes(req.query.lang) ? req.query.lang : supportedLanguages[0];
cacheCreateCard(req.params.characterId, null, language)
.then(() => {
res.status(200).json({
status: 'ok',
url: `/characters/id/${req.params.characterId}.png`,
});
})
.catch(next);
});
app.get('/prepare/name/:world/:characterName', limiter, (req, res, next) => {
getCharacterIdByName(req.params.world, req.params.characterName)
.then(characterId => {
if (characterId == null) {
res.status(404).send({ status: 'error', reason: 'Character not found.' });
} else {
res.redirect(`/prepare/id/${characterId}${getOriginalQueryString(req)}`);
}
})
.catch(next);
});
app.get('/characters/id/:characterId.png', limiter, (req, res, next) => {
const language = typeof req.query.lang === 'string' && supportedLanguages.includes(req.query.lang) ? req.query.lang : supportedLanguages[0];
cacheCreateCard(req.params.characterId, null, language)
.then(result => {
const image = result.binary.image;
res.writeHead(200, {
'Cache-Control': 'public, max-age=14400',
'Content-Length': Buffer.byteLength(image),
'Content-Type': 'image/png',
});
res.end(image, 'binary');
})
.catch(next);
});
app.get('/characters/id/:characterId', (req, res) => {
res.redirect(`/characters/id/${req.params.characterId}.png${getOriginalQueryString(req)}`);
});
app.get('/characters/name/:world/:characterName.png', limiter, (req, res, next) => {
getCharacterIdByName(req.params.world, req.params.characterName)
.then(characterId => {
if (characterId == null) {
res.status(404).send({ status: 'error', reason: 'Character not found.' });
} else {
res.redirect(`/characters/id/${characterId}${getOriginalQueryString(req)}`);
}
})
.catch(next);
});
app.get('/characters/name/:world/:characterName', (req, res) => {
res.redirect(`/characters/name/${req.params.world}/${req.params.characterName}.png${getOriginalQueryString(req)}`);
});
app.get('/', async (req, res) => {
res.redirect('https://github.com/xivapi/XIV-Character-Cards');
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({
status: 'error',
reason: error instanceof Error ? error.stack : String(error),
});
});
app.listen(port, () => {
console.log(`Listening at http://localhost:${port}`);
creator.ensureInit().then(() => console.log('CardCreator initialization complete'));
});