diff --git a/package.json b/package.json
index 7224655..05a2afa 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
diff --git a/src/artist/index.js b/src/artist/index.js
index acea0ae..417d629 100644
--- a/src/artist/index.js
+++ b/src/artist/index.js
@@ -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();
});
diff --git a/src/constants.js b/src/constants.js
index 650a76e..3fba1b2 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -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';
diff --git a/src/index.js b/src/index.js
index 7bf2b56..ab09ea8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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 = {
@@ -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;
@@ -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);
diff --git a/src/metrics/index.js b/src/metrics/index.js
index 29a97b8..726523e 100644
--- a/src/metrics/index.js
+++ b/src/metrics/index.js
@@ -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());
diff --git a/src/place/PlaceClient.js b/src/place/PlaceClient.js
new file mode 100644
index 0000000..eb0bce0
--- /dev/null
+++ b/src/place/PlaceClient.js
@@ -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('')[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};
+ }
+
+}
diff --git a/web/index.html b/web/index.html
index ca1e0a6..93ff071 100644
--- a/web/index.html
+++ b/web/index.html
@@ -12,22 +12,55 @@
crossorigin="anonymous" referrerpolicy="no-referrer"/>
-
+
-
-
0
-
active connections
-
(0 capable of placing pixels)
-
-
(0 pixels/min)
+
+
+
+
+
0
+ active connections
+
+
+
+
+
+
+
0
+ capable of placing pixels
+
+
+
+
+
+
+
0 pixels/min
+
+ estimated place rate
+
+
+
+
+
+
+
0
+ pixels
+
+ not matching template
+ (0% completed, template has
+ 0 pixels)
+
+
+
-
+
Loading...
@@ -38,7 +71,8 @@
(template-manger overlay
-
Powered by Chief
+
Powered by Chief