const formatMessage = require('format-message'); const BlockType = require('../../extension-support/block-type'); const ArgumentType = require('../../extension-support/argument-type'); const Cast = require('../../util/cast'); const Legacy = require('./legacy'); const Icon = require("./icon.svg"); /** * Class for Scratch Authentication blocks * @constructor */ let currentPrivateCode = ''; class JgScratchAuthenticateBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; this.promptStatus = { inProgress: false, blocked: false, completed: false, userClosed: false, }; this.loginInfo = {}; // legacy this.keepAllowingAuthBlock = true; this.disableConfirmationShown = false; } /** * dummy function for reseting user provided permisions when a save is loaded */ deserialize() { this.disableConfirmationShown = false; } /** * @returns {object} metadata for this extension and its blocks. */ getInfo() { return { id: 'jgScratchAuthenticate', name: 'Scratch Auth', color1: '#FFA01C', color2: '#ff8C00', blockIconURI: Icon, // TODO: docs doesnt exist, make some docs // docsURI: 'https://docs.penguinmod.com/extensions/scratch-auth', blocks: [ // LEGACY BLOCK { opcode: 'authenticate', text: formatMessage({ id: 'jgScratchAuthenticate.blocks.authenticate', default: 'get scratch username and set sign in location name to [NAME]', description: "Block that returns the user's name on Scratch." }), disableMonitor: true, hideFromPalette: true, arguments: { NAME: { type: ArgumentType.STRING, defaultValue: "PenguinMod" } }, blockType: BlockType.REPORTER }, // NEW BLOCKS { opcode: 'showPrompt', text: formatMessage({ id: 'jgScratchAuthenticate.blocks.showPrompt', default: 'show login message as [NAME]', description: "Block that shows the Log in menu from Scratch Authentication." }), arguments: { NAME: { type: ArgumentType.STRING, menu: 'loginLocation' } }, blockType: BlockType.COMMAND }, { opcode: 'getPromptStatus', text: formatMessage({ id: 'jgScratchAuthenticate.blocks.promptStatus', default: 'login prompt [STATUS]?', description: "The status of the login prompt for Scratch Authentication." }), arguments: { STATUS: { type: ArgumentType.STRING, menu: "promptStatus" } }, disableMonitor: true, blockType: BlockType.BOOLEAN }, { opcode: 'privateCode', text: formatMessage({ id: 'jgScratchAuthenticate.blocks.privateCode', default: 'authentication code', description: "The login code when Scratch Authentication closes the login prompt." }), disableMonitor: true, blockType: BlockType.REPORTER }, { opcode: 'serverRedirectLocation', text: formatMessage({ id: 'jgScratchAuthenticate.blocks.serverRedirectLocation', default: 'redirect location', description: "The redirect location when Scratch Authentication closes the login prompt." }), disableMonitor: true, blockType: BlockType.REPORTER }, '---', { text: formatMessage({ id: 'jgScratchAuthenticate.labels.loginInfo1', default: 'The blocks below invalidate', description: "Label to denote that blocks invalidate the Scratch Auth private code below this label" }), blockType: BlockType.LABEL }, { text: formatMessage({ id: 'jgScratchAuthenticate.labels.loginInfo2', default: 'the authentication code from above.', description: "Label to denote that blocks invalidate the Scratch Auth private code below this label" }), blockType: BlockType.LABEL }, { opcode: 'validLogin', text: formatMessage({ id: 'jgScratchAuthenticate.blocks.validLogin', default: 'login is valid?', description: "Whether or not the authentication was valid." }), disableMonitor: true, // this doesnt seem to be important, // login should always be valid when checking on client-side hideFromPalette: true, blockType: BlockType.BOOLEAN }, { opcode: 'scratchUsername', text: formatMessage({ id: 'jgScratchAuthenticate.blocks.scratchUsername', default: 'scratch username', description: "The username that was logged in." }), disableMonitor: true, blockType: BlockType.REPORTER }, ], menus: { loginLocation: { items: '_getLoginLocations', isTypeable: true, }, promptStatus: [ { text: 'in progress', value: 'inProgress' }, { text: 'blocked', value: 'blocked' }, { text: 'complete', value: 'completed' }, { text: 'closed by the user', value: 'userClosed' }, ] } }; } // menus _getLoginLocations() { const nameSplit = document.title.split(" - "); nameSplit.pop(); const projectName = Cast.toString(nameSplit.join(" - ")); return [ projectName === 'PenguinMod' ? 'Project' : projectName, 'PenguinMod', 'Game', ]; } // util async parseLoginCode_() { if (!currentPrivateCode) throw new Error('Private code not present'); const req = await fetch(`https://pm-bapi.vercel.app/api/verifyToken?privateCode=${currentPrivateCode}`); const json = await req.json(); this.loginInfo = { valid: json.valid, username: json.username }; return this.loginInfo; } // blocks showPrompt(args) { // reset this.promptStatus = { inProgress: true, blocked: false, completed: false, userClosed: false, }; this.loginInfo = {}; const loginLocation = Cast.toString(args.NAME); const sanitizedName = encodeURIComponent(loginLocation.substring(0, 256).replace(/[^a-zA-Z0-9 _\-\.\[\]\(\)]+/gmi, "")); const waitingLink = `https://studio.penguinmod.com/scratchAuthExt.html?openLocation=${encodeURIComponent(window.origin)}`; // listen for events before opening let login; let finished = false; const listener = (event) => { if (event.origin !== (new URL(waitingLink)).origin) { return; } if (!(event.data && event.data.scratchauthd1)) { return; } const data = event.data.scratchauthd1; const privateCode = data.pv; currentPrivateCode = privateCode; // update status this.promptStatus.inProgress = false; this.promptStatus.completed = true; finished = true; window.removeEventListener("message", listener); login.close(); }; window.addEventListener("message", listener); // open prompt login = window.open( `https://auth.itinerary.eu.org/auth/?redirect=${btoa(waitingLink)}${sanitizedName.length > 0 ? `&name=${sanitizedName}` : ""}`, "Scratch Authentication", `scrollbars=yes,resizable=yes,status=no,location=yes,toolbar=no,menubar=no,width=768,height=512,left=200,top=200` ); if (!login) { // popup was blocked most likely this.promptStatus.inProgress = false; this.promptStatus.blocked = true; return; } // .onclose doesnt work on most platforms it seems // so just set interval const closedInterval = setInterval(() => { if (!login.closed) return; this.promptStatus.inProgress = false; if (!finished) { this.promptStatus.userClosed = true; } window.removeEventListener("message", listener); clearInterval(closedInterval); }, 500); } privateCode() { const code = currentPrivateCode; currentPrivateCode = ''; return code; } serverRedirectLocation() { const waitingLink = `https://studio.penguinmod.com/scratchAuthExt.html?openLocation=${window.origin}`; return waitingLink; } getPromptStatus(args) { const option = Cast.toString(args.STATUS); if (!(option in this.promptStatus)) return false; return this.promptStatus[option]; } // parsing privat4e code blocks async validLogin() { if (Object.keys(this.loginInfo).length <= 0) { try { await this.parseLoginCode_(); } catch { // just say invalid if we cant parse return false; } } return !!this.loginInfo.valid; } async scratchUsername() { if (Object.keys(this.loginInfo).length <= 0) { try { await this.parseLoginCode_(); } catch { // just say no username if we cant parse return ''; } } return Cast.toString(this.loginInfo.username); } // legacy block authenticate(...args) { return Legacy.authenticate(this, ...args); } } module.exports = JgScratchAuthenticateBlocks;