class LocalStoragePseudoCloud {
constructor() {
this.peer = null;
this.conn = null;
this.isHost = false;
this.VAR_PREFIX = 'pm_var_';
this.LIST_PREFIX = 'pm_list_';
this.LOG_KEY = 'pm_local_log';
this.BANNED_USERS_KEY = 'pm_banned_users';
this.ALLOWED_USER = 'ralsei-';
this.messageCooldown = 2000; // 2 seconds per message per user
this.lastMessageTime = {};
// Auto-load PeerJS
if (typeof Peer === 'undefined') {
const s = document.createElement('script');
s.src = 'https://unpkg.com/[email protected]/dist/peerjs.min.js';
s.onload = () => console.log('PeerJS loaded.');
document.head.appendChild(s);
}
}
getInfo() {
return {
id: 'LocalStoragePseudoCloud',
name: 'Local Storage Cloud (WebRTC) Advanced',
color1: '#5ba58c',
color2: '#4b8d78',
blocks: [
// VARIABLES
{ opcode: 'setVar', blockType: Scratch.BlockType.COMMAND, text: 'set variable [KEY] to [VALUE]', arguments: { KEY: { type: Scratch.ArgumentType.STRING }, VALUE: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'getVar', blockType: Scratch.BlockType.REPORTER, text: 'get variable [KEY]', arguments: { KEY: { type: Scratch.ArgumentType.STRING } } },
// LISTS
{ opcode: 'createList', blockType: Scratch.BlockType.COMMAND, text: 'create list [LIST]', arguments: { LIST: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'addToList', blockType: Scratch.BlockType.COMMAND, text: 'add [ITEM] to list [LIST]', arguments: { ITEM: { type: Scratch.ArgumentType.STRING }, LIST: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'setItem', blockType: Scratch.BlockType.COMMAND, text: 'set item [INDEX] of list [LIST] to [ITEM]', arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, LIST: { type: Scratch.ArgumentType.STRING }, ITEM: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'deleteItem', blockType: Scratch.BlockType.COMMAND, text: 'delete item [INDEX] of list [LIST]', arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, LIST: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'getItem', blockType: Scratch.BlockType.REPORTER, text: 'item [INDEX] of list [LIST]', arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, LIST: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'listLength', blockType: Scratch.BlockType.REPORTER, text: 'length of list [LIST]', arguments: { LIST: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'clearList', blockType: Scratch.BlockType.COMMAND, text: 'clear list [LIST]', arguments: { LIST: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'listContains', blockType: Scratch.BlockType.BOOLEAN, text: 'does list [LIST] contain [ITEM]', arguments: { LIST: { type: Scratch.ArgumentType.STRING }, ITEM: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'listAsArray', blockType: Scratch.BlockType.REPORTER, text: 'list [LIST] as array', arguments: { LIST: { type: Scratch.ArgumentType.STRING } } },
// CHAT-FRIENDLY BLOCKS
{ opcode: 'sendMessage', blockType: Scratch.BlockType.COMMAND, text: 'send message [TEXT] as [USERNAME]', arguments: { TEXT: { type: Scratch.ArgumentType.STRING }, USERNAME: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'lastMessage', blockType: Scratch.BlockType.REPORTER, text: 'last message' },
{ opcode: 'allMessages', blockType: Scratch.BlockType.REPORTER, text: 'all messages' },
{ opcode: 'clearChat', blockType: Scratch.BlockType.COMMAND, text: 'clear chat' },
{ opcode: 'messageCount', blockType: Scratch.BlockType.REPORTER, text: 'messages count' },
// BAN USERS
{ opcode: 'banUser', blockType: Scratch.BlockType.COMMAND, text: 'ban user [USERNAME]', arguments: { USERNAME: { type: Scratch.ArgumentType.STRING } } },
// LOGGING
{ opcode: 'clearLogs', blockType: Scratch.BlockType.COMMAND, text: 'clear logs' },
{ opcode: 'logsAsArray', blockType: Scratch.BlockType.REPORTER, text: 'logs as array' },
// CLOUD
{ opcode: 'startHost', blockType: Scratch.BlockType.COMMAND, text: 'start cloud host' },
{ opcode: 'connectCloud', blockType: Scratch.BlockType.COMMAND, text: 'connect to cloud with code [CODE]', arguments: { CODE: { type: Scratch.ArgumentType.STRING } } },
{ opcode: 'cloudStatus', blockType: Scratch.BlockType.REPORTER, text: 'cloud status' }
]
};
}
// ---------- helpers ----------
_log(action, user = '') {
const log = JSON.parse(localStorage.getItem(this.LOG_KEY) || '[]');
log.push({ time: new Date().toISOString(), user, action });
localStorage.setItem(this.LOG_KEY, JSON.stringify(log));
}
_send(type, payload) {
if (this.conn && this.conn.open) this.conn.send({ type, payload });
}
_syncAll() {
const vars = {};
const lists = {};
for (let k in localStorage) {
if (k.startsWith(this.VAR_PREFIX)) vars[k] = localStorage.getItem(k);
if (k.startsWith(this.LIST_PREFIX)) lists[k] = localStorage.getItem(k);
}
this._send('sync', { vars, lists });
}
// ---------- VARIABLES ----------
setVar(a) {
localStorage.setItem(this.VAR_PREFIX + a.KEY, a.VALUE);
this._log(`set variable ${a.KEY}=${a.VALUE}`);
this._send('var', a);
}
getVar(a) {
return localStorage.getItem(this.VAR_PREFIX + a.KEY) || '';
}
// ---------- LISTS ----------
createList(a) {
localStorage.setItem(this.LIST_PREFIX + a.LIST, '[]');
this._log(`created list ${a.LIST}`);
this._send('list', { name: a.LIST, value: [] });
}
_getList(name) {
return JSON.parse(localStorage.getItem(this.LIST_PREFIX + name) || '[]');
}
_setList(name, arr) {
localStorage.setItem(this.LIST_PREFIX + name, JSON.stringify(arr));
this._log(`set list ${name}=${JSON.stringify(arr)}`);
this._send('list', { name, value: arr });
}
addToList(a) {
const l = this._getList(a.LIST);
l.push(a.ITEM);
this._setList(a.LIST, l);
}
setItem(a) {
const l = this._getList(a.LIST);
if (a.INDEX > 0 && a.INDEX <= l.length) {
l[a.INDEX - 1] = a.ITEM;
this._setList(a.LIST, l);
}
}
deleteItem(a) {
const l = this._getList(a.LIST);
if (a.INDEX > 0 && a.INDEX <= l.length) {
l.splice(a.INDEX - 1, 1);
this._setList(a.LIST, l);
}
}
getItem(a) { return this._getList(a.LIST)[a.INDEX-1] || ''; }
listLength(a) { return this._getList(a.LIST).length; }
clearList(a) { this._setList(a.LIST, []); }
listContains(a) { return this._getList(a.LIST).includes(a.ITEM); }
listAsArray(a) { return JSON.stringify(this._getList(a.LIST)); }
// ---------- CHAT ----------
_chatListName() { return 'chat'; }
sendMessage(a) {
const user = a.USERNAME || 'guest';
const now = Date.now();
if (this.lastMessageTime[user] && now - this.lastMessageTime[user] < this.messageCooldown) return;
this.lastMessageTime[user] = now;
const banned = JSON.parse(localStorage.getItem(this.BANNED_USERS_KEY) || '[]');
if (banned.includes(user)) return;
const chat = this._getList(this._chatListName());
chat.push(`${user}: ${a.TEXT}`);
this._setList(this._chatListName(), chat);
this._log(`send message as ${user}: ${a.TEXT}`, user);
}
lastMessage() {
const chat = this._getList(this._chatListName());
return chat[chat.length-1] || '';
}
allMessages() { return this._getList(this._chatListName()); }
clearChat() { this._setList(this._chatListName(), []); this._log('cleared chat'); }
messageCount() { return this._getList(this._chatListName()).length; }
// ---------- BAN ----------
banUser(a) {
if (this.getVar({KEY:'username'}) !== this.ALLOWED_USER) return;
const banned = JSON.parse(localStorage.getItem(this.BANNED_USERS_KEY) || '[]');
if (!banned.includes(a.USERNAME)) banned.push(a.USERNAME);
localStorage.setItem(this.BANNED_USERS_KEY, JSON.stringify(banned));
this._log(`banned user ${a.USERNAME}`, this.ALLOWED_USER);
}
// ---------- LOGS ----------
clearLogs() { localStorage.setItem(this.LOG_KEY, '[]'); }
logsAsArray() { return JSON.stringify(JSON.parse(localStorage.getItem(this.LOG_KEY)||'[]')); }
// ---------- CLOUD ----------
startHost() {
if (typeof Peer === 'undefined') { alert('PeerJS not loaded yet. Wait a second.'); return; }
this.isHost = true;
this.peer = new Peer();
this.peer.on('open', id => { alert('Cloud code: '+id); console.log('Hosting cloud with code:', id); });
this.peer.on('connection', conn => { this.conn = conn; conn.on('open', () => this._syncAll()); conn.on('data', d => this._handleData(d)); });
}
connectCloud(a) {
if (typeof Peer === 'undefined') { alert('PeerJS not loaded yet. Wait a second.'); return; }
this.peer = new Peer();
this.peer.on('open', () => { this.conn = this.peer.connect(a.CODE); this.conn.on('data', d => this._handleData(d)); });
}
_handleData(d) {
if(d.type==='sync'){ Object.entries(d.payload.vars).forEach(([k,v])=>localStorage.setItem(k,v)); Object.entries(d.payload.lists).forEach(([k,v])=>localStorage.setItem(k,v)); }
if(d.type==='var') localStorage.setItem(this.VAR_PREFIX+d.payload.KEY,d.payload.VALUE);
if(d.type==='list') localStorage.setItem(this.LIST_PREFIX+d.payload.name,JSON.stringify(d.payload.value));
}
cloudStatus() { if(!this.peer)return'offline'; if(this.isHost)return'hosting'; if(this.conn&&this.conn.open)return'connected'; return'connecting'; }
}
Scratch.extensions.register(new LocalStoragePseudoCloud());










0 comments