├── README.md └── script.user.js /README.md: -------------------------------------------------------------------------------- 1 | # ForsenPlace Script 2 | 3 | Forsen related script. Forsen bots, automation, cheats, extensions, total r/place domination. Everything that is somewhat related to Forsen. 4 | 5 | ## Instructions 6 | 7 | 1. Install the [Tampermonkey](https://www.tampermonkey.net) browser extension. 8 | 2. [Click this](https://github.com/ForsenPlace/Script/raw/main/script.user.js). Tampermonkey should let you install the script. Click on **Install**. 9 | 3. Restart your browser. 10 | 4. Open a [r/place](https://www.reddit.com/r/place) tab. If it's working you will see "Obtaining access token..." at the top right. 11 | 12 | ## Things to know 13 | 14 | - The script automatically fetches new [orders](https://github.com/ForsenPlace/Orders). 15 | - Using multiple accounts at the same time can get you "banned" (you get a very long delay) from r/place. 16 | -------------------------------------------------------------------------------- /script.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name ForsenPlace Script 3 | // @namespace https://github.com/ForsenPlace/Script 4 | // @version 16 5 | // @description Script 6 | // @author ForsenPlace 7 | // @match https://www.reddit.com/r/place/* 8 | // @match https://new.reddit.com/r/place/* 9 | // @icon https://cdn.frankerfacez.com/emoticon/545961/4 10 | // @require https://cdn.jsdelivr.net/npm/toastify-js 11 | // @resource TOASTIFY_CSS https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css 12 | // @updateURL https://github.com/ForsenPlace/Script/raw/main/script.user.js 13 | // @downloadURL https://github.com/ForsenPlace/Script/main/script.user.js 14 | // @grant GM_getResourceText 15 | // @grant GM_addStyle 16 | // @grant GM.xmlHttpRequest 17 | // @connect reddit.com 18 | // ==/UserScript== 19 | 20 | const ORDERS_URL = 'https://raw.githubusercontent.com/ForsenPlace/Orders/main/orders.json' 21 | 22 | const ORDER_UPDATE_DELAY = 4 * 60 * 1000 23 | const TOAST_DURATION = 10 * 1000 24 | const MAP_ERROR_RETRY_DELAY = 6 * 1000 25 | const PARSE_ERROR_REFRESH_DELAY = 10 * 1000 26 | const AFTER_PAINT_DELAY = 5.25 * 60 * 1000 27 | const CHECK_AGAIN_DELAY = 30 * 1000 28 | const REFRESH_TOKEN_DELAY = 30 * 60 * 1000 29 | 30 | const COLOR_TO_INDEX = { 31 | '#6D001A': 0, 32 | '#BE0039': 1, 33 | '#FF4500': 2, 34 | '#FFA800': 3, 35 | '#FFD635': 4, 36 | '#FFF8B8': 5, 37 | '#00A368': 6, 38 | '#00CC78': 7, 39 | '#7EED56': 8, 40 | '#00756F': 9, 41 | '#009EAA': 10, 42 | '#00CCC0': 11, 43 | '#2450A4': 12, 44 | '#3690EA': 13, 45 | '#51E9F4': 14, 46 | '#493AC1': 15, 47 | '#6A5CFF': 16, 48 | '#94B3FF': 17, 49 | '#811E9F': 18, 50 | '#B44AC0': 19, 51 | '#E4ABFF': 20, 52 | '#DE107F': 21, 53 | '#FF3881': 22, 54 | '#FF99AA': 23, 55 | '#6D482F': 24, 56 | '#9C6926': 25, 57 | '#FFB470': 26, 58 | '#000000': 27, 59 | '#515252': 28, 60 | '#898D90': 29, 61 | '#D4D7D9': 30, 62 | '#FFFFFF': 31 63 | }; 64 | const INDEX_TO_NAME = { 65 | '0': 'burgundy', 66 | '1': 'dark red', 67 | '2': 'red', 68 | '3': 'orange', 69 | '4': 'yellow', 70 | '5': 'pale yellow', 71 | '6': 'dark green', 72 | '7': 'green', 73 | '8': 'light green', 74 | '9': 'dark teal', 75 | '10': 'teal', 76 | '11': 'light teal', 77 | '12': 'dark blue', 78 | '13': 'blue', 79 | '14': 'light blue', 80 | '15': 'indigo', 81 | '16': 'periwinkle', 82 | '17': 'lavender', 83 | '18': 'dark purple', 84 | '19': 'purple', 85 | '20': 'pale purple', 86 | '21': 'magenta', 87 | '22': 'pink', 88 | '23': 'light pink', 89 | '24': 'dark brown', 90 | '25': 'brown', 91 | '26': 'beige', 92 | '27': 'black', 93 | '28': 'dark gray', 94 | '29': 'gray', 95 | '30': 'light gray', 96 | '31': 'white' 97 | }; 98 | 99 | var currentOrdersByPrio = []; 100 | var accessToken; 101 | var canvas = document.createElement('canvas'); 102 | 103 | (async function () { 104 | GM_addStyle(GM_getResourceText('TOASTIFY_CSS')); 105 | canvas.width = 2000; 106 | canvas.height = 2000; 107 | canvas.style.display = 'none'; 108 | canvas = document.body.appendChild(canvas); 109 | 110 | // Get the token 111 | Toastify({ 112 | text: 'Obtaining access token...', 113 | duration: TOAST_DURATION 114 | }).showToast(); 115 | accessToken = await getAccessToken(); 116 | Toastify({ 117 | text: 'Obtained access token!', 118 | duration: TOAST_DURATION 119 | }).showToast(); 120 | 121 | // Start working 122 | await updateOrders(); 123 | executeOrders(); 124 | 125 | // Periodically refresh the orders 126 | setInterval(updateOrders, ORDER_UPDATE_DELAY); 127 | 128 | // Periodically refresh the token 129 | setInterval(async () => { 130 | Toastify({ 131 | text: 'Refreshing access token...', 132 | duration: TOAST_DURATION 133 | }).showToast(); 134 | accessToken = await getAccessToken(); 135 | Toastify({ 136 | text: 'Refreshed access token!', 137 | duration: TOAST_DURATION 138 | }).showToast(); 139 | }, REFRESH_TOKEN_DELAY) 140 | })(); 141 | 142 | async function getAccessToken() { 143 | const usingOldReddit = window.location.href.includes('new.reddit.com'); 144 | const url = usingOldReddit ? 'https://new.reddit.com/r/place/' : 'https://www.reddit.com/r/place/'; 145 | const response = await fetch(url); 146 | const responseText = await response.text(); 147 | 148 | return responseText.split('\"accessToken\":\"')[1].split('"')[0]; 149 | } 150 | 151 | function updateOrders() { 152 | fetch(ORDERS_URL).then(async (response) => { 153 | if (!response.ok) return console.warn('Couldn\'t get orders (error response code)'); 154 | const newOrders = await response.json(); 155 | 156 | if (JSON.stringify(newOrders) !== JSON.stringify(currentOrdersByPrio)) { 157 | currentOrdersByPrio = newOrders; 158 | Toastify({ 159 | text: `Obtained new orders!`, 160 | duration: TOAST_DURATION 161 | }).showToast(); 162 | } 163 | }).catch((e) => console.warn('Couldn\'t get orders', e)); 164 | } 165 | 166 | async function executeOrders() { 167 | var ctx; 168 | try { 169 | ctx = await getCanvasFromUrl(await getCurrentImageUrl('0'), 0, 0); 170 | ctx = await getCanvasFromUrl(await getCurrentImageUrl('1'), 1000, 0); 171 | ctx = await getCanvasFromUrl(await getCurrentImageUrl('2'), 0, 1000); 172 | ctx = await getCanvasFromUrl(await getCurrentImageUrl('3'), 1000, 1000); 173 | } catch (e) { 174 | console.warn('Error obtaining map', e); 175 | Toastify({ 176 | text: `Couldn\'t get map. Trying again in ${MAP_ERROR_RETRY_DELAY / 1000} seconds...`, 177 | duration: MAP_ERROR_RETRY_DELAY 178 | }).showToast(); 179 | setTimeout(executeOrders, MAP_ERROR_RETRY_DELAY); 180 | return; 181 | } 182 | 183 | for (const [prioIndex, orders] of currentOrdersByPrio.entries()) { 184 | let start = Math.floor(Math.random() * orders.length); 185 | for (let offset = 0; offset < orders.length; offset++) { 186 | const order = orders[(start + offset) % orders.length] 187 | const x = order[0]; 188 | const y = order[1]; 189 | const colorId = order[2]; 190 | const rgbaAtLocation = ctx.getImageData(x, y, 1, 1).data; 191 | const hex = rgbToHex(rgbaAtLocation[0], rgbaAtLocation[1], rgbaAtLocation[2]); 192 | const currentColorId = COLOR_TO_INDEX[hex]; 193 | 194 | // If the pixel color is already correct skip 195 | if (currentColorId == colorId) continue; 196 | 197 | Toastify({ 198 | text: `Changing pixel on ${x}, ${y} with priority ${prioIndex + 1} from ${INDEX_TO_NAME[currentColorId]} to ${INDEX_TO_NAME[colorId]}`, 199 | duration: TOAST_DURATION 200 | }).showToast(); 201 | const res = await place(x, y, colorId); 202 | const data = await res.json(); 203 | 204 | try { 205 | if (data.errors) { 206 | const error = data.errors[0]; 207 | const nextPixel = error.extensions.nextAvailablePixelTs + 3000; 208 | const nextPixelDate = new Date(nextPixel); 209 | const delay = nextPixelDate.getTime() - Date.now(); 210 | Toastify({ 211 | text : `Too early to place pixel! Next pixel at ${ nextPixelDate.toLocaleTimeString()}`, 212 | duration: delay 213 | }).showToast(); 214 | setTimeout(executeOrders, delay); 215 | } else { 216 | const nextPixel = data.data.act.data[0].data.nextAvailablePixelTimestamp + 3000; 217 | const nextPixelDate = new Date(nextPixel); 218 | const delay = nextPixelDate.getTime() - Date.now(); 219 | Toastify({ 220 | text : `Pixel placed on ${x}, ${y}! Next pixel at ${nextPixelDate.toLocaleTimeString()}`, 221 | duration: delay 222 | }).showToast(); 223 | setTimeout(executeOrders, delay); 224 | } 225 | } catch (e) { 226 | // The token probably expired, refresh and hope for the best 227 | console.warn ('Error parsing response', e); 228 | Toastify({ 229 | text : `Error parsing response after placing pixel. Refreshing the page in ${PARSE_ERROR_REFRESH_DELAY / 1000} seconds...`, 230 | duration: PARSE_ERROR_REFRESH_DELAY 231 | }).showToast(); 232 | setTimeout(() => { 233 | window.location.reload(); 234 | }, PARSE_ERROR_REFRESH_DELAY); 235 | } 236 | 237 | return; 238 | } 239 | } 240 | 241 | Toastify({ 242 | text: `Every pixel is correct! checking again in ${CHECK_AGAIN_DELAY / 1000} seconds...`, 243 | duration: CHECK_AGAIN_DELAY 244 | }).showToast(); 245 | setTimeout(executeOrders, CHECK_AGAIN_DELAY); 246 | } 247 | 248 | function place(x, y, color) { 249 | return fetch('https://gql-realtime-2.reddit.com/query', { 250 | method: 'POST', 251 | body: JSON.stringify({ 252 | 'operationName': 'setPixel', 253 | 'variables': { 254 | 'input': { 255 | 'actionName': 'r/replace:set_pixel', 256 | 'PixelMessageData': { 257 | 'coordinate': { 258 | 'x': x % 1000, 259 | 'y': y % 1000 260 | }, 261 | 'colorIndex': color, 262 | 'canvasIndex': getCanvasIndex(x, y) 263 | } 264 | } 265 | }, 266 | '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 } }' 267 | }), 268 | headers: { 269 | 'origin': 'https://hot-potato.reddit.com', 270 | 'referer': 'https://hot-potato.reddit.com/', 271 | 'apollographql-client-name': 'mona-lisa', 272 | 'Authorization': `Bearer ${accessToken}`, 273 | 'Content-Type': 'application/json' 274 | } 275 | }); 276 | } 277 | 278 | function getCanvasIndex(x, y) { 279 | if (x <= 999) { 280 | return y <= 999 ? 0 : 2; 281 | } else { 282 | return y <= 999 ? 1 : 3; 283 | } 284 | } 285 | 286 | async function getCurrentImageUrl(tag) { 287 | return new Promise((resolve, reject) => { 288 | const ws = new WebSocket('wss://gql-realtime-2.reddit.com/query', 'graphql-ws'); 289 | 290 | ws.onopen = () => { 291 | ws.send(JSON.stringify({ 292 | 'type': 'connection_init', 293 | 'payload': { 294 | 'Authorization': `Bearer ${accessToken}` 295 | } 296 | })); 297 | ws.send(JSON.stringify({ 298 | 'id': '1', 299 | 'type': 'start', 300 | 'payload': { 301 | 'variables': { 302 | 'input': { 303 | 'channel': { 304 | 'teamOwner': 'AFD2022', 305 | 'category': 'CANVAS', 306 | 'tag': tag 307 | } 308 | } 309 | }, 310 | 'extensions': {}, 311 | 'operationName': 'replace', 312 | 'query': 'subscription replace($input: SubscribeInput!) { subscribe(input: $input) { id ... on BasicMessage { data { __typename ... on FullFrameMessageData { __typename name timestamp } } __typename } __typename } }' 313 | } 314 | })); 315 | }; 316 | 317 | ws.onmessage = (message) => { 318 | const { data } = message; 319 | const parsed = JSON.parse(data); 320 | 321 | if (!parsed.payload || !parsed.payload.data || !parsed.payload.data.subscribe || !parsed.payload.data.subscribe.data) return; 322 | 323 | ws.close(); 324 | resolve(parsed.payload.data.subscribe.data.name + `?noCache=${Date.now() * Math.random()}`); 325 | } 326 | 327 | ws.onerror = reject; 328 | }); 329 | } 330 | 331 | function getCanvasFromUrl(url, x, y) { 332 | return new Promise((resolve, reject) => { 333 | var ctx = canvas.getContext('2d'); 334 | GM.xmlHttpRequest({ 335 | method: "GET", 336 | url: url, 337 | responseType: 'blob', 338 | onload: function(response) { 339 | var urlCreator = window.URL || window.webkitURL; 340 | var imageUrl = urlCreator.createObjectURL(this.response); 341 | var img = new Image(); 342 | img.onload = () => { 343 | ctx.drawImage(img, x, y); 344 | resolve(ctx); 345 | }; 346 | img.src = imageUrl; 347 | } 348 | }); 349 | }); 350 | } 351 | 352 | function rgbToHex(r, g, b) { 353 | return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase(); 354 | } 355 | --------------------------------------------------------------------------------