Untitled

 avatar
unknown
javascript
3 years ago
10 kB
2
Indexable
// ==UserScript==
// @name         ForsenPlace Script (Modified by YisusOnDev for esPlace)
// @namespace    https://github.com/YisusOnDev/esplace-war
// @version      16
// @description  Script 
// @author       ForsenPlace
// @match        https://www.reddit.com/r/place/*
// @match        https://new.reddit.com/r/place/*
// @icon         https://cdn.frankerfacez.com/emoticon/545961/4
// @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/YisusOnDev/esplace-war/raw/main/script.user.js
// @downloadURL  https://github.com/YisusOnDev/esplace-war/main/script.user.js
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant GM.xmlHttpRequest
// @connect reddit.com
// ==/UserScript==

const ORDERS_URL = 'https://raw.githubusercontent.com/YisusOnDev/esplace-war/main/orders.json'

const ORDER_UPDATE_DELAY = 4 * 60 * 1000
const TOAST_DURATION = 10 * 1000
const MAP_ERROR_RETRY_DELAY = 6 * 1000
const PARSE_ERROR_REFRESH_DELAY = 10 * 1000
const AFTER_PAINT_DELAY = 5.25 * 60 * 1000
const CHECK_AGAIN_DELAY = 30 * 1000
const REFRESH_TOKEN_DELAY = 30 * 60 * 1000

const COLOR_TO_INDEX = {
	'#6D001A': 0,
	'#BE0039': 1,
	'#FF4500': 2,
	'#FFA800': 3,
	'#FFD635': 4,
	'#FFF8B8': 5,
	'#00A368': 6,
	'#00CC78': 7,
	'#7EED56': 8,
	'#00756F': 9,
	'#009EAA': 10,
	'#00CCC0': 11,
	'#2450A4': 12,
	'#3690EA': 13,
	'#51E9F4': 14,
	'#493AC1': 15,
	'#6A5CFF': 16,
	'#94B3FF': 17,
	'#811E9F': 18,
	'#B44AC0': 19,
	'#E4ABFF': 20,
	'#DE107F': 21,
	'#FF3881': 22,
	'#FF99AA': 23,
	'#6D482F': 24,
	'#9C6926': 25,
	'#FFB470': 26,
	'#000000': 27,
	'#515252': 28,
	'#898D90': 29,
	'#D4D7D9': 30,
	'#FFFFFF': 31
};
const INDEX_TO_NAME = {
	'0': 'burgundy',
	'1': 'dark red',
	'2': 'red',
	'3': 'orange',
	'4': 'yellow',
	'5': 'pale yellow',
	'6': 'dark green',
	'7': 'green',
	'8': 'light green',
	'9': 'dark teal',
	'10': 'teal',
	'11': 'light teal',
	'12': 'dark blue',
	'13': 'blue',
	'14': 'light blue',
	'15': 'indigo',
	'16': 'periwinkle',
	'17': 'lavender',
	'18': 'dark purple',
	'19': 'purple',
	'20': 'pale purple',
	'21': 'magenta',
	'22': 'pink',
	'23': 'light pink',
	'24': 'dark brown',
	'25': 'brown', 
	'26': 'beige', 
	'27': 'black',
	'28': 'dark gray',
	'29': 'gray',
	'30': 'light gray',
	'31': 'white'
};

var currentOrdersByPrio = [];
var accessToken;
var canvas = document.createElement('canvas');

(async function () {
	GM_addStyle(GM_getResourceText('TOASTIFY_CSS'));
	canvas.width = 2000;
	canvas.height = 2000;
	canvas.style.display = 'none';
	canvas = document.body.appendChild(canvas);

	// Get the token
	Toastify({
		text: 'Obtaining access token...',
		duration: TOAST_DURATION
	}).showToast();
	accessToken = await getAccessToken();
	Toastify({
		text: 'Obtained access token!',
		duration: TOAST_DURATION
	}).showToast();

	// Start working
	await updateOrders();
	executeOrders();

	// Periodically refresh the orders
	setInterval(updateOrders, ORDER_UPDATE_DELAY);

	// Periodically refresh the token
	setInterval(async () => {
		Toastify({
			text: 'Refreshing access token...',
			duration: TOAST_DURATION
		}).showToast();
        accessToken = await getAccessToken();
		Toastify({
			text: 'Refreshed access token!',
			duration: TOAST_DURATION
		}).showToast();
    }, REFRESH_TOKEN_DELAY)
})();

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();

	return responseText.split('\"accessToken\":\"')[1].split('"')[0];
}

function updateOrders() {
	fetch(ORDERS_URL).then(async (response) => {
		if (!response.ok) return console.warn('Couldn\'t get orders (error response code)');
		const newOrders = await response.json();

		if (JSON.stringify(newOrders) !== JSON.stringify(currentOrdersByPrio)) {
			currentOrdersByPrio = newOrders;
			Toastify({
				text: `Obtained new orders!`,
				duration: TOAST_DURATION
			}).showToast();
		}
	}).catch((e) => console.warn('Couldn\'t get orders', e));
}

async function executeOrders() {
	var ctx;
	try {
		ctx = await getCanvasFromUrl(await getCurrentImageUrl('0'), 0, 0);
		ctx = await getCanvasFromUrl(await getCurrentImageUrl('1'), 1000, 0);
		ctx = await getCanvasFromUrl(await getCurrentImageUrl('2'), 0, 1000);
		ctx = await getCanvasFromUrl(await getCurrentImageUrl('3'), 1000, 1000);
	} catch (e) {
		console.warn('Error obtaining map', e);
		Toastify({
			text: `Couldn\'t get map. Trying again in ${MAP_ERROR_RETRY_DELAY / 1000} seconds...`,
			duration: MAP_ERROR_RETRY_DELAY
		}).showToast();
		setTimeout(executeOrders, MAP_ERROR_RETRY_DELAY);
		return;
	}

	for (const [prioIndex, orders] of currentOrdersByPrio.entries()) {
		let start = Math.floor(Math.random() * orders.length);
		for (let offset = 0; offset < orders.length; offset++) {
			const order = orders[(start + offset) % orders.length]
			const x = order[0];
			const y = order[1];
			const colorId = order[2];
			const rgbaAtLocation = ctx.getImageData(x, y, 1, 1).data;
			const hex = rgbToHex(rgbaAtLocation[0], rgbaAtLocation[1], rgbaAtLocation[2]);
			const currentColorId = COLOR_TO_INDEX[hex];
	
			// If the pixel color is already correct skip
			if (currentColorId == colorId) continue;
	
			Toastify({
				text: `Changing pixel on ${x}, ${y} with priority ${prioIndex + 1} from ${INDEX_TO_NAME[currentColorId]} to ${INDEX_TO_NAME[colorId]}`,
				duration: TOAST_DURATION
			}).showToast();
			const res = await place(x, y, colorId);
			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 : `Too early to place pixel! Next pixel at ${ nextPixelDate.toLocaleTimeString()}`,
						duration: delay
					}).showToast();
					setTimeout(executeOrders, 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 : `Pixel placed on ${x}, ${y}! Next pixel at ${nextPixelDate.toLocaleTimeString()}`,
						duration: delay
					}).showToast();
					setTimeout(executeOrders, delay);
				}
			} catch (e) {
				// The token probably expired, refresh and hope for the best
				console.warn ('Error parsing response', e);
				Toastify({
					text : `Error parsing response after placing pixel. Refreshing the page in ${PARSE_ERROR_REFRESH_DELAY / 1000} seconds...`,
					duration: PARSE_ERROR_REFRESH_DELAY
				}).showToast();
				setTimeout(() => {
					window.location.reload();
				}, PARSE_ERROR_REFRESH_DELAY);
			}
	
			return;
		}
	}

	Toastify({
		text: `Every pixel is correct! checking again in ${CHECK_AGAIN_DELAY / 1000} seconds...`,
		duration: CHECK_AGAIN_DELAY
	}).showToast();
	setTimeout(executeOrders, CHECK_AGAIN_DELAY);
}

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 % 1000,
							'y': y % 1000
						},
						'colorIndex': color,
						'canvasIndex': getCanvasIndex(x, y)
					}
				}
			},
			'query': 'mutation setPixel($input: ActInput!) { act(input: $input) { data { ... on BasicMessage { id data { ... on GetUserCooldownResponseMessageData { nextAvailablePixelTimestamp __typename } ... on SetPixelResponseMessageData { timestamp __typename } __typename } __typename } __typename } __typename } }'
		}),
		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'
		}
	});
}

function getCanvasIndex(x, y) {
    if (x <= 999) {
        return y <= 999 ? 0 : 2;
    } else {
        return y <= 999 ? 1 : 3;
    }
}

async function getCurrentImageUrl(tag) {
	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': tag
							}
						}
					},
					'extensions': {},
					'operationName': 'replace',
					'query': 'subscription replace($input: SubscribeInput!) { subscribe(input: $input) { id ... on BasicMessage { data { __typename ... on FullFrameMessageData { __typename name timestamp } } __typename } __typename } }'
				}
			}));
		};

		ws.onmessage = (message) => {
			const { data } = message;
			const parsed = JSON.parse(data);

			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 + `?noCache=${Date.now() * Math.random()}`);
		}

		ws.onerror = reject;
	});
}

function getCanvasFromUrl(url, x, y) {
	return new Promise((resolve, reject) => {
		var ctx = canvas.getContext('2d');
		GM.xmlHttpRequest({
			method: "GET",
			url: url,
			responseType: 'blob',
			onload: function(response) {
				var urlCreator = window.URL || window.webkitURL;
				var imageUrl = urlCreator.createObjectURL(this.response);
				var img = new Image();
				img.onload = () => {
					ctx.drawImage(img, x, y);
					resolve(ctx);
				};
			img.src = imageUrl;
			}
		});
	});
}

function rgbToHex(r, g, b) {
	return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}