summaryrefslogtreecommitdiff
path: root/server.js
blob: 66c4af5d4eb774af7f7aa2e1414aaa37ce6152cf (plain)
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/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});
            await ch.assertQueue("crunch", {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());

function grabPlayer(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, type: "matches" });
}

// search for a player name in one region
// request process for lifetime
// return an array (JSONAPI response)
async function searchPlayerInRegion(region, name, id) {
    let players = [],
        found = false;
    while (true) {
        console.log("searching in", region, name, id);
        try {
            // find players by name
            let opts = {
                uri: "https://api.dc01.gamelockerapp.com/shards/" + region + "/players",
                headers: {
                    "X-Title-Id": "semc-vainglory",
                    "Authorization": MADGLORY_TOKEN
                },
                qs: {},
                json: true,
                gzip: true
            };
            // prefer player id over name
            if (id == undefined) opts["qs"]["filter[playerNames]"] = name
            else opts["qs"]["filter[playerIds]"] = id
            players = await request(opts);

            console.log("found", name, region);
            found = true;
            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", region, name, id, err.statusCode);
                return [];
            }
        }
    }

    if (!found) return [];

    // notify web that data is being loaded
    await ch.publish("amq.topic", "player." + name,
        new Buffer("search_success"));
    // 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" });

    return players.data;
}

// search for player name on all shards
// grab player(s)
// send a notification for results and request updates
async function searchPlayer(name) {
    let found = false;
    console.log("searching up", name);
    await Promise.all(REGIONS.map(async (region) => {
        let players = await searchPlayerInRegion(region, name, undefined);
        if (players.length > 0)
            found = true;

        // request grab jobs
        await Promise.all(players.map((p) =>
            grabPlayer(p.attributes.name, p.attributes.shardId, undefined, p.id)));
    }));
    // notify web
    if (!found)
        await ch.publish("amq.topic", "player." + name,
            new Buffer("search_fail"));
}

// update a player from API based on db record
// & grab player
async function updatePlayer(player) {
    // set last_update and request an update job
    // if last_update is null, we need that player's full history
    let grabstart;
    if (player.get("last_update") == null) grabstart = undefined;
    else grabstart = player.get("last_match_created_date");

    await player.update({ last_update: seq.fn("NOW") },
        { fields: ["last_update"] } );

    let players = await searchPlayerInRegion(
        player.get("shard_id"), player.get("name"), player.get("api_id"));

    // will be the same as `player` 99.9% of the time
    // but not when the player changed their name
    // search happened by ID, so we get only 1 player back
    // but player.name != players[0].attributes.name
    // (with a name change)
    await Promise.all(players.map((p) =>
        grabPlayer(p.attributes.name, p.attributes.shardId, grabstart, p.id)));
}

// update a region's samples since samples_last_update
async function updateSamples(region) {
    let record = await model.Keys.findOne({
        where: {
            type: "samples_last_update",
            key: region
        }
    }), last_update;
    if (record == undefined)
        last_update = (new Date(value=0)).toISOString();
    else last_update = record.get("value");

    console.log("updating samples", region, last_update);
    await ch.sendToQueue("grab", new Buffer(
        JSON.stringify({ region: region,
            params: {
                sort: "-createdAt",
                "filter[createdAt-start]": last_update
            }
        })),
        { persistent: true, type: "samples" });

    last_update = (new Date()).toISOString();
    if (record == null)
        await model.Keys.create({
            type: "samples_last_update",
            key: region,
            value: last_update
        });
    else
        await record.update({ value: last_update });
}

/* routes */
// force an update
app.post("/api/player/:name/search", async (req, res) => {
    searchPlayer(req.params.name);  // do not await, just fire
    res.sendStatus(204);  // notifications will follow
});
// update a known user
// flow:
//   * get from db
//   * update lifetime from `/players` via id, catch IGN changes
//   * update from `/matches`
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);
        searchPlayer(req.params.name);  // fire away
        res.sendStatus(204);
        return;
    }
    console.log("player in db, updating", req.params.name);
    updatePlayer(player);  // fire away
    res.sendStatus(204);
});
// crunch a known user
app.post("/api/player/:name/crunch", async (req, res) => {
    let player = await model.Player.findOne({ where: { name: req.params.name } });
    if (player == undefined) {
        console.log("player not found in db, won't crunch", req.params.name);
        res.sendStatus(404);
        return;
    }
    console.log("player in db, crunching", req.params.name);
    await ch.sendToQueue("crunch", new Buffer(player.api_id),
        { persistent: true, type: "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") ] });
    updatePlayer(player);  // do not await
    res.json(player);
});
// download sample sets
app.post("/api/samples/:region", async (req, res) => {
    await updateSamples(req.params.region);
    res.sendStatus(204);
});
app.post("/api/samples", async (req, res) => {
    await Promise.all(REGIONS.map((region) =>
        updateSamples(region)));
    res.sendStatus(204);
});
// crunch global stats
app.post("/api/crunch", async (req, res) => {
    console.log("crunching global stats");
    await ch.sendToQueue("crunch", new Buffer(""),
        { persistent: true, type: "global" });
    res.sendStatus(204);
});

// 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");
});