Skip to content

Commit

Permalink
Track completion stats from r/place
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahvdAa committed Jul 21, 2023
1 parent c7c7d38 commit a50b306
Show file tree
Hide file tree
Showing 8 changed files with 700 additions and 21 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
"private": true,
"dependencies": {
"@myrotvorets/clean-up-after-multer": "^1.1.6",
"canvas": "^2.11.2",
"express": "^4.18.2",
"express-ws": "^5.0.2",
"multer": "^1.4.5-lts.1",
"node-fetch": "^3.3.1",
"pngjs": "^7.0.0",
"postgres": "^3.3.4",
"prom-client": "^14.2.0"
"prom-client": "^14.2.0",
"ws": "^8.13.0"
},
"type": "module"
}
2 changes: 2 additions & 0 deletions src/artist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ router.post('/order', upload.fields([{
client.ws.sendPayload('order', payload);
}

chief.placeClient.updateOrders(path.join(IMAGES_DIRECTORY, `${id}.png`), [xOffset, yOffset]);

res.type('text/plain').send('Template has been pushed out.');
next();
});
Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const FLAG_HAS_PRIORITY_MAPPING = 1 << 1;

export const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3000';
export const COLLECT_NODE_METRICS = process.env.NODE_METRICS ?? false;
export const PLACE_STATS = process.env.PLACE_STATS ?? true;
export const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID ?? '';
export const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET ?? '';
export const DISCORD_SERVER_ID = process.env.DISCORD_SERVER_ID ?? '958464581699768380';
Expand Down
32 changes: 30 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import express from 'express';
import expressWs from 'express-ws';
import postgres from 'postgres';
import {BASE_URL, DISCORD_CLIENT_ID, HTTP_PORT, IMAGES_DIRECTORY, POSTGRES_CONNECTION_URI} from './constants.js';
import {
BASE_URL,
DISCORD_CLIENT_ID,
HTTP_PORT,
IMAGES_DIRECTORY,
PLACE_STATS,
POSTGRES_CONNECTION_URI
} from './constants.js';
import {PlaceClient} from './place/PlaceClient.js';
import path from 'node:path';

const app = express();

const chief = {
Expand All @@ -10,7 +20,8 @@ const chief = {
messagesIn: 0,
messagesOut: 0
},
sql: postgres(POSTGRES_CONNECTION_URI)
sql: postgres(POSTGRES_CONNECTION_URI),
placeClient: Boolean(PLACE_STATS) ? new PlaceClient() : null,
};
express.application.chief = chief;

Expand Down Expand Up @@ -39,6 +50,23 @@ ALTER TABLE orders ADD COLUMN IF NOT EXISTS offset_x INTEGER NOT NULL DEFAULT -5
ALTER TABLE orders ADD COLUMN IF NOT EXISTS offset_y INTEGER NOT NULL DEFAULT -500;
`);

if (chief.placeClient) {
const [order] = await chief.sql`SELECT * FROM orders ORDER BY created_at DESC LIMIT 1;`;
if (order) {
chief.placeClient.updateOrders(path.join(IMAGES_DIRECTORY, `${order.id}.png`), [order.offset_x, order.offset_y]);
}
chief.placeClient.connect();

setInterval(() => {
if (!chief.placeClient.connected) {
chief.stats.completion = undefined;
return;
}

chief.stats.completion = chief.placeClient.getOrderDifference();
}, 1000)
}

expressWs(app);
app.use('/api', (await import('./api/index.js')).default);
app.use('/artist', (await import('./artist/index.js')).default);
Expand Down
34 changes: 34 additions & 0 deletions src/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,40 @@ register.registerMetric(new client.Gauge({
}
}
}));
if (chief.placeClient) {
register.registerMetric(new client.Counter({
name: 'template_pixels_total',
help: 'The total amount of pixels in the template',
collect() {
this.reset();
this.inc(chief.stats.completion?.total ?? 0);
}
}));
register.registerMetric(new client.Counter({
name: 'template_pixels_wrong',
help: 'The amount of currently wrong in the template',
collect() {
this.reset();
this.inc(chief.stats.completion?.wrong ?? 0);
}
}));
register.registerMetric(new client.Counter({
name: 'template_pixels_right',
help: 'The amount of currently right pixels in the template',
collect() {
this.reset();
this.inc(chief.stats.completion?.right ?? 0);
}
}));
register.registerMetric(new client.Counter({
name: 'place_message_queue_size',
help: 'The amount of messages in the queue of the place client',
collect() {
this.reset();
this.inc(chief.placeClient.queue.length);
}
}));
}

router.get('/', async (req, res) => {
res.type('text/plain').send(await register.metrics());
Expand Down
159 changes: 159 additions & 0 deletions src/place/PlaceClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {WebSocket} from 'ws';
import {createCanvas, loadImage} from 'canvas';

const trackedCanvases = [1, 2, 4, 5];
const canvasPositions = [[0, 0], [1000, 0], [2000, 0], [0, 1000], [1000, 1000], [2000, 1000]];

export class PlaceClient {

canvasTimestamps = [];
canvas = createCanvas(3000, 2000).getContext('2d');
orderCanvas = createCanvas(3000, 2000).getContext('2d');
connected = false;
queue = [];

async connect() {
console.log('Getting reddit access token...');
const accessToken = await this.getAccessToken();

console.log('Connecting to r/place...');
const ws = new WebSocket('wss://gql-realtime-2.reddit.com/query', {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/116.0',
Origin: 'https://www.reddit.com'
}
});

function subscribeCanvas(id) {
ws.send(JSON.stringify({
id: '2',
type: 'start',
payload: {
variables: {
input: {
channel: {
teamOwner: 'GARLICBREAD',
category: 'CANVAS',
tag: String(id)
}
}
},
extension: {},
operationName: 'replace',
query: 'subscription replace($input: SubscribeInput!) { subscribe(input: $input) { id ... on BasicMessage { data { __typename ... on FullFrameMessageData { __typename name timestamp } ... on DiffFrameMessageData { __typename name currentTimestamp previousTimestamp } } __typename } __typename }}'
}
}));
}

ws.on('open', () => {
this.connected = true;
ws.send(JSON.stringify({
type: 'connection_init',
payload: {
Authorization: `Bearer ${accessToken}`
}
}));
trackedCanvases.forEach((canvas) => {
subscribeCanvas(canvas);
this.canvasTimestamps[canvas] = 0;
});
});

this.queue = [];
ws.on('message', async (message) => {
this.queue.push(message);
});

ws.on('close', () => {
console.log('Disconnected from place, reconnecting...');
this.connected = false;
setTimeout(() => this.connect(), 1000);
});

while (ws.readyState !== WebSocket.CLOSED) {
if (this.queue.length === 0) {
await new Promise((resolve) => setTimeout(resolve, 50));
continue;
}

const {payload} = JSON.parse(this.queue.shift());
if (!payload?.data?.subscribe?.data) continue;

const {__typename, name, previousTimestamp, currentTimestamp, timestamp} = payload.data.subscribe.data;
if (__typename !== 'FullFrameMessageData' && __typename !== 'DiffFrameMessageData') continue;

const canvas = name.match(/-frame\/(\d)\//)[1];
if (previousTimestamp && previousTimestamp !== this.canvasTimestamps[canvas]) {
console.log('Missing diff frame, reconnecting...');
ws.close();
continue;
}
this.canvasTimestamps[canvas] = currentTimestamp ?? timestamp;

const canvasPosition = canvasPositions[canvas];

if (__typename === 'FullFrameMessageData') {
this.canvas.clearRect(canvasPosition[0], canvasPosition[1], 1000, 1000);
}

const image = await fetch(name);
const parsedImage = await loadImage(Buffer.from(await image.arrayBuffer()));
this.canvas.drawImage(parsedImage, canvasPosition[0], canvasPosition[1]);
}
}

async getAccessToken() {
const response = await fetch('https://reddit.com/r/place', {
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/116.0'
}
});
const body = await response.text();

// todo: yuck
const configRaw = body.split('<script id="data">window.___r = ')[1].split(';</script>')[0];
const config = JSON.parse(configRaw);

return config.user.session.accessToken;
}

async updateOrders(orderpath, offset) {
this.orderCanvas = createCanvas(3000, 2000).getContext('2d');

const parsedImage = await loadImage(orderpath);
this.orderCanvas.drawImage(parsedImage, offset[0] + 1500, offset[1] + 1000);
}

getOrderDifference() {
let right = 0;
let wrong = 0;

const orderData = this.orderCanvas.getImageData(0, 0, 3000, 2000);
const canvasData = this.canvas.getImageData(0, 0, 3000, 2000);

for (let x = 0; x < 3000; x++) {
for (let y = 0; y < 2000; y++) {
const i = ((y * 3000) + x) * 4;
const a = orderData.data[i + 3];
if (a === 0) continue;

const r = orderData.data[i];
const g = orderData.data[i + 1];
const b = orderData.data[i + 2];
const currentR = canvasData.data[i];
const currentG = canvasData.data[i + 1];
const currentB = canvasData.data[i + 2];

if (r === currentR && g === currentG && b === currentB) {
right++;
} else {
wrong++;
}
}
}

return {right, wrong, total: right + wrong};
}

}
Loading

0 comments on commit a50b306

Please sign in to comment.