Skip to content

Instantly share code, notes, and snippets.

@GREEB
Created April 2, 2022 17:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GREEB/5b5af7ce032b0e514b5f0c3d059d2160 to your computer and use it in GitHub Desktop.
Save GREEB/5b5af7ce032b0e514b5f0c3d059d2160 to your computer and use it in GitHub Desktop.
veganplace.user.js
// ==UserScript==
// @name VeganPlace Bot
// @namespace https://github.com/Squarific/Bot
// @version 11
// @description The bot for vegans
// @author Squarific
// @match https://www.reddit.com/r/place/*
// @match https://new.reddit.com/r/place/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @require https://cdn.jsdelivr.net/npm/toastify-js
// @resource TOASTIFY_CSS https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// @updateURL https://github.com/Squarific/Bot/raw/master/placenlbot.user.js
// @downloadURL https://github.com/Squarific/Bot/raw/master/placenlbot.user.js
// @grant GM_getResourceText
// @grant GM_addStyle
// ==/UserScript==
// Sorry voor de rommelige code, haast en clean gaatn iet altijd samen ;)
var socket;
var hasOrders = false;
var accessToken;
var currentOrderCanvas = document.createElement('canvas');
var currentOrderCtx = currentOrderCanvas.getContext('2d');
var currentPlaceCanvas = document.createElement('canvas');
const COLOR_MAPPINGS = {
'#FF4500': 2,
'#FFA800': 3,
'#FFD635': 4,
'#00A368': 6,
'#7EED56': 8,
'#2450A4': 12,
'#3690EA': 13,
'#51E9F4': 14,
'#811E9F': 18,
'#B44AC0': 19,
'#FF99AA': 23,
'#9C6926': 25,
'#000000': 27,
'#898D90': 29,
'#D4D7D9': 30,
'#FFFFFF': 31
};
var order = [];
for (var i = 0; i < 1000000; i++) {
order.push(i);
}
order.sort(() => Math.random() - 0.5);
(async function () {
GM_addStyle(GM_getResourceText('TOASTIFY_CSS'));
currentOrderCanvas.width = 2000;
currentOrderCanvas.height = 1000;
currentOrderCanvas = document.body.appendChild(currentOrderCanvas);
currentPlaceCanvas.width = 2000;
currentPlaceCanvas.height = 1000;
currentPlaceCanvas = document.body.appendChild(currentPlaceCanvas);
Toastify({
text: 'Access token retrieval...',
duration: 10000
}).showToast();
accessToken = await getAccessToken();
Toastify({
text: 'Accesstoken picked up!',
duration: 10000
}).showToast();
connectSocket();
attemptPlace();
})();
function connectSocket() {
Toastify({
text: 'Connecting to PlaceNL server...',
duration: 10000
}).showToast();
socket = new WebSocket('wss://vegan.averysmets.com/api/ws');
socket.onopen = function () {
Toastify({
text: 'Connected to PlaceNL server!',
duration: 10000
}).showToast();
socket.send(JSON.stringify({ type: 'getmap' }));
};
socket.onmessage = async function (message) {
var data;
try {
data = JSON.parse(message.data);
} catch (e) {
return;
}
switch (data.type.toLowerCase()) {
case 'map':
Toastify({
text: `New map loaded (reason: ${data.reason ? data.reason : 'connected to server'})`,
duration: 10000
}).showToast();
currentOrderCtx = await getCanvasFromUrl(`https://vegan.averysmets.com/maps/${data.data}`, currentOrderCanvas);
hasOrders = true;
break;
default:
break;
}
};
socket.onclose = function (e) {
Toastify({
text: `PlaceNL server has disconnected: ${e.reason}`,
duration: 10000
}).showToast();
console.error('Socketfout: ', e.reason);
socket.close();
setTimeout(connectSocket, 1000);
};
}
async function attemptPlace() {
if (!hasOrders) {
setTimeout(attemptPlace, 2000); // probeer opnieuw in 2sec.
return;
}
var ctx;
try {
ctx = await getCanvasFromUrl(await getCurrentImageUrl('0'), currentPlaceCanvas, 0, 0);
ctx = await getCanvasFromUrl(await getCurrentImageUrl('1'), currentPlaceCanvas, 1000, 0)
} catch (e) {
console.warn('Map retrieval error: ', e);
Toastify({
text: 'Error retrieving map. Retry in 10 sec...',
duration: 10000
}).showToast();
setTimeout(attemptPlace, 10000); // probeer opnieuw in 15sec.
return;
}
const rgbaOrder = currentOrderCtx.getImageData(0, 0, 2000, 1000).data;
const rgbaCanvas = ctx.getImageData(0, 0, 2000, 1000).data;
for (const j of order) {
for (var l = 0; l < 10; l++) {
const i = (j * 10) + l;
// negeer lege order pixels.
if (rgbaOrder[(i * 4) + 3] === 0) continue;
const hex = rgbToHex(rgbaOrder[(i * 4)], rgbaOrder[(i * 4) + 1], rgbaOrder[(i * 4) + 2]);
// Deze pixel klopt.
if (hex === rgbToHex(rgbaCanvas[(i * 4)], rgbaCanvas[(i * 4) + 1], rgbaCanvas[(i * 4) + 2])) continue;
const x = i % 2000;
const y = Math.floor(i / 2000);
Toastify({
text: `Pixel trying to place on ${x}, ${y}...`,
duration: 10000
}).showToast();
const res = await place(x, y, COLOR_MAPPINGS[hex]);
const data = await res.json();
try {
if (data.errors) {
const error = data.errors[0];
const nextPixel = error.extensions.nextAvailablePixelTs + 3000;
const nextPixelDate = new Date(nextPixel);
const delay = nextPixelDate.getTime() - Date.now();
Toastify({
text: `Pixel placed too fast! Next pixel is placed at ${nextPixelDate.toLocaleTimeString()}.`,
duration: delay
}).showToast();
setTimeout(attemptPlace, delay);
} else {
const nextPixel = data.data.act.data[0].data.nextAvailablePixelTimestamp + 3000;
const nextPixelDate = new Date(nextPixel);
const delay = nextPixelDate.getTime() - Date.now();
Toastify({
text: `Error in response analysis: ${e}.`,
duration: 10000
}).showToast();
setTimeout(attemptPlace, 10000);
}
} catch (e) {
console.warn('Error in response analysis', e);
Toastify({
text: `Error in response analysis: ${e}.`,
duration: 10000
}).showToast();
setTimeout(attemptPlace, 10000);
}
return;
}
}
Toastify({
text: `All pixels are already in the right place! Try again in 30 sec....`,
duration: 30000
}).showToast();
setTimeout(attemptPlace, 30000); // probeer opnieuw in 30sec.
}
function place(x, y, color) {
return fetch('https://gql-realtime-2.reddit.com/query', {
method: 'POST',
body: JSON.stringify({
'operationName': 'setPixel',
'variables': {
'input': {
'actionName': 'r/replace:set_pixel',
'PixelMessageData': {
'coordinate': {
'x': x,
'y': y
},
'colorIndex': color,
'canvasIndex': 0
}
}
},
'query': 'mutation setPixel($input: ActInput!) {\n act(input: $input) {\n data {\n ... on BasicMessage {\n id\n data {\n ... on GetUserCooldownResponseMessageData {\n nextAvailablePixelTimestamp\n __typename\n }\n ... on SetPixelResponseMessageData {\n timestamp\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n'
}),
headers: {
'origin': 'https://hot-potato.reddit.com',
'referer': 'https://hot-potato.reddit.com/',
'apollographql-client-name': 'mona-lisa',
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
}
async function getAccessToken() {
const usingOldReddit = window.location.href.includes('new.reddit.com');
const url = usingOldReddit ? 'https://new.reddit.com/r/place/' : 'https://www.reddit.com/r/place/';
const response = await fetch(url);
const responseText = await response.text();
// TODO: ew
return responseText.split('\"accessToken\":\"')[1].split('"')[0];
}
async function getCurrentImageUrl(id = '0') {
return new Promise((resolve, reject) => {
const ws = new WebSocket('wss://gql-realtime-2.reddit.com/query', 'graphql-ws');
ws.onopen = () => {
ws.send(JSON.stringify({
'type': 'connection_init',
'payload': {
'Authorization': `Bearer ${accessToken}`
}
}));
ws.send(JSON.stringify({
'id': '1',
'type': 'start',
'payload': {
'variables': {
'input': {
'channel': {
'teamOwner': 'AFD2022',
'category': 'CANVAS',
'tag': id
}
}
},
'extensions': {},
'operationName': 'replace',
'query': 'subscription replace($input: SubscribeInput!) {\n subscribe(input: $input) {\n id\n ... on BasicMessage {\n data {\n __typename\n ... on FullFrameMessageData {\n __typename\n name\n timestamp\n }\n }\n __typename\n }\n __typename\n }\n}'
}
}));
};
ws.onmessage = (message) => {
const { data } = message;
const parsed = JSON.parse(data);
// TODO: ew
if (!parsed.payload || !parsed.payload.data || !parsed.payload.data.subscribe || !parsed.payload.data.subscribe.data) return;
ws.close();
resolve(parsed.payload.data.subscribe.data.name);
}
ws.onerror = reject;
});
}
function getCanvasFromUrl(url, canvas, x = 0, y = 0) {
return new Promise((resolve, reject) => {
var ctx = canvas.getContext('2d');
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
ctx.drawImage(img, x, y);
resolve(ctx);
};
img.onerror = reject;
img.src = url;
});
}
function rgbToHex(r, g, b) {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment