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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
#!/usr/bin/env node
/* jshint esnext: true */
var amqp = require("amqplib"),
Seq = require("sequelize"),
request = require("request-promise"),
express = require("express"),
bodyParser = require("body-parser"),
http = require("http"),
sleep = require("sleep-promise");
var MADGLORY_TOKEN = process.env.MADGLORY_TOKEN,
DATABASE_URI = process.env.DATABASE_URI,
RABBITMQ_URI = process.env.RABBITMQ_URI || "amqp://localhost",
REGIONS = ["na", "eu", "sg", "sa", "ea"];
if (MADGLORY_TOKEN == undefined) throw "Need an API token";
var rabbit, ch, seq,
app = express(),
server = http.Server(app);
// connect to broker, retrying forever
(async () => {
while (true) {
try {
seq = new Seq(DATABASE_URI, { logging: () => {} });
rabbit = await amqp.connect(RABBITMQ_URI);
ch = await rabbit.createChannel();
await ch.assertQueue("grab", {durable: true});
await ch.assertQueue("process", {durable: true});
break;
} catch (err) {
console.error(err);
await sleep(5000);
}
}
model = require("../orm/model")(seq, Seq);
})();
server.listen(8880);
app.use(express.static("assets"));
app.use(bodyParser.json());
// request a grab job
function requestUpdate(name, region, last_match_created_date, id) {
last_match_created_date = last_match_created_date || new Date(value=0);
// add 1s, because createdAt-start <= x <= createdAt-end
// so without the +1s, we'd always get the last_match_created_date match back
last_match_created_date.setSeconds(last_match_created_date.getSeconds() + 1);
let payload = {
"region": region,
"params": {
"filter[playerIds]": id,
"filter[createdAt-start]": last_match_created_date.toISOString(),
"filter[gameMode]": "casual,ranked",
"sort": "-createdAt"
}
};
console.log("requesting update for", name, region);
return ch.sendToQueue("grab", new Buffer(JSON.stringify(payload)),
{ persistent: true });
}
// search for player name on all shards
// send a notification for results and request updates
async function searchPlayer(name) {
let found = false;
console.log("looking up", name);
await Promise.all(REGIONS.map(async (region) => {
let players = [];
while (true) {
console.log("looking up", name, region);
try {
// find players by name
players = await request({
uri: "https://api.dc01.gamelockerapp.com/shards/" + region + "/players",
headers: {
"X-Title-Id": "semc-vainglory",
"Authorization": MADGLORY_TOKEN
},
qs: {
"filter[playerNames]": name
},
json: true,
gzip: true
});
console.log("found", name, region);
break;
} catch (err) {
if (err.statusCode == 429) {
console.log("rate limited, sleeping");
await sleep(100); // no return, no break => retry
} else if (err.statusCode != 404) console.error(err);
if (err.statusCode != 429) {
console.log("failed", name, region, err.statusCode);
return;
}
}
}
// players.length will be 1 in 99.9% of all cases
// - but this will cover the 0.01% too
//
// send to processor, so the player is in db
// no matter whether we find matches or not
await ch.sendToQueue("process", new Buffer(JSON.stringify(players.data)),
{ persistent: true, type: "player" });
// request grab jobs
await Promise.all(players.data.map((p) =>
requestUpdate(p.attributes.name, p.attributes.shardId, undefined, p.id)));
found = true;
}));
// notify web
if (found)
await ch.publish("amq.topic", "player." + name,
new Buffer("search_success"));
else
await ch.publish("amq.topic", "player." + name,
new Buffer("search_fail"));
}
// update a player based on db record
function updatePlayer(player) {
player.last_update = new Date();
// set last_update and request an update job
return Promise.all([
player.save(),
requestUpdate(player.name, player.shard_id,
player.last_match_created_date, player.api_id)
]);
}
/* routes */
// force an update
app.post("/api/player/:name/search", (req, res) => {
searchPlayer(req.params.name); // do not await, just fire
res.sendStatus(204); // notifications will follow
});
// update a known user
app.post("/api/player/:name/update", async (req, res) => {
let player = await model.Player.findOne({ where: { name: req.params.name } });
if (player == undefined) {
console.log("player not found in db, searching instead", req.params.name);
await searchPlayer(req.params.name);
res.sendStatus(204);
return;
}
console.log("player in db, updating", req.params.name);
await updatePlayer(player);
res.sendStatus(204);
});
// update a random user
app.post("/api/player", async (req, res) => {
let player = await model.Player.findOne({ order: [ Seq.fn("RAND") ] });
await updatePlayer(player);
res.json(player);
});
// crunch data
app.post("/api/stats/:dimension_on/update", async (req, res) => {
// forward JSON
let payload = {
dimension_on: req.params.dimension_on,
filter: req.body
};
console.log(payload);
await ch.sendToQueue("crunch", new Buffer(JSON.stringify(payload)),
{ persistent: true });
res.sendStatus(204);
});
/* internal monitoring */
app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html");
});
|