0xff) {inputBufferPointer=0xE0;}
156 | }
157 | processor.writemem(IBP,inputBufferPointer);
158 | return processor.execute(text.length*15000); // Wait until Buffer empty
159 | }
160 |
161 | function pasteToBuffer(textIn) {
162 | var regex = new RegExp(/(.|[\r\n]){1,31}/g);
163 | var fragments = textIn.match(regex);
164 | if (fragments==null) return;
165 | for (const fragment of fragments) {
166 | writeToKeyboardBuffer(fragment);
167 | }
168 | return;
169 | }
170 |
171 | // The following is from https://github.com/mattgodbolt/jsbeeb/blob/master/tests/test.js
172 | function runFor(cycles) {
173 | var left = cycles;
174 | var stopped = false;
175 | return new Promise(function (resolve) {
176 | var runAnIter = function () {
177 | var todo = Math.max(0, Math.min(left, MaxCyclesPerIter));
178 | if (todo) {
179 | stopped = !processor.execute(todo);
180 | left -= todo;
181 | }
182 | if (left && !stopped) {
183 | setTimeout(runAnIter, 0);
184 | } else {
185 | resolve();
186 | }
187 | };
188 | runAnIter();
189 | });
190 | }
191 |
192 |
193 | function runUntilInput() {
194 | var idleAddr = processor.model.isMaster ? 0xe7e6 : 0xe581;
195 | var hit = false;
196 | var hook = processor.debugInstruction.add(function (addr) {
197 | if (addr === idleAddr) {
198 | hit = true;
199 | return hit;
200 | }
201 | });
202 | return runFor(20 * 2000000).then(function () {
203 | hook.remove();
204 | runFor(1);
205 | return hit;
206 | });
207 | }
208 |
209 |
210 | return {
211 | emulate:emulate,
212 | tokenise:tokenise
213 | };
214 | }
215 | );
216 |
--------------------------------------------------------------------------------
/mastodon.js:
--------------------------------------------------------------------------------
1 |
2 | const ENABLE_TEXT_REPLY = false;
3 | const log = require('npmlog');
4 | log.level = process.env.LOG_LEVEL || 'verbose';
5 | const Mastodon = require('mastodon');
6 | require('dotenv').config();
7 | const config = {
8 | access_token: process.env.ACCESS_TOKEN,
9 | api_url: `https://${process.env.API_HOST}/api/v1/`,
10 | hashtag: process.env.HASHTAG,
11 | };
12 |
13 | const mastodon = new Mastodon(config);
14 | const fs = require('fs');
15 |
16 | function exec(cmd) {
17 | const exec = require('child_process').exec;
18 | return new Promise((resolve, reject) => {
19 | exec(cmd, (error, stdout, stderr) => {
20 | if (error) {
21 | console.warn(error);
22 | }
23 | resolve(stdout? stdout.trim() : stderr);
24 | });
25 | });
26 | }
27 |
28 | function post(path, params) {
29 | log.info("Post", path, params)
30 | }
31 |
32 | function get(path, params) {
33 | log.info("get", path, params)
34 | }
35 |
36 |
37 | async function videoReply(filename, mediaType, replyTo, text, toot, checksum, hasAudio, tag) {
38 |
39 | if (toot.spoiler_text == "") {
40 | console.log("No CW on bot source post")
41 | }
42 |
43 | try {
44 | let resp = await mastodon.post('media', { file: fs.createReadStream(filename), description: "BBC Micro Bot graphics output - " + toot.spoiler_text });
45 | log.info(JSON.stringify(resp.data.id));
46 | let id = resp.data.id; // Source: https://bbcmic.ro/#"+progData
47 | let params = { status: "I ran " + text + "'s program and got this.\nSource: https://bbcmic.ro/?t=" + tag + " #bbcbasic", media_ids: [id], in_reply_to_id: replyTo };
48 | params.visibility = "public";
49 |
50 | let response = await mastodon.post('statuses', params);
51 |
52 | log.info("Media post DONE ", JSON.stringify(response.data.id));
53 |
54 | await mastodon.post('statuses/' + response.data.in_reply_to_id + '/favourite');
55 | log.info("Favourited " + toot.id);
56 |
57 | let user = response.data.in_reply_to_account_id;
58 | log.info("User " + user);
59 | let relationship = await mastodon.get('accounts/relationships', { id: user });
60 | log.info("Relationship " + (relationship.data[0].following ? "following" : "not following"));
61 |
62 | // If we're not following the user, reblog the toot
63 | if (relationship.data[0].following) {
64 | log.info("Reposting toot " + response.data.id);
65 | await mastodon.post('statuses/' + response.data.id + '/reblog');
66 | }
67 |
68 | exec('rm '+filename);
69 |
70 | //return {full:"https://bbcmic.ro/"+experimental+"#"+progData,key:short_url}
71 | }
72 |
73 | catch (e) {
74 |
75 | log.info("Media post FAILED");
76 | log.info(e);
77 | return null;
78 | }
79 | }
80 |
81 |
82 | function noOutput(toot) {
83 | console.warn("NO VIDEO CAPTURED");
84 | if (!ENABLE_TEXT_REPLY) return;
85 | try {
86 | post('statuses/update', { status: "@" + toot.user.screen_name + " Sorry, no output captured from that program", in_reply_to_status_id: toot.id });
87 | }
88 | catch (e) {
89 | log.info("Non-media post FAILED");
90 | log.info(e);
91 | }
92 | }
93 |
94 | function block(toot) {
95 | post('blocks/create', { screen_name: toot.user.screen_name });
96 | }
97 |
98 | module.exports = {
99 | videoReply: videoReply,
100 | noOutput: noOutput,
101 | block: block,
102 | post: post,
103 | get: get
104 | };
105 |
--------------------------------------------------------------------------------
/mastodon.json:
--------------------------------------------------------------------------------
1 | [06/11/2022 20:39:39 ] [LOG] Serv: {
2 | id: '109292779039545453',
3 | created_at: '2022-11-05T19:11:01.429Z',
4 | in_reply_to_id: '109292630262682237',
5 | in_reply_to_account_id: '109246658710563854',
6 | sensitive: false,
7 | spoiler_text: '',
8 | visibility: 'public',
9 | language: 'en',
10 | uri: 'https://mastodon.me.uk/users/bbcmicrobot/statuses/109292779039545453',
11 | url: 'https://mastodon.me.uk/@bbcmicrobot/109292779039545453',
12 | replies_count: 1,
13 | reblogs_count: 0,
14 | favourites_count: 2,
15 | edited_at: null,
16 | favourited: false,
17 | reblogged: false,
18 | muted: false,
19 | bookmarked: false,
20 | pinned: false,
21 | content: 'Thanks @Jaffa yeah that might be the way. I can have it respond to toots with #bbcmicrobot (I think this is specific enough not to be spammy. I will check with @Floppy once things calm down a bit)
',
22 | reblog: null,
23 | application: {
24 | name: 'Mastodon for iOS',
25 | website: 'https://app.joinmastodon.org/ios'
26 | },
27 | account: {
28 | id: '109289826032916877',
29 | username: 'bbcmicrobot',
30 | acct: 'bbcmicrobot',
31 | display_name: 'BBC Micro Bot :mastodon:',
32 | locked: false,
33 | bot: true,
34 | discoverable: true,
35 | group: false,
36 | created_at: '2022-11-05T00:00:00.000Z',
37 | note: 'Programs in a single toot of code 👾 1980s BBC Micro Model B 32K 6502. Acorn Computers and ARM stuff. BBC BASIC.
Also building a 3D #retrocomputing simulator at virtual.bbcmic.ro #bbcmicrobot
⚠️ Bot not online yet... I'm busy porting it!
https://www.bbcmicrobot.com
',
38 | url: 'https://mastodon.me.uk/@bbcmicrobot',
39 | avatar: 'https://cdn.masto.host/mastodonmeuk/accounts/avatars/109/289/826/032/916/877/original/7d1450f49b48a83e.png',
40 | avatar_static: 'https://cdn.masto.host/mastodonmeuk/accounts/avatars/109/289/826/032/916/877/original/7d1450f49b48a83e.png',
41 | header: 'https://cdn.masto.host/mastodonmeuk/accounts/headers/109/289/826/032/916/877/original/b7165f77e4b670b0.png',
42 | header_static: 'https://cdn.masto.host/mastodonmeuk/accounts/headers/109/289/826/032/916/877/original/b7165f77e4b670b0.png',
43 | followers_count: 508,
44 | following_count: 46,
45 | statuses_count: 36,
46 | last_status_at: '2022-11-07',
47 | emojis: [ [Object] ],
48 | fields: [ [Object] ]
49 | },
50 | media_attachments: [],
51 | mentions: [
52 | {
53 | id: '109246658710563854',
54 | username: 'Jaffa',
55 | url: 'https://social.linux.pizza/@Jaffa',
56 | acct: 'Jaffa@social.linux.pizza'
57 | },
58 | {
59 | id: '1',
60 | username: 'Floppy',
61 | url: 'https://mastodon.me.uk/@Floppy',
62 | acct: 'Floppy'
63 | }
64 | ],
65 | tags: [
66 | {
67 | name: 'bbcmicrobot',
68 | url: 'https://mastodon.me.uk/tags/bbcmicrobot'
69 | }
70 | ],
71 | emojis: [],
72 | card: null,
73 | poll: null
74 | }
75 |
--------------------------------------------------------------------------------
/output/README.md:
--------------------------------------------------------------------------------
1 | Program records go here
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bbcmicrobot",
3 | "version": "0.3.0",
4 | "description": "Runs your tweet on an 8-bit computer emulator",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server & node client",
8 | "test": "node server test & node client test",
9 | "install": "bash ./install.sh"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "aws-sdk": "^2.1265.0",
15 | "bad-words": "^3.0.3",
16 | "console-stamp": "^0.2.9",
17 | "dotenv": "^8.2.0",
18 | "express": "^4.17.1",
19 | "gifsicle": "^5.1.0",
20 | "grapheme-splitter": "^1.0.4",
21 | "htmlparser2": "^8.0.1",
22 | "jsbeeb": "git+https://github.com/mattgodbolt/jsbeeb.git#8935c9a3a095e846f63c0e4a08070f76ad01473e",
23 | "mastodon": "^1.2.2",
24 | "npmlog": "^7.0.1",
25 | "request": "^2.88.2",
26 | "requirejs": "^2.3.6"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/parser.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const TRY = (process.argv.indexOf("try") > -1)
4 |
5 | require('dotenv').config();
6 | const Filter = require('bad-words');
7 | const customFilter = new Filter({ placeHolder: '*'});
8 | //customFilter.addWords('words','here');
9 | const Grapheme = require('grapheme-splitter');
10 | var splitter = new Grapheme();
11 | const htmlparser2 = require('htmlparser2');
12 |
13 | function processInput(toot) {
14 | if (TRY) return toot.text.trim();
15 |
16 | var out = '';
17 | var ignore = 0;
18 | const htmlparser = new htmlparser2.Parser({
19 | onopentag(name, attributes) {
20 | if (ignore) {
21 | ++ignore;
22 | return;
23 | }
24 |
25 | var c = attributes['class'];
26 | if (c !== undefined && c.match(/\b(?:mention|hashtag)\b/)) {
27 | ignore = 1;
28 | return;
29 | }
30 | if (name === 'p' || name === 'br') out += '\n';
31 | },
32 | ontext(text) {
33 | if (!ignore) out += text;
34 | },
35 | onclosetag(name) {
36 | if (ignore) --ignore;
37 | },
38 | });
39 |
40 | console.log(toot.text)
41 | htmlparser.parseComplete(toot.text);
42 | out = out.trim();
43 | out = out.replace(/[“”]/g,'"');
44 | console.log(out)
45 |
46 | return out;
47 | }
48 |
49 | function parseTweet(toot){
50 | var graphemes = splitter.splitGraphemes(toot.text.trim());
51 | var one_hour = 2000000*60*60;
52 |
53 | var c = {
54 | emulator: "beebjit",
55 | flags: "-accurate -rom 7 roms/gxr.rom -opt video:paint-start-cycles=60680000,video:border-chars=0 -frame-cycles 1 -max-frames 150",
56 | cycles: 69000000,
57 | compressed: false,
58 | input: "",
59 | mode: 1,
60 | }
61 |
62 | for (let i = 0; i -1)
6 |
7 | const express = require('express');
8 | const https = require("https");
9 | const fs = require("fs");
10 | const cert_path = "./certs/";
11 | const Feed = TEST ? require("./test").Feed : require('./hashtag');
12 |
13 | // add timestamps in front of log messages
14 | require( 'console-stamp' )( console, { pattern: 'dd/mm/yyyy HH:MM:ss '},"Serv:" );
15 |
16 | function log(l){console.log(l)}
17 |
18 | var tootFeed = new Feed();
19 | var app = express();
20 | var emulators = 0;
21 | var served = 0;
22 |
23 | app.get('/pop', (req, res) => {
24 | if (req.client.authorized) {
25 | let toot = (tootFeed.queue.length>0) ? tootFeed.queue.pop() : "{}";
26 |
27 | res.send(toot);
28 |
29 | }
30 | })
31 |
32 | app.get('/quit', (req, res) => {
33 | if (req.client.authorized) {
34 | process.exit();
35 | }
36 | })
37 |
38 | var options = {
39 | key: fs.readFileSync(cert_path+'server_key.pem'),
40 | cert: fs.readFileSync(cert_path+'server_cert.pem'),
41 | ca: [ fs.readFileSync(cert_path+'server_cert.pem') ],
42 | requestCert: true,
43 | rejectUnauthorized: true
44 | };
45 |
46 | var listener = https.createServer(options, app).listen(PORT, function () {
47 | console.log('BBC Micro Bot toot server listening on port ' + listener.address().port);
48 | });
49 |
50 | // Poll the twitter mentions
51 |
52 | tootFeed.update();
53 | setInterval(function(){ tootFeed.update(); }, 45000);
54 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 |
4 | // Monitor timeline timeline
5 | function Tests(since_id){
6 |
7 | console.log("TEST TWEETS QUEUED");
8 |
9 | var tests = [
10 | {
11 | name: "ROCKET_MODE", // Test that a slow program completes
12 | text: "🚀0 MODE 2:VDU5\n10 FOR X = 0 TO 1279 STEP8:FOR Y = 0 TO 1023 STEP 4:GCOL 0, RND(7):PLOT 69, X, Y:NEXT:NEXT\nREPEAT UNTIL FALSE",
13 | mediaType: "image/png",
14 | checksum: "a02c1e9f18e3a86718067695c0e6e97ffdd5c6bd"
15 | },
16 | {
17 | name: "LIKE_THE_CLAPPERS", // Test clapperboard emoji
18 | text: "🎬10MO.2:REP.F.A=0TO14:P.;A:F.D=1TO360:N.,:U.0",
19 | mediaType: "image/gif",
20 | checksum: "768e2a06a7a02b115f5e8efe5fa652810e051b43"
21 | },
22 | {
23 | name: "FRAME_CAPTURE", // MODE 0-6
24 | text: "0 MODE 2\n10 FOR C = 0 TO 7\n20 COLOUR C\n30 PRINT \"COLOUR \",C\n40 NEXT C\n"+
25 | "60 MOVE 0,0\n70 DRAW 1279,0\n80 DRAW 1279,1023\n90 DRAW 0,1023\n100 DRAW 0,0\n"+
26 | "110 DRAW 1279,1023\n120 VDU 23,1,0;0;0;0;\n130 P.TAB(0,16);INT(TIME/10)/10;\" s \"\n140 GOTO 130",
27 | mediaType: "image/gif",
28 | checksum: "b0a979b0be31f48fc85b29635f55489857327f26"
29 | },
30 | {
31 | name: "CHARACTERS",
32 | text: "10 PRINT“>&<<”'SPC39\"|\"\n20 VDU 23,1,0;0;0;0;\n", // Tests twitter HTML escapes for <,&,> and OS X auto ""
33 | mediaType: "image/png",
34 | checksum: "c3f630a42cc39990a6e38c574a93f6c79b3c5a8a"
35 | },
36 | /* beebjit doesn't currently support capturing audio output
37 | {
38 | name: "STATICAUDIO", // Test static image with audio gives a video
39 | text: '0V.279;0;0;0;0;12:P."BEEP":REP.V.7:U.NOTINKEY50',
40 | mediaType: "image/gif",
41 | hasAudio: true,
42 | checksum: "810209c18581c36ad7a3eb40502519e1aec39cae"
43 | },
44 | {
45 | name: "AUDIOVISUAL", // Video with sound
46 | text: '1MO.2:V.5:ENV.1,1,-26,-36,-45,255,255,255,127,0,0,0,126,0:SO.1,1,1,1\n2GC.0,RND(7):PL.85,RND(1280),1023A.RND:G.2\n',
47 | mediaType: "image/gif",
48 | hasAudio: true,
49 | checksum: "4a954818f333f1d9a3b7334246bcdb5056295e3d"
50 | },
51 | */
52 | {
53 | name: "MODE6", // Test stripes aren't transparent in PNG
54 | text: '1MO.6:?&D0=2:F.L=0TO999:V.32+L MOD95:N.:V.19;4;0;279;0;0;0;0;',
55 | mediaType: "image/png",
56 | checksum: "06577a813c4df4f59f0e2325e9fe5874b7106293"
57 | },
58 | {
59 | name: "RUNCHECK", // Regression test for program that didn't used to get run
60 | text: '0REM THIS SHOULD GET RUN\n1MO.6:P."MODE6":V.19;4;0;19,1,6;0;279;0;0;0;0',
61 | mediaType: "image/png",
62 | checksum: "b595b191a31cff941162438d1ce0135d71018a01"
63 | },
64 | {
65 | name: "YOUONLYRUNONCE", // Check that an explicit RUN suppresses an implicit one.
66 | text: '1PRINT"HELLO":!-512=&B000B\nRUN',
67 | mediaType: "image/png",
68 | checksum: "28222f638d2c0b97e7e03d0e54561ab7364bd445"
69 | },
70 | {
71 | name: "NOLINENOS", // Test no line numbers -> tokeniser.
72 | text: "P.\"HELLO\";\nV.279;0;0;0;0;32\nP.\"WORLD\"",
73 | mediaType: "image/png",
74 | checksum: "5c3db47017774d43ad27c9916af332d471e273e6"
75 | },
76 | {
77 | name: "TOKENS", // Test tokens -> tokeniser.
78 | text: "\xf1~\u0190\n\xef279;0;0;0;0;\n",
79 | mediaType: "image/png",
80 | checksum: "27760d3701f31e398df07429364ef0ebcc8b2434"
81 | },
82 | {
83 | name: "MENTIONS", // Test mention and hashtag removal
84 | text: "@BBCMicroBot @RhEolisM #bbcmicrobot 1V.279;0;0;0;0;12:PRINTCHR$141\"Hello\"'CHR$141\"Hello\"CHR$21\n",
85 | mediaType: "image/png",
86 | checksum: "10e6285dc55ec5ddab8470e8f038725db2d0ffbc"
87 | },
88 | {
89 | name: "OVERLONG", // Test overlong line doesn't crash the bot
90 | text: "0REM " + ("BBC".repeat(88)),
91 | mediaType: "text/plain",
92 | checksum: ""
93 | },
94 | {
95 | name: "TOKENISE_LONG", // Test tokenisation handles a long input
96 | text: "0PRINT" + (":PRINT".repeat(125)),
97 | mediaType: "image/gif",
98 | checksum: "930d0ab94529922c030d9797f2ec9a9c735a319c"
99 | },
100 | {name: null, text: null}
101 | ]
102 |
103 | this.queue = [];
104 | while (tests.length) {
105 | var test = tests.pop();
106 | var toot = {
107 | 'account' : {'url':'@test@localhost'},
108 | 'user' : {'screen_name':""}, //
109 | 'text' : test.text,
110 | 'id' : test.name,
111 | 'bbcmicrobot_has_audio' : (test.hasAudio == true),
112 | 'bbcmicrobot_checksum' : test.checksum,
113 | 'bbcmicrobot_media_type' : test.mediaType,
114 | 'url' : "https://www.bbcmicrobot.com/test.html"
115 | };
116 | this.queue.push(toot);
117 | }
118 | }
119 |
120 | // Get the next section of timeline timeline
121 | Tests.prototype.update = async function () {}
122 |
123 | function exec(cmd) {
124 | const exec = require('child_process').exec;
125 | return new Promise((resolve, reject) => {
126 | exec(cmd, (error, stdout, stderr) => {
127 | if (error) {
128 | throw error;
129 | }
130 | resolve(stdout? stdout.trim() : stderr);
131 | });
132 | });
133 | }
134 |
135 | function videoReply(filename,mediaType,id,replyTo,tweet,checksum,hasAudio){
136 | console.log("checksum: "+checksum)
137 | if (tweet.bbcmicrobot_checksum != checksum) {
138 | throw new Error(id+' TEST - \u001b[31mFAILED\u001b[0m')
139 | }
140 | console.log("mediaType: "+mediaType)
141 | if (tweet.bbcmicrobot_media_type != mediaType) {
142 | throw new Error(id+' TEST - \u001b[31mFAILED\u001b[0m')
143 | }
144 | console.log("hasAudio: "+hasAudio)
145 | if (tweet.bbcmicrobot_has_audio != hasAudio) {
146 | throw new Error(id+' TEST - \u001b[31mFAILED\u001b[0m')
147 | }
148 | if (mediaType == 'image/gif') {
149 | exec('ffprobe -v 0 -select_streams a -show_streams '+filename).then(
150 | function(audioInfo) {
151 | var videoHasAudio = (audioInfo.length > 0);
152 | console.log("videoHasAudio: "+videoHasAudio);
153 | if (hasAudio != videoHasAudio) {
154 | throw new Error(id+' TEST - \u001b[31mFAILED\u001b[0m')
155 | }
156 | });
157 | }
158 | console.log(replyTo+' TEST - \u001b[32mOK\u001b[0m')
159 | }
160 |
161 | function noOutput(tweet) {
162 | // If the checksum is empty then we expect no output.
163 | if (tweet.bbcmicrobot_checksum == '') {
164 | console.log(tweet.id+' TEST - \u001b[32mOK\u001b[0m')
165 | } else {
166 | throw new Error(tweet.id+' TEST - \u001b[31mFAILED\u001b[0m')
167 | }
168 | }
169 |
170 | function block(tweet) {
171 | throw new Error(tweet.id+' TEST - \u001b[31mFAILED\u001b[0m')
172 | }
173 |
174 | module.exports = {
175 | Feed: Tests,
176 | videoReply: videoReply,
177 | noOutput: noOutput,
178 | block: block
179 | };
180 |
--------------------------------------------------------------------------------
/tools/bbcbasictokenise:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -CSAD
2 | use strict;
3 | use warnings;
4 |
5 | # Path to base2048 repo.
6 | # FIXME: Need to sort out not to need custom code in there for this to work
7 | # outside my machine!
8 | my ($base2048dir) = ($0 =~ m,(.*/),);
9 | $base2048dir //= '';
10 | $base2048dir .= '../base2048';
11 |
12 | # To twitter these inclusive ranges count as 1 character (everything else as 2):
13 | # U+0000-U+10FF
14 | # U+2000-U+200D # various spaces
15 | # U+2010-U+201F # various punctuation
16 | # U+2032-U+2037 # various prime marks
17 | my %token = (
18 | 'AND' => "\x{580}",
19 | 'DIV' => "\x{281}",
20 | 'EOR' => "\x{E82}",
21 | 'MOD' => "\x{583}",
22 | 'OR' => "\x{184}",
23 | 'ERROR' => "\x{F85}",
24 | 'LINE' => "\x{186}",
25 | 'OFF' => "\x{287}",
26 | 'STEP' => "\x{388}",
27 | 'SPC' => "\x{F89}",
28 | 'TAB(' => "\x{38A}",
29 | 'ELSE' => "\x{18B}",
30 | 'THEN' => "\x{18C}",
31 | # 8D encodes a line number in GOTO/GOSUB
32 | 'OPENIN' => "\x{18E}",
33 | #'PTR' => "\x8F" # (right form)
34 |
35 | 'PAGE' => "\x{490}",
36 | '?PAGE=' => "?\x{490}=",
37 | '!PAGE=' => "!\x{490}=",
38 | '$PAGE=' => "\$\x{490}=",
39 | 'TIME' => "\x{191}",
40 | '?TIME=' => "?\x{191}=",
41 | '!TIME=' => "!\x{191}=",
42 | '$TIME=' => "\$\x{191}=",
43 | 'LOMEM' => "\x{1092}",
44 | '?LOMEM=' => "?\x{1092}=",
45 | '!LOMEM=' => "!\x{1092}=",
46 | '$LOMEM=' => "\$\x{1092}=",
47 | 'HIMEM' => "\x{493}",
48 | '?HIMEM=' => "?\x{493}=", # E.g. ?HIMEM=32 or P%?HIMEM=32
49 | '!HIMEM=' => "!\x{493}=", # E.g. !HIMEM=32 or P%!HIMEM=32
50 | '$HIMEM=' => "\$\x{493}=", # E.g. $HIMEM="HELLO"
51 | 'ABS' => "\x{294}",
52 | 'ACS' => "\x{195}",
53 | 'ADVAL' => "\x{196}",
54 | 'ASC' => "\x{297}",
55 | 'ASN' => "\x{298}",
56 | 'ATN' => "\x{199}",
57 | 'BGET' => "\x{19A}",
58 | 'COS' => "\x{19B}",
59 | 'COUNT' => "\x{19C}",
60 | 'DEG' => "\x{19D}",
61 | 'ERL' => "\x{19E}",
62 | 'ERR' => "\x{19F}",
63 |
64 | 'EVAL' => "\x{3A0}",
65 | 'EXP' => "\xA1",
66 | 'EXT' => "\xA2",
67 | 'FALSE' => "\x{1A3}",
68 | 'FN' => "\xA4",
69 | 'GET' => "\xA5",
70 | 'INKEY' => "\xA6",
71 | 'INSTR(' => "\xA7",
72 | 'INT' => "\xA8",
73 | 'LEN' => "\xA9",
74 | 'LN' => "\xAA",
75 | 'LOG' => "\xAB",
76 | 'NOT' => "\xAC",
77 | 'OPENUP' => "\xAD",
78 | 'OPENOUT' => "\xAE",
79 | 'PI' => "\xAF",
80 |
81 | 'POINT(' => "\xB0",
82 | 'POS' => "\xB1",
83 | 'RAD' => "\xB2",
84 | 'RND' => "\xB3",
85 | 'SGN' => "\xB4",
86 | 'SIN' => "\xB5",
87 | 'SQR' => "\xB6",
88 | 'TAN' => "\xB7",
89 | 'TO' => "\xB8",
90 | 'TRUE' => "\xB9",
91 | 'USR' => "\xBA",
92 | 'VAL' => "\xBB",
93 | 'VPOS' => "\xBC",
94 | 'CHR$' => "\xBD",
95 | 'GET$' => "\xBE",
96 | 'INKEY$' => "\xBF",
97 |
98 | 'LEFT$(' => "\xC0",
99 | 'MID$(' => "\xC1",
100 | 'RIGHT$(' => "\xC2",
101 | 'STR$' => "\xC3",
102 | 'STRING$(' => "\xC4",
103 | 'EOF' => "\xC5",
104 | 'AUTO' => "\xC6",
105 | 'DELETE' => "\xC7",
106 | 'LOAD' => "\xC8",
107 | 'LIST' => "\xC9",
108 | 'NEW' => "\xCA",
109 | 'OLD' => "\xCB",
110 | 'RENUMBER' => "\xCC",
111 | 'SAVE' => "\xCD",
112 | # CE unused by BASIC II (EDIT in later versions)
113 | #'PTR' => "\xCF" # (left form)
114 |
115 | 'PAGE=' => "\xD0=",
116 | 'PA.=' => "\xD0=",
117 | 'TIME=' => "\xD1=",
118 | 'TI.=' => "\xD1=",
119 | 'LOMEM=' => "\xD2=",
120 | 'HIMEM=' => "\xD3=",
121 | 'H.=' => "\xD3=",
122 | 'SOUND' => "\xD4",
123 | 'BPUT' => "\xD5",
124 | 'CALL' => "\xD6",
125 | 'CHAIN' => "\xD7",
126 | 'CLEAR' => "\xD8",
127 | 'CLOSE' => "\xD9",
128 | 'CLG' => "\x{3DA}",
129 | 'CLS' => "\xDB",
130 | 'DATA' => "\xDC",
131 | 'DEF' => "\xDD",
132 | 'DIM' => "\xDE",
133 | 'DRAW' => "\xDF",
134 |
135 | 'END' => "\xE0",
136 | 'ENDPROC' => "\xE1",
137 | 'ENVELOPE' => "\xE2",
138 | 'FOR' => "\x{1E3}",
139 | 'GOSUB' => "\x{1E4}",
140 | 'GOTO' => "\x{1E5}",
141 | 'GCOL' => "\x{1E6}",
142 | 'IF' => "\x{2E7}",
143 | 'INPUT' => "\xE8",
144 | 'LET' => "\xE9",
145 | 'LOCAL' => "\xEA",
146 | 'MODE' => "\xEB",
147 | 'MOVE' => "\xEC",
148 | 'NEXT' => "\xED",
149 | 'NEXT:NEXT' => "\xED,",
150 | 'NEXT:NEXT:NEXT' => "\xED,,",
151 | 'NEXT:NEXT:NEXT:NEXT' => "\xED,,,",
152 | 'N.:N.' => "\xED,",
153 | 'N.:N.:N.' => "\xED,,",
154 | 'N.:N.:N.:N.' => "\xED,,,",
155 | 'ON' => "\xEE",
156 | 'VDU' => "\xEF",
157 |
158 | 'PLOT' => "\xF0",
159 | 'PRINT' => "\x{10F1}",
160 | 'PROC' => "\x{4F2}",
161 | 'READ' => "\x{6F3}",
162 | 'REM' => "\xF4",
163 | 'REPEAT' => "\x{4F5}",
164 | 'REPORT' => "\xF6",
165 | 'RESTORE' => "\x{7F7}",
166 | 'RETURN' => "\xF8",
167 | 'RUN' => "\xF9",
168 | 'STOP' => "\xFA",
169 |
170 | 'COLOUR' => "\xFB",
171 | 'TRACE' => "\xFC",
172 | 'UNTIL' => "\xFD",
173 | 'WIDTH' => "\xFE",
174 | 'OSCLI' => "\xFF"
175 | );
176 |
177 | # Minimum abbreviations:
178 | my @abbrevs = (
179 | 'ABS' => 'AB.',
180 | 'ACS' => 'AC.',
181 | 'ADVAL' => 'AD.',
182 | 'AND' => 'A.',
183 | 'ASC' => 'AS.',
184 | 'ASN' => 'ASN',
185 | 'ATN' => 'AT.',
186 | 'AUTO' => 'AU.',
187 | 'BGET' => 'B.',
188 | 'BPUT' => 'BP.',
189 | 'CALL' => 'CA.',
190 | 'CHAIN' => 'CH.',
191 | 'CHR$' => 'CHR.',
192 | 'CLEAR' => 'CL.',
193 | 'CLG' => 'CLG',
194 | 'CLOSE' => 'CLO.',
195 | 'CLS' => 'CLS',
196 | 'COLOUR' => 'C.',
197 | 'COS' => 'COS',
198 | 'COUNT' => 'COU.',
199 | 'DATA' => 'D.',
200 | 'DEF' => 'DEF',
201 | 'DEG' => 'DE.',
202 | 'DELETE' => 'DEL.',
203 | 'DIM' => 'DIM',
204 | 'DIV' => 'DI.',
205 | 'DRAW' => 'DR.',
206 | 'ELSE' => 'EL.',
207 | 'END' => 'END',
208 | 'ENDPROC' => 'E.',
209 | 'ENVELOPE' => 'ENV.',
210 | 'EOF' => 'EO.',
211 | 'EOR' => 'EOR',
212 | 'ERL' => 'ER.',
213 | 'ERR' => 'ERR',
214 | 'ERROR' => 'ERR.',
215 | 'EVAL' => 'EV.',
216 | 'EXP' => 'EX.',
217 | 'EXT' => 'EXT',
218 | 'FALSE' => 'FA.',
219 | 'FN' => 'FN',
220 | 'FOR' => 'F.',
221 | 'GCOL' => 'GC.',
222 | 'GET$' => 'GE.',
223 | 'GET' => 'GET',
224 | 'GOSUB' => 'GOS.',
225 | 'GOTO' => 'G.',
226 | 'HIMEM' => 'H.',
227 | 'IF' => 'IF',
228 | 'INKEY' => 'INKEY',
229 | 'INKEY$' => 'INK.',
230 | 'INPUT' => 'I.',
231 | 'INSTR(' => 'INS.',
232 | 'INT' => 'INT',
233 | 'LEFT$(' => 'LE.',
234 | 'LEN' => 'LEN',
235 | 'LET' => 'LET',
236 | 'LINE' => 'LIN.',
237 | 'LIST' => 'L.',
238 | 'LN' => 'LN',
239 | 'LOAD' => 'LO.',
240 | 'LOCAL' => 'LOC.',
241 | 'LOG' => 'LOG',
242 | 'LOMEM' => 'LOM.',
243 | 'MID$(' => 'M.',
244 | 'MOD' => 'MOD',
245 | 'MODE' => 'MO.',
246 | 'MOVE' => 'MOV.',
247 | 'NEW' => 'NEW',
248 | 'NEXT' => 'N.',
249 | 'NOT' => 'NO.',
250 | 'OFF' => 'OF.',
251 | 'OLD' => 'O.',
252 | 'ON' => 'ON',
253 | 'OPENIN' => 'OP.',
254 | 'OPENOUT' => 'OPENO.',
255 | 'OPENUP' => 'OPENU.',
256 | 'OPT' => 'OPT',
257 | 'OR' => 'OR',
258 | 'OSCLI' => 'OS.',
259 | 'PAGE' => 'PA.',
260 | 'PI' => 'PI',
261 | 'PLOT' => 'PL.',
262 | 'POINT(' => 'PO.',
263 | 'POS' => 'POS',
264 | 'PRINT' => 'P.',
265 | 'PROC' => 'PRO.',
266 | 'PTR' => 'PT.',
267 | 'RAD' => 'RAD',
268 | 'READ' => 'REA.',
269 | 'REM' => 'REM',
270 | 'RENUMBER' => 'REN.',
271 | 'REPEAT' => 'REP.',
272 | 'REPORT' => 'REPO.',
273 | 'RESTORE' => 'RES.',
274 | 'RETURN' => 'R.',
275 | 'RIGHT$(' => 'RI.',
276 | 'RND' => 'RN.',
277 | 'RUN' => 'RU.',
278 | 'SAVE' => 'SA.',
279 | 'SGN' => 'SG.',
280 | 'SIN' => 'SI.',
281 | 'SOUND' => 'SO.',
282 | 'SPC' => 'SP.',
283 | 'SQR' => 'SQ.',
284 | 'STEP' => 'S.',
285 | 'STOP' => 'STO.',
286 | 'STR$' => 'STR.',
287 | 'STRING$(' => 'STRI.',
288 | 'TAB(' => 'TAB.',
289 | 'TAN' => 'T.',
290 | 'THEN' => 'TH.',
291 | 'TIME' => 'TI.',
292 | 'TO' => 'TO',
293 | 'TRACE' => 'TR.',
294 | 'TRUE' => 'TRU.',
295 | 'UNTIL' => 'U.',
296 | 'USR' => 'US.',
297 | 'VAL' => 'VA.',
298 | 'VDU' => 'V.',
299 | 'VPOS' => 'VP.',
300 | 'WIDTH' => 'W.',
301 | );
302 |
303 | sub expand_token {
304 | my $token = shift;
305 | for (my $i = 1; $i < @abbrevs; $i += 2) {
306 | if ($abbrevs[$i] eq $token) {
307 | return $abbrevs[$i - 1];
308 | }
309 | }
310 | return $token;
311 | }
312 |
313 | # Hash to allow undoing abbreviations so we can work with full tokens, which we
314 | # then tokenise, optimise, or re-abbreviate (but having ensured the shortest
315 | # possible abbreviation is used).
316 | my %abbrevs;
317 | for (my $i = 0; $i < @abbrevs; $i += 2) {
318 | my $token = $abbrevs[$i];
319 | my $abbrev = $abbrevs[$i + 1];
320 | if (!exists($token{$token})) {
321 | $token{$token} = $abbrev;
322 | }
323 | while (1) {
324 | if (exists($abbrevs{$abbrev})) {
325 | warn "Collision for '$abbrev': '$abbrevs{$abbrev}' vs '$token'\n";
326 | }
327 | $abbrevs{$abbrev} = $token;
328 | last if (length($abbrev) == length($token));
329 | # Remove the '.' then add another character and replace the '.'.
330 | $abbrev = substr($abbrev, 0, -1);
331 | $abbrev .= substr($token, length($abbrev), 1) . '.';
332 | }
333 | }
334 |
335 | my $decode;
336 | if (@ARGV > 0 && $ARGV[0] eq '--decode') {
337 | shift @ARGV;
338 | my @detoken = ();
339 | for my $k (sort keys %token) {
340 | my $t = $token{$k};
341 | if (length($t) == 2 && substr($t, -1) eq '=' && substr($k, -1) eq '=') {
342 | # print STDERR "trimming '$k' ";
343 | $t = substr($t, 0, -1);
344 | $k = substr($k, 0, -1);
345 | # print STDERR "to '$k'\n";
346 | }
347 | if (length($t) == 1) {
348 | $detoken[ord($t)] = $k;
349 | # printf STDERR "token &%02x = '%s'\n", ord($t), $k;
350 | if (ord($t)>=0x100) {
351 | $detoken[ord($t)&0xff] //= $k;
352 | }
353 | }
354 | }
355 | for (32 .. 126) {
356 | $detoken[$_] //= chr($_);
357 | }
358 | for (0 .. 0xff) {
359 | $detoken[$_] //= sprintf '[UNKNOWN TOKEN &%02x]', $_;
360 | }
361 | $_ = <>;
362 | if (s/^\x{1F5DC}//) {
363 | local $/ = undef;
364 | my $base2048 = $_ . <>;
365 | print "len = " , length($base2048) , "\n";
366 | $_ = `node \Q$base2048dir\E/src/decode.js \Q$base2048\E`;
367 | }
368 | while (defined $_) {
369 | my ($q, $c);
370 | for (my $i = 0; $i < length($_); ++$i) {
371 | my $p = $c;
372 | $c = substr($_, $i, 1);
373 | if ($c eq '"') {
374 | $q = !$q;
375 | next;
376 | }
377 | my $codepoint = ord($c);
378 | my $replacement;
379 | if ($q) {
380 | # FIXME: Decode VDU sequences encoded as Unicode in strings to
381 | # somehow?
382 | if ($codepoint < 32 || ($codepoint >= 127 && $codepoint < 160)) {
383 | # Replace non-printables with equivalent printables.
384 | $replacement = chr(0x100 + $codepoint);
385 | }
386 | } elsif ($codepoint >= 0x80 && $codepoint <= 0x10ff) {
387 | # Expand token
388 | $replacement = ' ' . $detoken[$codepoint & 0xff];
389 | } elsif ($codepoint >= ord('A') && $codepoint <= ord('Z')) {
390 | if (substr($_, $i + 1) =~ /([A-Z]*\.)/) {
391 | # Expand abbreviation.
392 | $c .= $1;
393 | $replacement = expand_token($c);
394 | }
395 | }
396 | if (defined($replacement)) {
397 | $_ = substr($_, 0, $i) . $replacement . substr($_, $i + length($c));
398 | $i += length($replacement) - length($c);
399 | next;
400 | }
401 | }
402 | print;
403 | $_ = <>;
404 | }
405 | exit 0;
406 | }
407 |
408 | my $seen_rem;
409 |
410 | sub token {
411 | my ($pre, $w, $k) = @_;
412 | my $q = 0;
413 | ++$q while $pre =~ /"/g;
414 | if ($q % 2 == 1) {
415 | return $w . $k;
416 | }
417 | if (substr($k, -1) eq '.' and exists $abbrevs{$k}) {
418 | $k = $abbrevs{$k};
419 | }
420 | if (!exists $token{$k}) {
421 | print STDERR "KEYWORD '$k'\n";
422 | return $w . $k;
423 | }
424 | if ($k eq 'REM') {
425 | $seen_rem = 1;
426 | }
427 | my $t = $token{$k};
428 | if ($t =~ /^[A-Z]/) {
429 | return $w . $t;
430 | }
431 | return $t;
432 | }
433 | my $T = join("|",map quotemeta, sort {length($b)<=>length($a) or $a cmp $b} (keys %token, keys %abbrevs));
434 |
435 | my $o = '';
436 |
437 | my $len = 0;
438 | while (<>) {
439 | if ($len > 0) {
440 | print "\n";
441 | $o .= "\n";
442 | ++$len;
443 | }
444 | chomp;
445 | # Leave already tokenised lines alone.
446 | # if (/[^\0-\x7f]/) {
447 | # goto leave_alone;
448 | #}
449 | #next if !$full && /^REM/;
450 | $seen_rem = 0;
451 | s/( *?)($T)/$seen_rem ? $1.$2 : token($`,$1,$2)/ge;
452 | my ($q, $c);
453 | for (my $i = 0; $i < length($_); ++$i) {
454 | my $p = $c;
455 | $c = substr($_, $i, 1);
456 | if ($c eq '"') {
457 | $q = !$q;
458 | next;
459 | }
460 | # Don't strip spaces from REMs - if they're in the code then their
461 | # contents is probably being peeked for data.
462 | last if ($c eq $token{'REM'});
463 | # Don't strip spaces from DATA.
464 | last if ($c eq $token{'DATA'});
465 | if (!$q) {
466 | if ($c eq ' ' and (!defined $p or $p =~ /^[^A-Za-z]$/)) {
467 | $_ = substr($_, 0, $i) . substr($_, $i + 1);
468 | --$i;
469 | }
470 | }
471 | }
472 | leave_alone:
473 | print;
474 | $o .= $_;
475 | print STDERR "Line length: ", length($_), " bytes\n";
476 | $len += length($_);
477 | }
478 | print STDERR "\nTotal length: $len bytes\n";
479 | if ($len > 281 || exists $ENV{FORCE_BASE2048}) {
480 | system 'node', "$base2048dir/src/index.js", $o;
481 | if ($len > 382) {
482 | print "Too long by ", $len - 382, " bytes (pre-BASE2048)\n";
483 | }
484 | }
485 |
486 | # BUG: O!2=A ORRND EL.F.R=0TO2 -> syntax error
487 | # and decoding doesn't insert spaces after tokens which need it
488 |
--------------------------------------------------------------------------------
/tweetdisk.js:
--------------------------------------------------------------------------------
1 | const request = require('request');
2 | const fs = require("fs");
3 | PNG = require("pngjs").PNG;
4 |
5 | function stegDecode(dataIn){
6 | const magicWord = 0x12345678;
7 | const version = 0x00000001;
8 |
9 | var image8 = new Uint8Array(dataIn);
10 | var data8 = new Uint8Array(900*900);
11 | var data32 = new Uint32Array(data8.buffer);
12 |
13 | let i = 0;
14 |
15 | for (let d = 0; d