Skip to content

Commit

Permalink
Add stats page, timelapse, extra options menu, QoL changes, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
MercurialPony committed Oct 16, 2023
1 parent a123ac9 commit f2af570
Show file tree
Hide file tree
Showing 16 changed files with 1,311 additions and 106 deletions.
128 changes: 113 additions & 15 deletions canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,12 @@ class ImageBuffer



const defaultUserData = {
cooldown: 0
};



class UserDataStore
{
constructor()
constructor(defaultUserData)
{
this._defaultUserData = defaultUserData;
this._map = new Map();
}

Expand All @@ -57,7 +53,7 @@ class UserDataStore

if(!userData)
{
this._map.set(userId, userData = structuredClone(defaultUserData));
this._map.set(userId, userData = structuredClone(this._defaultUserData));
}

return userData;
Expand Down Expand Up @@ -92,12 +88,15 @@ function hexToInt(hex)
return Number(`0x${hex}`);
}

const defaultCanvasUserData = { cooldown: 0 };

class Canvas extends EventEmitter
{
constructor()
{
super();
this.users = new UserDataStore();
this.pixelEvents = [];
this.users = new UserDataStore(defaultCanvasUserData);

setInterval(this._update.bind(this), 1000);
}
Expand Down Expand Up @@ -128,7 +127,7 @@ class Canvas extends EventEmitter
{
this.pixels.setColor(x, y, color);
this.info[x][y] = { userId, timestamp };
this.emit("pixel", x, y, color, userId, timestamp);
this.pixelEvents.push({ x, y, color, userId, timestamp });
}

isInBounds(x, y)
Expand All @@ -153,7 +152,9 @@ class Canvas extends EventEmitter
return false;
}

this._setPixel(x, y, color, userId, Date.now());
const timestamp = Date.now();
this._setPixel(x, y, color, userId, timestamp);
this.emit("pixel", x, y, color, userId, timestamp);

this.users.get(userId).cooldown = this.settings.maxCooldown;

Expand All @@ -163,10 +164,11 @@ class Canvas extends EventEmitter



Canvas.IO = class
Canvas.IO = class extends EventEmitter
{
constructor(canvas, path)
{
super();
this._canvas = canvas;
this._path = path;

Expand All @@ -191,11 +193,12 @@ Canvas.IO = class

const color = buf.readBuffer(3).readUintBE(0, 3);

const userId = buf.readBigUInt64BE();
const timestamp = buf.readBigUInt64BE();
const userId = buf.readBigUInt64BE().toString();
const timestamp = Number(buf.readBigUInt64BE());

this._canvas._setPixel(x, y, color, userId, timestamp);

this._canvas.pixels.setColor(x, y, color);
this._canvas.info[x][y] = { userId, timestamp };
this.emit("read", x, y, color, userId, timestamp);
}

return this;
Expand Down Expand Up @@ -232,6 +235,101 @@ Canvas.IO = class




const defaultUserStats = { pixelEvents: [] };

Canvas.Stats = class
{
constructor(canvas, io, getConnectedUserCount)
{
this.canvas = canvas;
this.getConnectedUserCount = getConnectedUserCount;

this.global = {
uniqueUserCount: 0,
colorCounts: {},
//
userCountOverTime: {},
pixelCountOverTime: {}
};

this.personal = new UserDataStore(defaultUserStats);

//

canvas.addListener("pixel", this._updateRealTime.bind(this));
io.addListener("read", this._updateRealTime.bind(this));

// TODO: Yucky!
if(FileSystem.existsSync("./canvas/userCountOverTime.json"))
{
this.global.userCountOverTime = JSON.parse(FileSystem.readFileSync("./canvas/userCountOverTime.json", { encoding: "utf-8" }));
}
}

startRecording(intervalMs, durationMs)
{
this._recordingIntervalMs = intervalMs;
this._recordingDurationMs = durationMs;

Utils.startInterval(this._recordingIntervalMs, this._updateAtInterval.bind(this));
}

_updateRealTime(x, y, color, userId, timestamp)
{
this.global.colorCounts[color] ??= 0;
this.global.colorCounts[color]++;

this.personal.get(userId).pixelEvents.push({ x, y, color, userId, timestamp });
}

_updateAtInterval()
{
console.log("Updated stats");

const currentTimeMs = Date.now();
const startTimeMs = currentTimeMs - this._recordingDurationMs;
const intervalTimeMs = this._recordingIntervalMs;



this.global.uniqueUserCount = new Set(this.canvas.pixelEvents.map(pixelEvent => pixelEvent.userId)).size; // TODO: update in real time?



for(const timestamp in this.global.userCountOverTime)
{
if(timestamp < startTimeMs)
{
delete this.global.userCountOverTime[timestamp];
}
}

this.global.userCountOverTime[currentTimeMs] = this.getConnectedUserCount();

// TODO: Yucky!
FileSystem.writeFileSync("./canvas/userCountOverTime.json", JSON.stringify(this.global.userCountOverTime));



// TODO This will break if there are periods of 0 placement
// TOOD So we need to fill out those intervals manually, make sure they are present
this.global.pixelCountOverTime = this.canvas.pixelEvents.groupBy(pixelEvent =>
{
const intervalStartTimeMs = Math.floor( (pixelEvent.timestamp - startTimeMs) / intervalTimeMs ) * intervalTimeMs;

return pixelEvent.timestamp < startTimeMs ? undefined : intervalStartTimeMs + startTimeMs;
} );

for(const timestamp in this.global.pixelCountOverTime)
{
this.global.pixelCountOverTime[timestamp] = this.global.pixelCountOverTime[timestamp].length;
}
}
}



/*
* ===============================
*/
Expand Down
43 changes: 34 additions & 9 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ async function userInfo(req, res, next)
* ===============================
*/

const canvas = new Canvas().initialize({ sizeX: 500, sizeY: 500, colors: [ "#be0039", "#ff4500", "#ffa800", "#ffd635", "#00a368", "#00cc78", "#7eed56", "#00756f", "#009eaa", "#2450a4", "#3690ea", "#51e9f4", "#493ac1", "#6a5cff", "#811e9f", "#b44ac0", "#ff3881", "#ff99aa", "#6d482f", "#9c6926", "#000000", "#898d90", "#d4d7d9", "#ffffff" ] });
const io = new Canvas.IO(canvas, "./canvas/current.hst").read();
const clients = new Map();

const canvas = new Canvas().initialize({ sizeX: 500, sizeY: 500, colors: [ "#6d001a", "#be0039", "#ff4500", "#ffa800", "#ffd635", "#fff8b8", "#00a368", "#00cc78", "#7eed56", "#00756f", "#009eaa", "#00ccc0", "#2450a4", "#3690ea", "#51e9f4", "#493ac1", "#6a5cff", "#94b3ff", "#811e9f", "#b44ac0", "#e4abff", "#de107f", "#ff3881", "#ff99aa", "#6d482f", "#9c6926", "#ffb470", "#000000", "#515252", "#898d90", "#d4d7d9", "#ffffff" ] });
const io = new Canvas.IO(canvas, "./canvas/current.hst");
const stats = new Canvas.Stats(canvas, io, () => clients.size);
io.read();
stats.startRecording(10 * 60 * 1000 /* 10 min */, 24 * 60 * 60 * 1000 /* 24 hrs */ );

// day 2 colors
// const colors = [ "#ff4500", "#ffa800", "#ffd635", "#00a368", "#7eed56", "#2450a4", "#3690ea", "#51e9f4", "#811e9f", "#b44ac0", "#ff99aa", "#9c6926", "#000000", "#898d90", "#d4d7d9", "ffffff" ];
Expand Down Expand Up @@ -120,6 +125,7 @@ app.get("/auth/discord", (req, res) =>
scope: oauthScope,
redirect_uri: oauthRedirectUrl,
response_type: "code",
state: req.query.from
});

res.redirect(`https://discord.com/api/oauth2/authorize?${query}`);
Expand All @@ -131,9 +137,11 @@ app.get("/auth/discord/redirect", async (req, res) =>
{
const code = req.query.code;

const redirectUrl = "/" + (req.query.state || "");

if (!code)
{
return res.redirect("/");
return res.redirect(redirectUrl);
}

const authRes = await fetch("https://discord.com/api/oauth2/token",
Expand All @@ -153,7 +161,7 @@ app.get("/auth/discord/redirect", async (req, res) =>

if(!authRes.ok)
{
return res.redirect("/");
return res.redirect(redirectUrl);
}

const auth = await authRes.json();
Expand All @@ -165,13 +173,13 @@ app.get("/auth/discord/redirect", async (req, res) =>

if(!userRes.ok)
{
return res.redirect("/");
return res.redirect(redirectUrl);
}

await promisify(req.session.regenerate.bind(req.session))(); // TODO: Clean old sessions associated with this user/id
req.session.user = await userRes.json();

res.redirect("/");
res.redirect(redirectUrl);
});


Expand Down Expand Up @@ -233,9 +241,9 @@ app.post("/placer", async (req, res) =>
{
const member = await client.guilds.cache.get(Config.guild.id).members.fetch(pixelInfo.userId.toString());

if(member && member.nickname)
if(member)
{
return res.json({ username: member.nickname });
return res.json({ username: member.nickname ? member.nickname : member.user.globalName });
}
}
catch(e)
Expand All @@ -254,6 +262,24 @@ app.post("/placer", async (req, res) =>



/*
* ===============================
*/

app.get("/stats-json", ExpressCompression(), userInfo, (req, res) =>
{
const statsJson = { global: Object.assign( { userCount: clients.size, pixelCount: canvas.pixelEvents.length } , stats.global ) };

if(req.member)
{
statsJson.personal = stats.personal.get(req.member.user.id);
}

res.json(statsJson);
});



/*
* ===============================
*/
Expand All @@ -280,7 +306,6 @@ function isBanned(member)
*/

let idCounter = 0;
const clients = new Map();

canvas.addListener("pixel", (x, y, color) =>
{
Expand Down
Loading

0 comments on commit f2af570

Please sign in to comment.