Move pixelcount into redis
This commit is contained in:
parent
dd757b035e
commit
db4f006f4a
|
@ -394,7 +394,6 @@ export class ChatProvider {
|
|||
user.ipSub,
|
||||
country,
|
||||
);
|
||||
console.log(allowed, needProxycheck, name, id, country);
|
||||
if (allowed) {
|
||||
logger.info(
|
||||
`${name} / ${user.ip} tried to send chat message but is not allowed`,
|
||||
|
|
111
src/core/Ranks.js
Normal file
111
src/core/Ranks.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* timers and cron for account related actions
|
||||
*/
|
||||
|
||||
import Sequelize from 'sequelize';
|
||||
import RegUser from '../data/sql/RegUser';
|
||||
import {
|
||||
getRanks,
|
||||
resetDailyRanks,
|
||||
saveDailyTop,
|
||||
loadDailyTop,
|
||||
} from '../data/redis/ranks';
|
||||
import socketEvents from '../socket/socketEvents';
|
||||
import logger from './logger';
|
||||
|
||||
import { MINUTE } from './constants';
|
||||
import { DailyCron } from '../utils/cron';
|
||||
|
||||
class Ranks {
|
||||
ranks; // Array
|
||||
|
||||
constructor() {
|
||||
this.updateRanking = this.updateRanking.bind(this);
|
||||
this.resetDailyRanking = this.resetDailyRanking.bind(this);
|
||||
this.prevTop = [];
|
||||
this.ranks = {
|
||||
dailyRanking: [],
|
||||
ranking: [],
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.prevTop = await loadDailyTop();
|
||||
await this.updateRanking();
|
||||
setInterval(this.updateRanking, 10 * MINUTE);
|
||||
DailyCron.hook(this.resetDailyRanking);
|
||||
}
|
||||
|
||||
/*
|
||||
* take array of {useId: score} and resolve
|
||||
* user informations
|
||||
*/
|
||||
static async populateRanking(rawRanks) {
|
||||
if (!rawRanks.length) {
|
||||
return rawRanks;
|
||||
}
|
||||
const uids = rawRanks.map((r) => r.id);
|
||||
const userData = await RegUser.findAll({
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
[
|
||||
Sequelize.fn(
|
||||
'DATEDIFF',
|
||||
Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
Sequelize.col('createdAt'),
|
||||
),
|
||||
'age',
|
||||
],
|
||||
],
|
||||
where: {
|
||||
id: uids,
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
for (let i = 0; i < userData.length; i += 1) {
|
||||
const { id, name, age } = userData[i];
|
||||
const dat = rawRanks.find((r) => r.id === id);
|
||||
if (dat) {
|
||||
dat.name = name;
|
||||
dat.age = age;
|
||||
}
|
||||
}
|
||||
return rawRanks;
|
||||
}
|
||||
|
||||
async updateRanking() {
|
||||
if (socketEvents.amIImportant()) {
|
||||
// TODO do this only in main shard
|
||||
}
|
||||
// populate dictionaries
|
||||
const ranking = await Ranks.populateRanking(
|
||||
await getRanks(
|
||||
false,
|
||||
1,
|
||||
100,
|
||||
));
|
||||
const dailyRanking = await Ranks.populateRanking(
|
||||
await getRanks(
|
||||
true,
|
||||
1,
|
||||
100,
|
||||
));
|
||||
this.ranks.ranking = ranking;
|
||||
this.ranks.dailyRanking = dailyRanking;
|
||||
}
|
||||
|
||||
async resetDailyRanking() {
|
||||
if (!socketEvents.amIImportant()) {
|
||||
return;
|
||||
}
|
||||
this.prevTop = await saveDailyTop(this.ranks.dailyRanking);
|
||||
logger.info('Resetting Daily Ranking');
|
||||
await resetDailyRanks();
|
||||
await this.updateRanking();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const rankings = new Ranks();
|
||||
export default rankings;
|
|
@ -12,7 +12,7 @@ import {
|
|||
setPixelByOffset,
|
||||
setPixelByCoords,
|
||||
} from './setPixel';
|
||||
import rankings from './ranking';
|
||||
import rankings from './Ranks';
|
||||
import canvases from './canvases';
|
||||
|
||||
import { THREE_CANVAS_HEIGHT, THREE_TILE_SIZE, TILE_SIZE } from './constants';
|
||||
|
@ -144,6 +144,7 @@ export async function drawByOffsets(
|
|||
/*
|
||||
* validate pixels
|
||||
*/
|
||||
let ranked = canvas.ranked && user.id && pcd;
|
||||
for (let u = 0; u < pixels.length; u += 1) {
|
||||
const [offset, color] = pixels[u];
|
||||
pxlOffsets.push(offset);
|
||||
|
@ -180,15 +181,18 @@ export async function drawByOffsets(
|
|||
throw new Error(8);
|
||||
}
|
||||
|
||||
/* dont rank antarctica */
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (canvas.ranked && (canvasId != 0 || y < 14450) && pcd) {
|
||||
pixels[u].push(true);
|
||||
if (canvasId == 0 && y > 14450) {
|
||||
ranked = false;
|
||||
}
|
||||
}
|
||||
|
||||
[retCode, pxlCnt, wait, coolDown, needProxycheck] = await allowPlace(
|
||||
ip,
|
||||
user.id,
|
||||
user.country,
|
||||
ranked,
|
||||
canvasId,
|
||||
i, j,
|
||||
clrIgnore,
|
||||
|
@ -198,11 +202,12 @@ export async function drawByOffsets(
|
|||
);
|
||||
|
||||
for (let u = 0; u < pxlCnt; u += 1) {
|
||||
const [offset, color, ranked] = pixels[u];
|
||||
const [offset, color] = pixels[u];
|
||||
setPixelByOffset(canvasId, color, i, j, offset);
|
||||
if (ranked) {
|
||||
rankedPxlCnt += 1;
|
||||
}
|
||||
|
||||
if (ranked) {
|
||||
rankedPxlCnt = pxlCnt;
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
@ -219,10 +224,6 @@ export async function drawByOffsets(
|
|||
}
|
||||
}
|
||||
|
||||
if (rankedPxlCnt) {
|
||||
user.incrementPixelcount(rankedPxlCnt);
|
||||
}
|
||||
|
||||
if (retCode !== 13) {
|
||||
curReqIPs.delete(ip);
|
||||
}
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* timers and cron for account related actions
|
||||
*/
|
||||
|
||||
import Sequelize from 'sequelize';
|
||||
import sequelize from '../data/sql/sequelize';
|
||||
import RegUser from '../data/sql/RegUser';
|
||||
import { saveDailyTop, loadDailyTop } from '../data/redis/PrevDayTop';
|
||||
import socketEvents from '../socket/socketEvents';
|
||||
import logger from './logger';
|
||||
|
||||
import { MINUTE } from './constants';
|
||||
import { DailyCron } from '../utils/cron';
|
||||
|
||||
class Ranks {
|
||||
ranks; // Array
|
||||
|
||||
constructor() {
|
||||
this.updateRanking = this.updateRanking.bind(this);
|
||||
this.resetDailyRanking = this.resetDailyRanking.bind(this);
|
||||
this.prevTop = [];
|
||||
this.ranks = {
|
||||
dailyRanking: [],
|
||||
ranking: [],
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.prevTop = await loadDailyTop();
|
||||
await this.updateRanking();
|
||||
setInterval(this.updateRanking, 5 * MINUTE);
|
||||
DailyCron.hook(this.resetDailyRanking);
|
||||
}
|
||||
|
||||
async updateRanking() {
|
||||
if (socketEvents.amIImportant()) {
|
||||
logger.info('Update pixel rankings in SQL');
|
||||
// recalculate ranking column
|
||||
await sequelize.query(
|
||||
// eslint-disable-next-line max-len
|
||||
'SET @r=0; UPDATE Users SET ranking= @r:= (@r + 1) ORDER BY totalPixels DESC;',
|
||||
);
|
||||
await sequelize.query(
|
||||
// eslint-disable-next-line max-len
|
||||
'SET @r=0; UPDATE Users SET dailyRanking= @r:= (@r + 1) ORDER BY dailyTotalPixels DESC;',
|
||||
);
|
||||
} else {
|
||||
logger.info('Get pixel rankings from SQL');
|
||||
}
|
||||
// populate dictionaries
|
||||
const ranking = await RegUser.findAll({
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
'totalPixels',
|
||||
'ranking',
|
||||
'dailyRanking',
|
||||
'dailyTotalPixels',
|
||||
[
|
||||
Sequelize.fn(
|
||||
'DATEDIFF',
|
||||
Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
Sequelize.col('createdAt'),
|
||||
),
|
||||
'age',
|
||||
],
|
||||
],
|
||||
limit: 100,
|
||||
where: {
|
||||
ranking: { [Sequelize.Op.not]: null },
|
||||
},
|
||||
order: ['ranking'],
|
||||
raw: true,
|
||||
});
|
||||
const dailyRanking = await RegUser.findAll({
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
'totalPixels',
|
||||
'ranking',
|
||||
'dailyRanking',
|
||||
'dailyTotalPixels',
|
||||
[
|
||||
Sequelize.fn(
|
||||
'DATEDIFF',
|
||||
Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
Sequelize.col('createdAt'),
|
||||
),
|
||||
'age',
|
||||
],
|
||||
],
|
||||
limit: 100,
|
||||
where: {
|
||||
dailyRanking: { [Sequelize.Op.not]: null },
|
||||
},
|
||||
order: ['dailyRanking'],
|
||||
raw: true,
|
||||
});
|
||||
this.ranks.ranking = ranking;
|
||||
this.ranks.dailyRanking = dailyRanking;
|
||||
}
|
||||
|
||||
async resetDailyRanking() {
|
||||
if (!socketEvents.amIImportant()) {
|
||||
return;
|
||||
}
|
||||
this.prevTop = await saveDailyTop(this.ranks.dailyRanking);
|
||||
logger.info('Resetting Daily Ranking');
|
||||
await RegUser.update({ dailyTotalPixels: 0 }, { where: {} });
|
||||
await this.updateRanking();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const rankings = new Ranks();
|
||||
export default rankings;
|
|
@ -10,8 +10,8 @@ import { QueryTypes, Utils } from 'sequelize';
|
|||
|
||||
import sequelize from './sql/sequelize';
|
||||
import { RegUser, Channel, UserBlock } from './sql';
|
||||
import { incrementPixelcount } from './sql/RegUser';
|
||||
import { setCoolDown, getCoolDown } from './redis/cooldown';
|
||||
import { getUserRanks } from './redis/ranks';
|
||||
import { getIPv6Subnet } from '../utils/ip';
|
||||
import { ADMIN_IDS } from '../core/config';
|
||||
|
||||
|
@ -50,7 +50,6 @@ export const regUserQueryInclude = [{
|
|||
class User {
|
||||
id; // string
|
||||
ip; // string
|
||||
wait; // ?number
|
||||
regUser; // Object
|
||||
channels; // Object
|
||||
blocked; // Array
|
||||
|
@ -179,13 +178,6 @@ class User {
|
|||
return getCoolDown(this.ipSub, this.id, canvasId);
|
||||
}
|
||||
|
||||
incrementPixelcount(amount = 1) {
|
||||
const { id } = this;
|
||||
if (id) {
|
||||
incrementPixelcount(id, amount);
|
||||
}
|
||||
}
|
||||
|
||||
async getTotalPixels() {
|
||||
const { id } = this;
|
||||
if (!id) return 0;
|
||||
|
@ -210,6 +202,7 @@ class User {
|
|||
}
|
||||
|
||||
async setCountry(country) {
|
||||
this.country = country;
|
||||
if (this.regUser && this.regUser.flag !== country) {
|
||||
this.regUser.update({
|
||||
flag: country,
|
||||
|
@ -229,7 +222,7 @@ class User {
|
|||
return true;
|
||||
}
|
||||
|
||||
getUserData() {
|
||||
async getUserData() {
|
||||
const {
|
||||
id,
|
||||
userlvl,
|
||||
|
@ -248,23 +241,25 @@ class User {
|
|||
name: null,
|
||||
mailVerified: false,
|
||||
blockDm: false,
|
||||
totalPixels: 0,
|
||||
dailyTotalPixels: 0,
|
||||
ranking: null,
|
||||
dailyRanking: null,
|
||||
mailreg: false,
|
||||
};
|
||||
}
|
||||
const { regUser } = this;
|
||||
const [
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
ranking,
|
||||
dailyRanking,
|
||||
] = await getUserRanks(id);
|
||||
return {
|
||||
...data,
|
||||
name: regUser.name,
|
||||
mailVerified: regUser.mailVerified,
|
||||
blockDm: regUser.blockDm,
|
||||
totalPixels: regUser.totalPixels,
|
||||
dailyTotalPixels: regUser.dailyTotalPixels,
|
||||
ranking: regUser.ranking,
|
||||
dailyRanking: regUser.dailyRanking,
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
ranking,
|
||||
dailyRanking,
|
||||
mailreg: !!(regUser.password),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* saving and loading the top 10 of the previous day
|
||||
*/
|
||||
|
||||
import client from './client';
|
||||
import logger from '../../core/logger';
|
||||
|
||||
const PREV_DAILY_TOP_KEY = 'prevtop';
|
||||
|
||||
/*
|
||||
* saves the top 10 into redis
|
||||
* @param dailyRanking Array of dailyRanking
|
||||
*/
|
||||
export async function saveDailyTop(dailyRanking) {
|
||||
const top10 = dailyRanking.slice(0, 10).map((user) => user.id);
|
||||
const jsonTop = JSON.stringify(top10);
|
||||
logger.info(`Saving current daily top 10 into redis: ${jsonTop}`);
|
||||
await client.set(PREV_DAILY_TOP_KEY, jsonTop);
|
||||
return top10;
|
||||
}
|
||||
|
||||
/*
|
||||
* load top10 from redis
|
||||
* @return Promis<Array> Array of user IDs of the top 10
|
||||
*/
|
||||
export async function loadDailyTop() {
|
||||
const jsonTop = await client.get(PREV_DAILY_TOP_KEY);
|
||||
logger.info(`Loaded current daily top 10 into redis: ${jsonTop}`);
|
||||
return (jsonTop) ? JSON.parse(jsonTop) : [];
|
||||
}
|
|
@ -10,11 +10,12 @@ import { REDIS_URL, SHARD_NAME } from '../../core/config';
|
|||
|
||||
const scripts = {
|
||||
placePxl: defineScript({
|
||||
NUMBER_OF_KEYS: 5,
|
||||
NUMBER_OF_KEYS: 8,
|
||||
SCRIPT: fs.readFileSync('./workers/lua/placePixel.lua'),
|
||||
transformArguments(...args) {
|
||||
return args.map((a) => ((typeof a === 'string') ? a : a.toString()));
|
||||
},
|
||||
transformReply(arr) { return arr.map((r) => Number(r)); },
|
||||
}),
|
||||
allowedChat: defineScript({
|
||||
NUMBER_OF_KEYS: 3,
|
||||
|
@ -22,6 +23,26 @@ const scripts = {
|
|||
transformArguments(...args) {
|
||||
return args.map((a) => ((typeof a === 'string') ? a : a.toString()));
|
||||
},
|
||||
transformReply(arr) { return arr.map((r) => Number(r)); },
|
||||
}),
|
||||
getUserRanks: defineScript({
|
||||
NUMBER_OF_KEYS: 2,
|
||||
SCRIPT: fs.readFileSync('./workers/lua/getUserRanks.lua'),
|
||||
transformArguments(...args) {
|
||||
return args.map((a) => ((typeof a === 'string') ? a : a.toString()));
|
||||
},
|
||||
transformReply(arr) { return arr.map((r) => Number(r)); },
|
||||
}),
|
||||
zmRankRev: defineScript({
|
||||
NUMBER_OF_KEYS: 1,
|
||||
SCRIPT: fs.readFileSync('./workers/lua/zmRankRev.lua'),
|
||||
transformArguments(key, uids) {
|
||||
return [
|
||||
key,
|
||||
...uids.map((a) => ((typeof a === 'string') ? a : a.toString())),
|
||||
];
|
||||
},
|
||||
transformReply(arr) { return arr.map((r) => Number(r)); },
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -5,15 +5,18 @@
|
|||
import client from './client';
|
||||
import { PREFIX as CAPTCHA_PREFIX } from './captcha';
|
||||
import { PREFIX as ALLOWED_PREFIX } from './isAllowedCache';
|
||||
import { RANKED_KEY, DAILY_RANKED_KEY, DAILY_CRANKED_KEY } from './ranks';
|
||||
import { CAPTCHA_TIME } from '../../core/config';
|
||||
|
||||
const PREFIX = 'cd';
|
||||
|
||||
/*
|
||||
* checks how many of the given pixels can be set,
|
||||
* sets user cooldown and returns the number of pixels to set
|
||||
* sets user cooldown, increments pixelcount
|
||||
* and returns the number of pixels to set
|
||||
* @param ip ip of request
|
||||
* @param id userId
|
||||
* @param ranked boolean if increasing rank
|
||||
* @param clrIgnore, bcd, pcd, cds incormations about canvas
|
||||
* @param i, j chunk coordinates
|
||||
* @param pxls Array with offsets of pixels
|
||||
|
@ -22,6 +25,8 @@ const PREFIX = 'cd';
|
|||
export default function allowPlace(
|
||||
ip,
|
||||
id,
|
||||
country,
|
||||
ranked,
|
||||
canvasId,
|
||||
i, j,
|
||||
clrIgnore,
|
||||
|
@ -35,9 +40,13 @@ export default function allowPlace(
|
|||
const ipCdKey = `${PREFIX}:${canvasId}:ip:${ip}`;
|
||||
const idCdKey = (id) ? `${PREFIX}:${canvasId}:id:${id}` : 'nope';
|
||||
const chunkKey = `ch:${canvasId}:${i}:${j}`;
|
||||
const cc = country || 'xx';
|
||||
const rankset = (ranked) ? RANKED_KEY : 'nope';
|
||||
const dailyset = (ranked) ? DAILY_RANKED_KEY : 'nope';
|
||||
return client.placePxl(
|
||||
isalKey, captKey, ipCdKey, idCdKey, chunkKey,
|
||||
clrIgnore, bcd, pcd, cds,
|
||||
// eslint-disable-next-line max-len
|
||||
isalKey, captKey, ipCdKey, idCdKey, chunkKey, rankset, dailyset, DAILY_CRANKED_KEY,
|
||||
clrIgnore, bcd, pcd, cds, id, cc,
|
||||
...pxls,
|
||||
);
|
||||
}
|
||||
|
|
30
src/data/redis/lua/getUserRanks.lua
Normal file
30
src/data/redis/lua/getUserRanks.lua
Normal file
|
@ -0,0 +1,30 @@
|
|||
-- Get ranks of user
|
||||
-- Currently only pixelcounts and rankings
|
||||
-- Keys:
|
||||
-- rankset: 'rank'
|
||||
-- dailyset: 'rankd'
|
||||
-- Args:
|
||||
-- userId
|
||||
-- Returns:
|
||||
-- {
|
||||
-- 1: totalPixels
|
||||
-- 2: dailyPixels
|
||||
-- 3: totalRanking
|
||||
-- 4: dailyRanking
|
||||
-- }
|
||||
local ret = {0, 0, 0, 0}
|
||||
-- get total pixels
|
||||
local cnt = redis.call('zscore', KEYS[1], ARGV[1])
|
||||
if cnt then
|
||||
ret[1] = cnt
|
||||
-- get total rank
|
||||
ret [3] = redis.call('zrevrank', KEYS[1], ARGV[1]) + 1
|
||||
end
|
||||
-- get daily pixels
|
||||
local dcnt = redis.call('zscore', KEYS[2], ARGV[1])
|
||||
if dcnt then
|
||||
ret[2] = dcnt
|
||||
-- get daily rank
|
||||
ret [4] = redis.call('zrevrank', KEYS[2], ARGV[1]) + 1
|
||||
end
|
||||
return ret
|
|
@ -1,18 +1,26 @@
|
|||
-- Checking requirements and calculating cooldown of user wthin
|
||||
-- redis itself. Does not set pixels directly. Pixels are set in batches
|
||||
-- Checking requirements for placing pixels, calculating cooldown
|
||||
-- of user and incrementing pixel counts wthin redis itself.
|
||||
-- Does not set pixels directly. Pixels are set in batches
|
||||
-- in RedisCanvas.js
|
||||
-- This script will get copied into the dist/workers directory from webpack
|
||||
-- Keys:
|
||||
-- isAlloweed: 'isal:ip' (proxycheck, blacklist, whitelist)
|
||||
-- isHuman 'human:ip' (captcha needed when expired)
|
||||
-- isHuman 'human:ip' captcha needed when expired,
|
||||
-- 'nope' if no captcha should be checked
|
||||
-- ipCD: 'cd:canvasId:ip:ip'
|
||||
-- uCD: 'cd:canvasId:id:userId'
|
||||
-- 'nope' if not logged in
|
||||
-- chunk: 'ch:canvasId:i:j'
|
||||
-- rankset: 'rank' sorted set of pixelcount
|
||||
-- 'nope' if not increasing ranks
|
||||
-- dailyset: 'rankd' sorted set of daily pixelcount
|
||||
-- countryset: sorted set for country stats
|
||||
-- Args:
|
||||
-- clrIgnore: integer number of what colors are considered unset
|
||||
-- bcd: number baseColldown (fixed to cdFactor and 0 if admin)
|
||||
-- pcd: number set pixel cooldown (fixed to cdFactor and 0 if admin)
|
||||
-- cds: max cooldown of canvas
|
||||
-- userId: '0' if not logged in
|
||||
-- cc country code
|
||||
-- off1, chonk offset of first pixel
|
||||
-- off2, chonk offset of second pixel
|
||||
-- ..., infinie pixels possible
|
||||
|
@ -21,7 +29,7 @@
|
|||
-- 1: pixel return status code (check ui/placePixel.js)
|
||||
-- 2: amount of successfully set pixels
|
||||
-- 3: total cooldown of user
|
||||
-- 4: info about placed pixel cooldown (addition of last pixel)
|
||||
-- 4: added cooldown of last pixel
|
||||
-- 5: if we have to update isAllowed( proxycheck)
|
||||
-- }
|
||||
local ret = {0, 0, 0, 0, 0}
|
||||
|
@ -69,9 +77,8 @@ local cli = tonumber(ARGV[1])
|
|||
local bcd = tonumber(ARGV[2])
|
||||
local pcd = tonumber(ARGV[3])
|
||||
local cds = tonumber(ARGV[4])
|
||||
for c = 5,#ARGV do
|
||||
for c = 7,#ARGV do
|
||||
local off = tonumber(ARGV[c]) * 8
|
||||
local clr = tonumber(ARGV[c + 1])
|
||||
-- get color of pixel on canvas
|
||||
local sclr = redis.call('bitfield', KEYS[5], 'get', 'u8', off)
|
||||
sclr = sclr[1]
|
||||
|
@ -88,20 +95,32 @@ for c = 5,#ARGV do
|
|||
end
|
||||
cd = cd + pxlcd
|
||||
if cd > cds then
|
||||
-- pixelstack used up
|
||||
-- report difference as last cd
|
||||
cd = cd - pxlcd
|
||||
pxlcd = cds - cd - pxlcd
|
||||
-- pixelstack used up
|
||||
ret[1] = 9
|
||||
break
|
||||
end
|
||||
pxlcnt = pxlcnt + 1
|
||||
end
|
||||
|
||||
if pxlcnt > 0 and cd > 0 then
|
||||
if pxlcnt > 0 then
|
||||
-- set cooldown
|
||||
if cd > 0 then
|
||||
redis.call('set', KEYS[3], '', 'px', cd)
|
||||
if KEYS[4] ~= "nope" then
|
||||
redis.call('set', KEYS[4], '', 'px', cd)
|
||||
end
|
||||
end
|
||||
-- increment pixelcount
|
||||
if KEYS[6] ~= 'nope' then
|
||||
redis.call('zincrby', KEYS[6], pxlcnt, ARGV[5])
|
||||
redis.call('zincrby', KEYS[7], pxlcnt, ARGV[5])
|
||||
if ARGV[6] ~= 'xx' then
|
||||
redis.call('zincrby', KEYS[8], pxlcnt, ARGV[6])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ret[2] = pxlcnt
|
||||
|
|
16
src/data/redis/lua/zmRankRev.lua
Normal file
16
src/data/redis/lua/zmRankRev.lua
Normal file
|
@ -0,0 +1,16 @@
|
|||
-- Get multiple ranks from sorted set
|
||||
-- Keys:
|
||||
-- set: sorted set
|
||||
-- Args:
|
||||
-- [member,...] dynamic amount of members to look for
|
||||
-- return:
|
||||
-- table with the ranks, 0 for an item with no rank
|
||||
local ret = {}
|
||||
for c = 1,#ARGV do
|
||||
local rank = redis.call('zrevrank', KEYS[1], ARGV[c])
|
||||
if not rank then
|
||||
rank = 0
|
||||
end
|
||||
ret[c] = rank
|
||||
end
|
||||
return ret
|
138
src/data/redis/ranks.js
Normal file
138
src/data/redis/ranks.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* counter for daily and total pixels and ranking
|
||||
*/
|
||||
import client from './client';
|
||||
|
||||
import logger from '../../core/logger';
|
||||
|
||||
export const RANKED_KEY = 'rank';
|
||||
export const DAILY_RANKED_KEY = 'rankd';
|
||||
export const DAILY_CRANKED_KEY = 'crankd';
|
||||
const PREV_DAY_TOP_KEY = 'prankd';
|
||||
const DAY_STATS_RANKS_KEY = 'ds';
|
||||
const CDAY_STATS_RANKS_KEY = 'cds';
|
||||
const PREV_DAILY_TOP_KEY = 'prevtop';
|
||||
|
||||
/*
|
||||
* get pixelcount and ranking
|
||||
* @param userId
|
||||
* @return [ totalPixels, dailyPixels, totalRanking, dailyRanking ]
|
||||
*/
|
||||
export async function getUserRanks(userId) {
|
||||
const ranks = await client.getUserRanks(
|
||||
RANKED_KEY, DAILY_RANKED_KEY,
|
||||
userId,
|
||||
);
|
||||
return ranks.map((r) => Number(r));
|
||||
}
|
||||
|
||||
/*
|
||||
* get userIds by ranks
|
||||
* @param daily integer if using daily or total score
|
||||
* @param start, amount rank to start and end
|
||||
* @return Array of objects with {userId, score, dailyScore} in given range
|
||||
*/
|
||||
export async function getRanks(daily, start, amount) {
|
||||
start -= 1;
|
||||
amount -= 1;
|
||||
let key;
|
||||
let valueName;
|
||||
let rankName;
|
||||
let oKey;
|
||||
let oValueName;
|
||||
let oRankName;
|
||||
if (daily) {
|
||||
key = DAILY_RANKED_KEY;
|
||||
valueName = 'dailyTotalPixels';
|
||||
rankName = 'dailyRanking';
|
||||
oKey = RANKED_KEY;
|
||||
oValueName = 'totalPixels';
|
||||
oRankName = 'ranking';
|
||||
} else {
|
||||
key = RANKED_KEY;
|
||||
valueName = 'totalPixels';
|
||||
rankName = 'ranking';
|
||||
oKey = DAILY_RANKED_KEY;
|
||||
oValueName = 'dailyTotalPixels';
|
||||
oRankName = 'dailyRanking';
|
||||
}
|
||||
/* returns { value: uid, score: pixelCnt } */
|
||||
const ranks = await client.zRangeWithScores(key, start, start + amount, {
|
||||
REV: true,
|
||||
});
|
||||
const uids = ranks.map((r) => r.value);
|
||||
if (!uids.length) {
|
||||
return uids;
|
||||
}
|
||||
const oScores = await client.zmScore(oKey, uids);
|
||||
/* impolemented with lua, which blocks :( */
|
||||
const oRanks = await client.zmRankRev(oKey, uids);
|
||||
const ret = [];
|
||||
for (let i = 0; i < ranks.length; i += 1) {
|
||||
const uob = {
|
||||
id: Number(uids[i]),
|
||||
[valueName]: ranks[i].score,
|
||||
[rankName]: i + 1,
|
||||
[oValueName]: oScores[i],
|
||||
[oRankName]: oRanks[i] + 1,
|
||||
};
|
||||
ret.push(uob);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* reset daily ranks
|
||||
* @return boolean for success
|
||||
*/
|
||||
export async function resetDailyRanks() {
|
||||
// store top 10
|
||||
console.log('DAILYREDIS SAVE TOP 10');
|
||||
await client.zRangeWithScores(PREV_DAY_TOP_KEY, DAILY_RANKED_KEY, 0, 9, {
|
||||
REV: true,
|
||||
WITHSCORES: true,
|
||||
});
|
||||
// store day
|
||||
const yesterday = new Date(Date.now() - 1000 * 3600 * 24);
|
||||
let day = yesterday.getUTCDate();
|
||||
if (day < 10) day = `0${day}`;
|
||||
let month = yesterday.getUTCMonth() + 1;
|
||||
if (month < 10) month = `0${month}`;
|
||||
const year = yesterday.getUTCFullYear();
|
||||
const dateKey = `${year}${month}${day}`;
|
||||
console.log('DAILYREDIS', dateKey);
|
||||
await client.zUnionStore(
|
||||
`${DAY_STATS_RANKS_KEY}:${dateKey}`,
|
||||
DAILY_RANKED_KEY,
|
||||
);
|
||||
await client.zUnionStore(
|
||||
`${CDAY_STATS_RANKS_KEY}:${dateKey}`,
|
||||
DAILY_CRANKED_KEY,
|
||||
);
|
||||
// reset daily counter
|
||||
console.log('DAILYREDIS RESET');
|
||||
await client.del(DAILY_RANKED_KEY);
|
||||
await client.del(DAILY_CRANKED_KEY);
|
||||
}
|
||||
|
||||
/*
|
||||
* saves the top 10 into redis
|
||||
* @param dailyRanking Array of dailyRanking
|
||||
*/
|
||||
export async function saveDailyTop(dailyRanking) {
|
||||
const top10 = dailyRanking.slice(0, 10).map((user) => user.id);
|
||||
const jsonTop = JSON.stringify(top10);
|
||||
logger.info(`Saving current daily top 10 into redis: ${jsonTop}`);
|
||||
await client.set(PREV_DAILY_TOP_KEY, jsonTop);
|
||||
return top10;
|
||||
}
|
||||
|
||||
/*
|
||||
* load top10 from redis
|
||||
* @return Promis<Array> Array of user IDs of the top 10
|
||||
*/
|
||||
export async function loadDailyTop() {
|
||||
const jsonTop = await client.get(PREV_DAILY_TOP_KEY);
|
||||
logger.info(`Loaded current daily top 10 into redis: ${jsonTop}`);
|
||||
return (jsonTop) ? JSON.parse(jsonTop) : [];
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { DataTypes, QueryTypes } from 'sequelize';
|
||||
|
||||
import logger from '../../core/logger';
|
||||
import sequelize from './sequelize';
|
||||
import { generateHash } from '../../utils/hash';
|
||||
|
||||
|
@ -42,28 +41,6 @@ const RegUser = sequelize.define('User', {
|
|||
allowNull: true,
|
||||
},
|
||||
|
||||
totalPixels: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
|
||||
dailyTotalPixels: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
|
||||
ranking: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
dailyRanking: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
// mail and Minecraft verified
|
||||
verified: {
|
||||
type: DataTypes.TINYINT,
|
||||
|
@ -204,59 +181,4 @@ export async function getNamesToIds(ids) {
|
|||
return idToNameMap;
|
||||
}
|
||||
|
||||
/*
|
||||
* increment user pixelcount in batches sequentially
|
||||
* Queue directly accesses queuedPxlIncrement in user object
|
||||
*/
|
||||
let incrementQueue = {};
|
||||
let pushLoop = null;
|
||||
const incrementLoop = async () => {
|
||||
const idKeys = Object.keys(incrementQueue);
|
||||
if (!idKeys.length) {
|
||||
pushLoop = null;
|
||||
return;
|
||||
}
|
||||
let queue = incrementQueue;
|
||||
incrementQueue = {};
|
||||
const orderedCnts = {};
|
||||
idKeys.forEach((id) => {
|
||||
const cnt = queue[id];
|
||||
const cntArr = orderedCnts[cnt];
|
||||
if (cntArr) {
|
||||
cntArr.push(id);
|
||||
} else {
|
||||
orderedCnts[cnt] = [id];
|
||||
}
|
||||
});
|
||||
queue = Object.keys(orderedCnts);
|
||||
for (let u = 0; u < queue.length; u += 1) {
|
||||
const by = queue[u];
|
||||
const id = orderedCnts[by];
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await RegUser.increment(['totalPixels', 'dailyTotalPixels'], {
|
||||
by,
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
silent: true,
|
||||
raw: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(`Error on pixel increment: ${err.message}`);
|
||||
}
|
||||
}
|
||||
pushLoop = setTimeout(incrementLoop, 250);
|
||||
};
|
||||
export async function incrementPixelcount(id, by) {
|
||||
if (incrementQueue[id]) {
|
||||
incrementQueue[id] += by;
|
||||
} else {
|
||||
incrementQueue[id] = by;
|
||||
}
|
||||
if (!pushLoop) {
|
||||
pushLoop = setTimeout(incrementLoop, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export default RegUser;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* send global ranking
|
||||
*/
|
||||
|
||||
import rankings from '../core/ranking';
|
||||
import rankings from '../core/Ranks';
|
||||
|
||||
export default (req, res) => {
|
||||
res.json(rankings.ranks);
|
||||
|
|
|
@ -9,7 +9,7 @@ import http from 'http';
|
|||
|
||||
import forceGC from './core/forceGC';
|
||||
import logger from './core/logger';
|
||||
import rankings from './core/ranking';
|
||||
import rankings from './core/Ranks';
|
||||
import sequelize from './data/sql/sequelize';
|
||||
import { connect as connectRedis } from './data/redis/client';
|
||||
import routes from './routes';
|
||||
|
|
|
@ -51,6 +51,9 @@ export default function ranks(
|
|||
|
||||
case 's/REC_ME':
|
||||
case 's/LOGIN': {
|
||||
if (!action.totalPixels) {
|
||||
return state;
|
||||
}
|
||||
const {
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
|
|
Loading…
Reference in New Issue
Block a user