Compare commits

..

No commits in common. "master" and "ansitex" have entirely different histories.

66 changed files with 9077 additions and 144 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea
.editorconfig
*.debug
dev/

View File

@ -1,21 +0,0 @@
image: docker:latest
stages:
- build
variables:
VERSION: 3.17c-${ARCH}
CACHETAG: build-${ARCH}
DOCKER_HOST: tcp://docker:2375
services:
- docker:dind
before_script:
- docker info
- docker version
- echo "$CI_JOB_TOKEN" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
- ls -alR .
include: .gitlab-docker-x86_64.yml
#include: .gitlab-docker-armv7l.yml

View File

@ -1,16 +0,0 @@
armv7l:build:
variables:
ARCH: armv7l
stage: build
image: docker:latest
script:
- if [ -f init ]; then chmod 500 init; fi
- ([ -z "$REFRESH" ] && docker pull ${CI_REGISTRY_IMAGE}:${CACHETAG}) || echo "true"
- docker build --cache-from ${CI_REGISTRY_IMAGE}:${CACHETAG} -t ${CI_REGISTRY_IMAGE}:${VERSION} -t ${CI_REGISTRY_IMAGE}:${CACHETAG} .
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
- docker push ${CI_REGISTRY_IMAGE}:${CACHETAG}
tags:
- docker
- armv7l
only:
- master

View File

@ -1,17 +0,0 @@
x86_64:build:
variables:
ARCH: x86_64
stage: build
image: docker:latest
script:
- if [ -f init ]; then chmod 500 init; fi
- ([ -z "$REFRESH" ] && docker pull ${CI_REGISTRY_IMAGE}:${CACHETAG}) || echo "true"
- docker build --cache-from ${CI_REGISTRY_IMAGE}:${CACHETAG} -t ${CI_REGISTRY_IMAGE}:${VERSION} -t ${CI_REGISTRY_IMAGE}:${CACHETAG} .
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
- docker push ${CI_REGISTRY_IMAGE}:${CACHETAG}
tags:
- docker
- x86_64
only:
- master

View File

@ -1,66 +0,0 @@
# NAME leenooks/sbbs
# VERSION 3.17c
FROM debian:stretch-slim
# Base utilities
RUN apt-get update \
&& apt-get install -yqq curl procps less zip unzip arj unrar-free lhasa arc zoo logrotate libmozjs185-1.0 cron \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Add in Leenooks' apt repository
RUN curl -s http://apt.leenooks.net/setup.sh | sh
# Add ZeroTier
RUN echo "deb http://download.zerotier.com/debian/stretch stretch main" > /etc/apt/sources.list.d/zerotier.list
# Leenooks Utils
RUN apt-get update \
&& apt-get install -yqq --allow-unauthenticated makenl zerotier-one \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Other Utilities
RUN echo "deb http://ftp.au.debian.org/debian/ stretch contrib" > /etc/apt/sources.list.d/contrib.list
RUN apt-get update \
&& apt-get install -yqq dosemu \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Build SBBS
RUN apt-get update \
&& apt-get install -yqq build-essential libnspr4-dev libncurses5-dev libmozjs185-dev cvs wget pkgconf \
&& mkdir /opt/sbbs && cd /tmp \
&& wget http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/install/GNUmakefile \
&& make RELEASE=1 USE_DOSEMU=1 NO_X=1 JSINCLUDE=/usr/include/js JSLIB=mozjs185 SBBSDIR=/opt/sbbs install \
&& rm -rf /opt/sbbs/3rdp /opt/sbbs/src \
&& mv /opt/sbbs/ctrl /opt/sbbs/ctrl.orig \
&& mkdir /opt/sbbs/nodes.orig && mv /opt/sbbs/node[1-4] /opt/sbbs/nodes.orig \
&& ln -sf nodes/node1 /opt/sbbs/ \
&& ln -sf nodes/node2 /opt/sbbs/ \
&& ln -sf nodes/node3 /opt/sbbs/ \
&& ln -sf nodes/node4 /opt/sbbs/ \
&& find /opt/sbbs -name CVS -type d -exec rm -rf {} + \
&& apt-get -y purge build-essential libnspr4-dev libncurses5-dev libmozjs185-dev cvs wget pkgconf \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /opt/sbbs
ENV SBBSCTRL=/opt/sbbs/ctrl
ENV SBBSEXEC=/opt/sbbs/exec
ENV PATH=$PATH:${SBBSEXEC}
COPY init /sbin/init
ENTRYPOINT [ "/sbin/init" ]
CMD ["sbbs"]
VOLUME [ "/var/lib/zerotier-one" ]
VOLUME [ "/opt/sbbs/data","/opt/sbbs/ctrl","/opt/sbbs/nodes","/opt/sbbs/fido" ]
# Set the default timezone for the container
RUN ln -sf /usr/share/zoneinfo/Australia/Melbourne /etc/localtime

45
ctrl/videotex.ini Normal file
View File

@ -0,0 +1,45 @@
; Videotex Options
; Our key used to sign frames we send to other systems
;gpg_key=0@videotex
[sqrl]
auth_url=https://sqrl.dege.au
auth_path=/sqrl/login
auth_post=/api/sqrl
; The prefix configurations need to be kept in sync with other nodes
; Frames with no specific owner are owned by this key
; [prefix:x] - applies to pages begining with x
; key= - the GPG key of the owner (for remote updates)
; user= - the local user that can edit these frames (via *04)
; logoans= - the service providers logo (for ANSI)
; logovtx= - the service providers logo (for VIEWDATA)
; code= - For Echomail, the echoarea tag
; last_page= - For Echomail, the last page tagged
[prefix]
key=0@videotex
logoans=G1swbRtbMTszMW1BG1szMm1OG1szNG1TG1szM21JG1swOzMwOzQ3bXRleA==
logovtx=AUECTgNTBEkHdGV4
user=1
; System frames are owned by this key
[prefix:9]
key=0@videotex
logoans=G1swbRtbMTszMW1BG1szMm1OG1szNG1TG1szM21JG1swOzMwOzQ3bXRleA==
logovtx=AUECTgNTBEkHdGV4
user=1
; ANSItex Help Pages
[prefix:516]
key=516@videotex
logoans=G1swbRtbMTszMW1BG1szMm1OG1szNG1TG1szM21JG1swOzMwOzQ3bXRleA==
logovtx=AUECTgNTBEkHdGV4
user=1
; Private Net
[prefix:10010]
user=1
logoans=UHJpdmF0ZU5ldA==
logovtx=UHJpdmF0ZU5ldA==

24
init
View File

@ -1,24 +0,0 @@
#!/bin/bash
set -e
NAME="Synchronet BBS"
export PATH=$PATH:${SBBSEXEC}
if [ ! -e "${SBBSCTRL}/sbbs.ini" ]; then
echo "* Installing SBBS ctrl files into ${SBBSCTRL}"
cp -a ${SBBSCTRL}.orig/* ${SBBSCTRL}
fi
if [ ! -d "${SBBSCTRL}/../nodes/node1" ]; then
echo "* Installing SBBS nodes files into ${SBBSCTRL}/../nodes"
cp -Ra ${SBBSCTRL}/../nodes.orig/* ${SBBSCTRL}/../nodes/
fi
if [ -x /usr/sbin/zerotier-one -a -n "${ENABLE_ZT}" ]; then
echo "* Starting ZeroTier"
mkdir /dev/net && mknod /dev/net/tun -m 666 c 10 200
/usr/sbin/zerotier-one -d
fi
exec "$@"

17
install.txt Normal file
View File

@ -0,0 +1,17 @@
26 May 2020
Installation nodes.
1. For a new installation - make sure you have logged in first and created your sysop user.
- No to creating guest account (or set it to a password so it isnt used - it asks questions during login about terminal configuration)
2. Setup files
- Replace answer.msg in text/ (or make a zero byte file)
- in mods/ ln -sf ansitex/main.js login.js
- in mods/ ln -sf ansitex/logon.js
- in mods/ ln -sf ansitex/main.js ansitex.js
- in mods/ baja ansitex/ansitex.src && mv ansitex/ansitex.bin . (NOT REQUIRED FROM 3.20a)
3 - Create Shell: SCFG->Command Shells->Ansitex (optionally limit to FLAGS, but requires (requirement string) ANSI)
- logon.js will force users to ansitex shell - to make a ini config setting for this
- Optionally System -> New User Values -> Shells ->Ansitex (NOT REQUIRED with login.js in place)
- Optionally Edit users and change their shell. (NOT REQUIRED with logon.js in place)
4 - Turn off sysop password required for sysop login
- SCFG -> Toggle Options -> Require Sys Pass During Login -> No

7
keys/genkey.txt Normal file
View File

@ -0,0 +1,7 @@
Key-Type: default
Key-Usage: encrypt,sign
Name-Real: Your Name
Name-Comment: Ansitex Page *???#
Name-Email: ???@videotex
Expire-Date: 0
%commit

82
load/control/echomail.js Normal file
View File

@ -0,0 +1,82 @@
/**
* This control renders echomail
*
* The system echomail prefix is *1, with echomail pages built from the following page number:
* zzzzEEpppp, where:
* + zzzz is the zone, zero padded, the zone identifies the message groups
* + EE is the echomail area, ie: the message areas
* + pppp is the message number, identified by the tag attached to the message header
*
* (Tags are added to the messages via an external process.)
*/
// All controls need a unique variable so that require() can know if the control has already been loaded
var CONTROL_ECHOMAIL = '1';
// Optional debug message so we can see that it is loaded
log(LOG_DEBUG,'+ Control ECHOMAIL loaded');
// A unique method name (same as the control name that is called as new method() on initialisation
function echomail(session,pagenum) {
log(LOG_DEBUG,' - Loading echomail page:'+pagenum);
// has this control completed
var complete = false;
var ready = false;
function init(session,pagenum) {
log(LOG_DEBUG,' - Echomail init('+pagenum+')');
ready = session.loadMessage(pagenum);
}
Object.defineProperty(this, 'getName', {
get: function () {
return 'ECHOMAIL';
}
});
Object.defineProperty(this, 'isComplete', {
get: function () {
return complete;
}
});
// Handle the keyboard responses as we receive them.
this.handle = function(read) {
log(LOG_DEBUG,'Control ECHOMAIL handle() start. ('+read+')');
switch(read) {
case KEY_DOWN:
session.page.scroll(0,1);
read = '';
break;
case KEY_UP:
session.page.scroll(0,-1);
read = '';
break;
}
session.render();
return read;
}
/**
* ready() is called after a control has been initialised, to determine if the control will take the input
* or if it is unable to do so
*
* If ready() returns:
* + false, the main programming will return ERR_ROUTE to the user,
* + true, the main programming will continue to load the frame, and then pass input to on the next loop handle()
*
* @returns {boolean}
*/
this.ready = function() {
log(LOG_DEBUG,'echomail:ready = '+JSON.stringify(ready));
return ready;
}
init.apply(this,arguments);
}

157
load/control/frameedit.js Normal file
View File

@ -0,0 +1,157 @@
load('ansiedit.js');
load('frame.js');
load('tree.js');
load('scrollbar.js');
load('event-timer.js');
load('graphic.js');
var CONTROL_FRAMEEDIT = '1';
function edit(session) {
log(LOG_DEBUG,'+ Control EDIT loaded');
var complete = false;
var inProperty = false;
function init(session) {
log(LOG_DEBUG,' - Edit init()');
}
Object.defineProperty(this,'getName', {
get: function() {
return 'Frame Edit';
}
});
Object.defineProperty(this,'isComplete', {
get: function() {
return complete;
}
});
log(LOG_DEBUG,' - Owner: '+JSON.stringify(fo.owner));
const frame = new Frame(1,1,console.screen_columns,console.screen_rows,BG_BLACK|LIGHTGRAY);
frame.gotoxy(1,1);
header = '\1n'+fo.pageownerlogo+' '.repeat(fo.settings.FRAME_HEADER-console.strlen(fo.pageownerlogo))+'\1n '+
'\1W\1H'+fo.page+' '.repeat(fo.settings.FRAME_PAGENUM-fo.page.length)+' '+
'\1G\1H'+' Edit';
frame.putmsg(header);
frame.open();
var editor = new ANSIEdit({
x: 1,
y: 2,
width: 80,
height: 23,
attr: WHITE,
//showPosition: true,
menuHeading: 'Frame Edit '+fo.page,
parentFrame: frame,
});
editor.open();
editor.menu.addItem('Properties',properties);
editor.menu.addItem('Exit',onexit);
editor.menu.addItem('Save & Exit',saveexit);
var x = new Graphic;
x.ANSI = fo.parse(base64_decode(fo.content));
log(LOG_DEBUG,' - Fields: '+JSON.stringify(fo.frame_fields));
const bin = x.BIN;
var o = 0; // offset into 'bin'
for (var yy = 0; yy < 22; yy++) {
for (var xx = 0; xx < 80; xx++) {
editor.putChar({
x : xx,
y : yy,
ch : bin.substr(o, 1),
attr : bin.substr(o + 1, 1).charCodeAt(0) || BG_BLACK
});
o = o + 2;
}
}
editor.cycle();
frame.cycle();
this.handle=function(read) {
if (! js.terminated) {
switch (ascii(read)) {
case 26:
if (inProperty) {
propFrame.close();
frame.top();
frame.cycle();
inProperty = false;
} else {
properties();
}
return '';
case 27:
if (inProperty) {
log(LOG_DEBUG, ' + FrameEdit properties(): ESC');
propFrame.close();
frame.top();
frame.cycle();
inProperty = false;
}
return '';
default:
if (inProperty) {
log(LOG_DEBUG, ' + FrameEdit properties(): read');
propFrame.putmsg(read);
propFrame.cycle();
} else {
editor.getcmd(read);
editor.cycle();
frame.cycle();
}
if (! complete)
return '';
}
}
editor.close();
frame.close();
fo.render();
return '';
}
function properties() {
inProperty = true;
log(LOG_DEBUG, '+ FrameEdit properties()');
frame.bottom();
propFrame = new Frame(1,2,40,14,BG_BLUE|WHITE,frame);
propFrame.gotoxy(1,1);
propFrame.putmsg('Properties!');
propFrame.open();
propFrame.cycle();
}
function save() {
fo.content = base64_encode(editor.exportAnsi().join(''));
fo.save();
}
function onexit() {
complete = true;
}
function saveexit() {
save();
onexit();
}
init.apply(this,arguments);
}

215
load/control/register.js Normal file
View File

@ -0,0 +1,215 @@
/**
* This handles user registration.
*
* The form must have the following fields:
* + USER The user's user id to login
* + EMAIL The users's email address - to receive tokens
* + FULLNAME The user's full name
* + PASS The users's preferred password
* + CITY The user's city
* + COUNTRY The user's country - 3 letter ISO code
* + PCODE THe user's postal code
*/
var CONTROL_REGISTER = '1';
var EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var cValChars='ACDEFHJKLMNPQRTUVWXY23456789!@$%&';
log(LOG_DEBUG,'+ Control REGISTER loaded');
function register(session) {
var code = '';
var complete = false;
var processed = false;
var ready = false;
function init(session) {
log(LOG_DEBUG,' - Register init()');
ready = true;
}
// Called before processing for a field
Object.defineProperty(this, 'getName', {
get: function () {
return 'Control-Registration';
}
});
Object.defineProperty(this, 'isComplete', {
get: function () {
return complete && processed;
}
});
this.handle = function(read) {
// Dont allow existing users to re-register
if (user.number) {
session.baselineSend('ALREADY_MEMBER',false);
return (read === '*') ? read : '';
}
log(LOG_DEBUG,'Control REGISTER handle() start. ('+read+')');
if (cf === undefined) {
log(LOG_DEBUG,' - CF not defined, returning');
return read;
}
log(LOG_DEBUG,'- Field '+cf.name+'('+JSON.stringify(cf)+')');
if ((cf.name === 'TOKEN') && (read === '#' || read === "\r")) {
if (cf.value === code) {
complete = true;
} else {
session.baselineSend('INVALID_CODE',false);
session.cursorOn(cf.c+cf.value.length,cf.r);
session.attr(cf.attribute);
read = '';
}
}
log(LOG_DEBUG,'- Field Value ['+cf.value+'] ('+code+')');
return read;
}
this.prefield = function() {
log(LOG_DEBUG,'- prefield: Field '+cf.name+'('+JSON.stringify(cf)+')');
// Make sure we got an email
if (cf.name === 'TOKEN') {
if (! code.length) {
log(LOG_DEBUG,' - BASELINE '+cf.name+'('+JSON.stringify(cf)+')');
session.baselineSend('TOKEN_EMAIL',false);
var email = session.fieldValue('EMAIL');
var uid = session.fieldValue('USER');
var name = session.fieldValue('FULLNAME');
log(LOG_DEBUG,' - VALIDATE EMAIL TO ('+JSON.stringify(system.matchuserdata(U_NETMAIL,email))+')');
// Validate Email hasnt been used
// Validate USER_ID hasnt been used
if ((email.indexOf('@') === -1) || ! EMAIL_REGEX.test(email) || (system.matchuserdata(U_NETMAIL,email) !== 0)) {
session.baselineSend('INVALID_EMAIL',false);
return;
}
if (! system.check_name(uid)) {
log(LOG_DEBUG,' - Cannot use user_id: ('+uid+')');
session.baselineSend('INVALID_UID',false);
return;
}
var msgbase = new MsgBase('mail');
for (var i=0;i<6;i++) {
code += cValChars.substr(parseInt(Math.random()*cValChars.length),1);
}
var hdrs = new Object();
hdrs.to = name;
hdrs.to_net_type = netaddr_type(email);
if (hdrs.to_net_type !== NET_NONE) {
hdrs.to_net_addr = email;
} else {
session.baselineSend('CANNOT_SEND_TOKEN',false);
return;
}
hdrs.from = system.name;
hdrs.from_net_addr = 'sysop@'+system.inet_addr;
hdrs.from_net_type = NET_INTERNET;
hdrs.subject = 'Registration TOKEN for '+system.name;
if (msgbase.open !== undefined && msgbase.open() === false) {
console.print("\r\n\1n\1h\1rERROR: \1y" + msgbase.last_error + "\1n \r\n");
console.pause();
msgbase.close();
bbs.hangup();
return;
}
var msg="CODE: " + code + "\r\n\r\n";
msg += 'Please use the above code to validate your login to '+system.name+'.';
//log(LOG_DEBUG,' - HDR:'+JSON.stringify(hdrs));
//log(LOG_DEBUG,' - MSG:'+JSON.stringify(msg));
if (! msgbase.save_msg(hdrs,msg)) {
console.print("\r\n\1n\1h\1rERROR: \1y" + msgbase.last_error + "\1n \r\n");
console.pause();
msgbase.close();
bbs.hangup();
return;
}
msgbase.close();
}
session.baselineSend('TOKEN_SENT',false);
log(LOG_DEBUG,'SENT EMAIL TOKEN ('+code+') ['+JSON.stringify(hdrs)+']');
}
}
this.process = function() {
log(LOG_DEBUG,'Creating user: ['+session.fieldValue('EMAIL')+'] ['+session.fieldValue('USER')+']');
try {
var newuser = system.new_user(session.fieldValue('USER'));
} catch (e) {
session.baselineSend('USER_EXISTS',false);
log(LOG_ERROR,"New user couldn't be created (user created while signing up)");
log(LOG_ERROR,JSON.stringify(e));
processed = true;
return this.isComplete;
}
if (typeof newuser === 'number') {
session.baselineSend('USER_CREATE_ERROR',false);
log(LOG_ERROR,"New user couldn't be created (error code "+newuser+")");
processed = true;
return this.isComplete;
}
newuser.security.password = '';
if (bbs.login(newuser.alias,null)) {
user.number = newuser.number;
user.security.password = session.fieldValue('PASS');
user.name = session.fieldValue('FULLNAME');
user.handle = session.fieldValue('USER');
user.location = session.fieldValue('CITY')+', '+session.fieldValue('COUNTRY');
user.zipcode = session.fieldValue('PCODE');
user.netmail = session.fieldValue('EMAIL');
user.comment = 'ANSItex registered user';
bbs.user_sync();
bbs.logon();
log(LOG_INFO,"Created user record #"+user.number+": "+user.alias);
action = ACTION_EXIT;
processed = true;
return this.isComplete;
} else {
session.baselineSend('LOGIN_ERROR',false);
log(LOG_INFO,"bbs.login() failed");
user.comment = 'Initial login failed!';
newuser.settings |= USER_DELETED;
delete newuser;
processed = true;
return this.isComplete;
}
}
// @todo Doesnt appear to be used
this.ready = function() {
log(LOG_DEBUG,'register:ready = '+JSON.stringify(ready));
return ready;
}
init.apply(this,arguments);
}

205
load/control/sqrllogin.js Normal file
View File

@ -0,0 +1,205 @@
/**
* This handles user registration.
*
* The form must have the following fields:
* + UID The user's user id to login
* + EMAIL The users's email address - to receive tokens
* + FULLNAME The user's full name
* + PASS The users's preferred password
* + CITY The user's city
* + COUNTRY The user's country - 3 letter ISO code
* + PCODE THe user's postal code
*/
var CONTROL_SQRL = '1';
require('http.js','HTTPRequest');
load('ansitex/load/qrcode-make.js');
load('frame.js');
log(LOG_DEBUG,'+ Control SQRL-LOGIN loaded');
// @todo This should move to handle - since we have to press 2 twice to get out.
function sqrllogin() {
var complete = false;
var cancel = false;
var sqrl = loadOptions('sqrl');
log(LOG_DEBUG,'OPTIONS: '+JSON.stringify(sqrl));
var http = new HTTPRequest();
http.SetupGet(sqrl.auth_path,undefined,sqrl.auth_url);
http.request_headers.push('Accept: application/json');
try {
http.SendRequest();
http.ReadResponse();
log(LOG_INFO,'SQRL: '+JSON.stringify(http.body));
var data = http.body
.split('')
.map(function(x) {return x.charCodeAt(0)});
var qr = qrcodegen.QrCode.encodeBinary(data,qrcodegen.QrCode.Ecc.LOW);
var subframe = new Frame((viewdata ? fo.settings.FRAME_WIDTH : fo.settings.FRAME_WIDTH-qr.size-2),2,(viewdata ? qr.size/2 : qr.size+2),22,BG_BLACK|LIGHTGRAY);
fo.qrcode(qr,subframe);
fo.baselineSend('CANCEL_MSG',false);
// Loop and see if the user has logged in
var nut = http.body.substr(http.body.indexOf('nut='),68);
} catch (err) {
log(LOG_INFO,'SQRL Error: '+err+' '+JSON.stringify(sqrl));
cancel = true;
}
// Called before processing for a field
Object.defineProperty(this, 'getName', {
get: function () {
return 'SQRL-LOGIN';
}
});
Object.defineProperty(this, 'isComplete', {
get: function () {
return complete;
}
});
// Nothing to do here
this.handle=function(read) {
log(LOG_DEBUG,'Control SQRL-LOGIN handle() start. ('+read+')');
if (read === '2') {
log(LOG_INFO,'SQRL: Cancelled with 2');
cancel = true;
} else {
log(LOG_DEBUG,'SQRL read ['+read+']');
try {
http = new HTTPRequest();
http.SetupGet(sqrl.auth_post+'?'+nut,undefined,sqrl.auth_url);
http.request_headers.push('Accept: application/json');
log(LOG_DEBUG,'Checking NUT in ['+nut+']');
http.SendRequest();
http.ReadResponse();
switch (http.response_code) {
case 404:
log(LOG_DEBUG,'- NUT not Authorised yet.');
break;
case 200:
var result = JSON.parse(http.body);
if (result.isReady) {
log(LOG_INFO,'NUT: '+result.msg);
log(LOG_INFO,'NEXT: '+result.nextPage);
if (result.msg === 'SQRL authenticated') {
log(LOG_DEBUG,'Getting Authenticated User ['+result.nextPage+']');
http = new HTTPRequest();
http.SetupGet(result.nextPage,undefined,'');
http.request_headers.push('Accept: application/json');
http.SendRequest();
http.ReadResponse();
log(LOG_DEBUG,'Getting Authenticated User Response ['+http.response_code+']');
if (http.response_code === 200) {
var sqrluser = http.body.substr(0,40);
var username = 'S'+sqrluser.substr(0,24)
log(LOG_DEBUG,'Getting Authenticated sqrluser ['+JSON.stringify(sqrluser)+']');
// Look through our user base for an existing user
var uid = system.matchuser(username);
log(LOG_DEBUG,'Getting Authenticated UID ['+JSON.stringify(uid)+']');
if (! uid) {
log(LOG_DEBUG,'New User ['+username+'] with pass ('+sqrluser+')');
var user = system.new_user(username);
log(LOG_DEBUG,'New User ['+JSON.stringify(user.number)+']');
user.name = username;
user.security.password = sqrluser;
user.handle = username.substr(0,8);
user.location = 'Earth';
user.zipcode = '000';
user.netmail = username+'@'+system.inet_addr;
user.comment = 'ANSItex registered user - with SQRL';
bbs.user_sync();
} else {
user = new User(uid);
}
log(LOG_DEBUG,'Getting Authenticated USER ['+JSON.stringify(user.number)+']');
// Existing user, we'll exit here
if (bbs.login(user.name,null,user.security.password)) {
log(LOG_DEBUG,' - User:'+JSON.stringify(user.number));
bbs.logon();
log(LOG_DEBUG,' - SEND TO EXIT:');
complete = true;
action = ACTION_EXIT;
if (typeof subframe === 'object')
subframe.close();
break;
} else {
log(LOG_ERROR,'- Login Failed? ');
cancel = true;
}
} else {
log(LOG_ERROR,'- Unhandled User Details: '+http.response_code);
cancel = true;
}
} else {
log(LOG_ERROR,'- Unhandled isReady msg: '+result.msg);
cancel = true;
}
complete = true;
if (typeof subframe === 'object')
subframe.close();
} else {
log(LOG_ERROR,'- Unhandled isReady: '+result.isReady);
cancel = true;
}
break;
default:
log(LOG_ERROR,'- Unhandled response code: '+http.response_code);
// We are done
cancel = true;
}
} catch (err) {
log(LOG_INFO,'SQRL Error: '+err+' '+JSON.stringify(sqrl));
cancel = true;
}
}
if (cancel) {
log(LOG_INFO,'SQRL: Processing CANCEL ['+read+'].');
complete = true;
if (typeof subframe === 'object')
subframe.close();
action = ACTION_GOTO;
next_page = FRAME_LOGIN;
}
return read;
}
}
this;

161
load/defs.js Normal file
View File

@ -0,0 +1,161 @@
require('smbdefs.js','MSG_DELETE');
require('sbbsdefs.js','SUB_FIDO');
load('string.js');
load('lz-string.js');
/**
* ANSItex definitions
*/
/* Our home on disk */
const ANSITEX_HOME = system.mods_dir+'ansitex';
/* Frames location */
const FRAMES_HOME = ANSITEX_HOME+'/text/';
/* Load frames from msgbase */
const FRAMES_MSG_BASE = 'vtx_data';
/* Load frames from files */
const FRAMES_MSG_FILES = true;
const FRAMES_EOF_MARKER = "\r\n=== EOF";
const PAGE_LENGTH = 4; // The size of our page tag as stored in the msgbase for echomail/netmail
const NUMERIC_REGEX = /^[0-9]*$/;
/* Unit of cost */
const FRAME_COSTUNIT = 'c';
/** ACTIONS **/
/* Exit the script */
const ACTION_EXIT = 99;
/* Reload the current frame */
const ACTION_RELOAD = 1;
/* Goto a specific frame */
const ACTION_GOTO = 2;
/* Goto previous frame */
const ACTION_BACKUP = 3;
/* Goto next frame */
const ACTION_NEXT = 4;
/* Terminate the session */
const ACTION_TERMINATE = 5;
/* Submit form contents */
const ACTION_SUBMITRF = 6;
/* Star command entry */
const ACTION_STAR = 7;
/* Edit a frame */
const ACTION_EDIT = 8;
/** MODES **/
/* Typing * command on baseline */
const MODE_BL = 1;
/* Field Input */
const MODE_FIELD = 2;
/* Asking if form should be submitted */
const MODE_SUBMITRF = 3;
/* Response frame not sent */
const MODE_RFNOTSENT = 4;
/* Response frame sent */
const MODE_RFSENT = 5;
/* Response frame error */
const MODE_RFERROR = 6;
/** FRAME TYPES **/
// @todo Change these to bits
/* Information Frame, requires no response after viewed */
const FRAME_TYPE_INFO = 0; //'i';
/* Terminate Frame, contents displayed and then carrier dropped */
const FRAME_TYPE_TERMINATE = 15; //'t';
/**
* Frame the calls an External Method
* Contents indicate the method to be called with arguments
*/
const FRAME_TYPE_EXTERNAL = 13; //'x';
/**
* Frame calls a door
* Contents indicate the name of the door
*/
const FRAME_TYPE_DOOR = 11; //'X';
/**
* Frame renders a BBS from sbbslist
* Contents can be:
* + preview(bbs) - to show the BBS preview display
* + telnet(bbs) - to telgate to the BBS
*/
const FRAME_TYPE_BBS = 'b';
/**
* Response frame, input fields are embedded in the frame and after input the
* response will be submitted to the Service Provider, or to a method
*/
const FRAME_TYPE_RESPONSE = 1; //'r';
/* Login frame, enables the user to authenticate to the system, or to a CUG */
const FRAME_TYPE_LOGIN = 12; //'l';
/* Mail template frames - mail templates will have the user's stats for the area passed to render() */
const FRAME_TYPE_MAIL_TEMPLATE = 'm';
/* Frame is a message */
const FRAME_TYPE_MESSAGE = 2; // 'M';
/* Disable *# going backwards for the following frames */
const FRAMES_NO_HISTORY = ['980a','98b','981a','982a','983a','998a'];
/* Frames prefixed with this are owned by the system */
const SYSTEM_OWNER = 9;
// @todo can we get this from the message base?
const SYSTEM_ZONE = 516;
/* Time to wait for a key press */
const INACTIVE_TIMEOUT = 100000;
/* Idle time for un-authenticated users */
const INACTIVE_NOLOGIN = 30000;
/* Idle time for authenticated users */
const INACTIVE_LOGIN = 5*60000;
/* Home Frame */
const FRAME_HOME = {frame: '1',index: 'a'};
/* Login Frame */
const FRAME_LOGIN = {frame: '98',index: 'a'};
/* Registration Frame */
const FRAME_REGISTER = {frame: '981',index: 'a'};
/* SQRL Login */
const FRAME_SQRL = {frame: '982',index: 'a'};
/* Login Failed */
const FRAME_LOGIN_FAILED = {frame: '983',index: 'a'};
/* Home page after authentication */
const FRAME_HOME_AUTH = {frame: '98',index: 'b'};
/* Home page for initial connection */
const FRAME_HOME_CONNECT = {frame: '980',index: 'a'};
const FRAME_SYSTEM_ERROR = {frame: '998',index: 'a'};
/* Attributes saved/loaded from files */
const FRAME_SAVE_ATTRS = [
'content', // raw source content of a frame (as stored in vtx/tex files)
'cost', // integer, frame cost
'date', // date, date frame created
'dynamic_fields', // array of fields // @todo deprecate and work these out when loading and parsing a frame
'frame', // Page ID,
'index', // Page index,
'input_fields', // array of fields // @todo deprecate and work these out when loading and parsing a frame
'isAccessible', // boolean // @todo deprecate and merge into 'state'
'isPublic', // boolean // @todo deprecate and merge into 'state'
'key', // array, representing our key actions (0-9)
'state', // integer, representing type, accessible and public status
'type', // frame type // @todo deprecate and merge into 'state'
'version', // frame version (1)
'window' // processed frame data
];
/* The page that has our echomail area reading template */
const MAIL_TEMPLATE_FRAME = {frame: '199',index: 'a'};
/* The page that has our echomail area summary template */
const MAIL_TEMPLATE_AREA_SUMMARY = {frame: '198',index: 'a'};
// The maximum size (length) of embedded dynamic fields in frames
const DYNAMIC_FIELD_SIZE_MAX = 50;
/** ESCAPE CODES **/
const ESC = '\x1b';
const FIELD_PASSWORD_MASK = '*';
const FIELD_TEXT = 't';
const FIELD_PASSWORD = 'p';

533
load/funcs.js Normal file
View File

@ -0,0 +1,533 @@
require('ansitex/load/defs.js','ANSITEX_HOME'); // ANSITEX definitions
// Array of page owners
pageowners = [];
// String repeat.
if (!String.prototype.repeat) {
String.prototype.repeat = function(count) {
'use strict';
if (this === null) {
throw new TypeError('can\'t convert ' + this + ' to object');
}
var str = '' + this;
count = +count;
if (count !== count) {
count = 0;
}
if (count < 0) {
throw new RangeError('repeat count must be non-negative: '+count);
}
if (count === Infinity) {
throw new RangeError('repeat count must be less than infinity');
}
count = Math.floor(count);
if (str.length === 0 || count === 0) {
return '';
}
// Ensuring count is a 31-bit integer allows us to heavily optimize the
// main part. But anyway, most current (August 2014) browsers can't handle
// strings 1 << 28 chars or longer, so:
if (str.length * count >= 1 << 28) {
throw new RangeError('repeat count must not overflow maximum string size');
}
var rpt = '';
for (;;) {
if ((count & 1) === 1) {
rpt += str;
}
count >>>= 1;
if (count === 0) {
break;
}
str += str;
}
return rpt;
};
}
/**
* Convert ANSI into BIN for loading into a Frame
*
* @param ansi
*/
function ans2bin(ansi,frame) {
var x = new Graphic;
x.ANSI = ansi;
var o = 0; // offset into 'bin'
for (var yy = 0; yy < 22; yy++) {
for (var xx = 0; xx < 80; xx++) {
frame.setData(
xx,
yy,
x.BIN.substr(o,1),
x.BIN.substr(o+1,1).charCodeAt(0) || BG_BLACK
);
o = o+2;
}
}
}
/**
* Dynamic Field Processing
*
* @param field field we want to get a value for
* @param length length to fill this field
* @param pad the padding character if field is less than length
* @param context a context value to determine the value from
* @returns {string|*|null}
* @note bbs.atcodes() cannot process modifiers, so this function is a replacement.
*/
function atcode(field,length,pad,context) {
'use strict';
pad = pad ? pad : ' ';
var result = '';
var args = [];
if (field.search(/:/)) {
args = field.split(':');
field = args.shift();
}
//log(LOG_DEBUG,'Field:'+field,'Args:'+JSON.stringify(args));
switch(field) {
// Get the ECHOAREA FTN AREA_TAG
case 'msg_area_areatag':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = context.msgbase.cfg.area_tag;
break;
// Get the ECHOAREA Description
case 'msg_area_desc':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = context.msgbase.cfg.description;
break;
// Oldest message in msgarea
// Our oldest message, is the first message with a tag from the headers
case 'msg_area_msgoldest_date':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.list_tagged[0];
result = x ? x.date : '';
break;
case 'msg_area_msgoldest_page':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.list_tagged[0];
return x ? context.getMessagePage(x.number) : null;
// Newest message in msgarea
// Our newest message, is the last message with a tag from the headers
case 'msg_area_msgnewest_date':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.list_tagged[context.list_tagged.length-1];
result = x ? x.date : '';
break;
case 'msg_area_msgnewest_page':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.list_tagged[context.list_tagged.length-1];
return x ? context.getMessagePage(x.number) : null;
// First unread message
case 'msg_area_msgunread_date':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.newMsgs();
result = x.length ? x.shift().date : '';
break;
case 'msg_area_msgunread_page':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.newMsgs();
return x.length ? context.getMessagePage(x.shift().number) : null;
// First unread message to me
case 'msg_area_msgotome_date':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.newMsgsToMe();
result = x.length > 1 ? x[1].date : '';
break;
case 'msg_area_msgtome_page':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
var x = context.newMsgsToMe();
return x.length > 1 ? context.getMessagePage(x[1].number) : null;
// Count of unread messages
case 'msg_area_new':
if (args.length === 1) {
context = new MsgArea();
context.code = args[0];
}
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = ''+context.newMsgs().length;
break;
// Count of unread messages to me
case 'msg_area_newtome':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = ''+(context.newMsgsToMe().length > 1 ? context.newMsgsToMe().length-1 : 0);
break;
// Is this message area in my new scan list
case 'msg_area_newscan':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = (context.getUserStats().scan_ptr & SCAN_CFG_TOYOU) ? 'YES' : 'NO';
break;
// Is this message area in my new scan list
case 'msg_area_pending':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = ''+context.list_untagged.length;
break;
// Get the ECHOAREA Total Number of Messages
case 'msg_area_total':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = ''+context.msgbase.total_msgs;
break;
// Get the ECHOAREA Group Name
case 'msg_grp_name':
if (typeof context !== 'object') {
log(LOG_ERROR,'Unable to render ['+field+'], no context provided');
break;
}
result = context.zone_name;
break;
case 'nodeid':
result = getNodeID();
break;
default:
result = (typeof bbs === 'undefined') ? '*'.repeat(Math.abs(length)) : bbs.atcode(field+(args.length ? ':'+args : ''));
}
if ((result === null) || (typeof result === 'undefined'))
result = '';
length = length ? length : result.length;
//log(LOG_DEBUG,' - result length ['+result.length+'] desired ('+length+')');
if (result.length < Math.abs(length))
result = (length < 0) ? padright(result,Math.abs(length),pad) : padleft(result,length,pad);
else if (result.length > Math.abs(length))
result = result.substr(0,Math.abs(length));
log(LOG_DEBUG,'* ATCODE ['+field+'] ('+length+'|"'+pad+'") returns ['+result+']');
return result;
}
/**
* Find a message base by code
*
* @param code
* @returns {number | string|boolean}
* @deprecated Can this move to the msgbases.js?
*/
function findMsgBase(code) {
if (! code)
code = "vtx_data";
for (var s in msg_area.sub) {
var sub = msg_area.sub[s];
writeln('sub:'+sub.code);
if (sub.code.substr(-code.length).toLowerCase() === code)
return sub;
}
return false;
}
/**
* Return an argument from argv, or an error if it doesnt exit
*
* @param key
* @param error
* @param abort
*/
function getArg(key,error,abort) {
index = argv.indexOf(key);
if ((index !== -1) && (! (argv[index+1] === undefined || argv[index+1].match(/^-/)))) {
return argv[index+1];
}
if (abort) {
log(LOG_ERROR,error);
exit(1);
}
}
/**
* Returns the current node number
*
* @returns {string}
*/
function getNodeID() {
var regex = new RegExp('^'+SYSTEM_ZONE+':');
var matches = [];
for each (var addr in system.fido_addr_list)
{
if (regex.test(addr)) {
addr = addr.replace(regex,'');
matches = addr.split('/',2);
break;
}
}
return (matches.length === 0) ? '-' : padright(matches[0],3,'0')+padright(matches[1],3,'0')+padright((typeof bbs === 'undefined') ? 0 : bbs.node_num,2,'0');
}
function getPageOwners() {
// Load the owner configuration into memory
if (! pageowners.length) {
var f = new File(ANSITEX_HOME+'/ctrl/videotex.ini');
if (f.open('r')) {
var logoans = f.iniGetValue('prefix','logoans');
var logovtx = f.iniGetValue('prefix','logovtx');
var users = f.iniGetValue('prefix','user');
//log(LOG_DEBUG,'+ pageOwner: users='+JSON.stringify(users));
pageowners.push({prefix: 0,logoans: logoans,logovtx: logovtx,user:users});
f.iniGetSections('prefix:').forEach(function (prefix) {
var p = parseInt(prefix.substr(7));
var logoans = f.iniGetValue(prefix,'logoans','');
var logovtx = f.iniGetValue(prefix,'logovtx','');
var users = f.iniGetValue(prefix,'user','');
//log(LOG_DEBUG,'+ pageOwner: users='+JSON.stringify(users));
pageowners.push({prefix: p,logoans: logoans,logovtx: logovtx,user:users});
});
} else {
log(LOG_DEBUG,'getPageOwners: Couldnt open videotex.ini? :'+JSON.stringify(f));
}
f.close();
// Sort the pageowners ascending
pageowners.sort(function(a,b) { return (a.prefix < b.prefix) ? 1 : ((b.prefix < a.prefix) ? -1 : 0); });
//log(LOG_DEBUG,'+ pageOwner: pageowners='+JSON.stringify(pageowners));
}
return pageowners;
}
function loadOptions(option) {
var f = new File(ANSITEX_HOME+'/ctrl/videotex.ini');
if (! f.open('r')) {
log(LOG_DEBUG,'loadOptions: Couldnt open videotex.ini? :'+JSON.stringify(f));
return undefined;
}
val = f.iniGetObject(option);
f.close();
return val;
}
function msgBaseImport(msgbase,page,text) {
var msgbase = new MsgBase(findMsgBase(msgbase).code);
log(LOG_DEBUG,'Sending ['+page+'] to message base ['+msgbase.cfg.code+']');
var hdr = { to:'All', from:'Videotex', subject:'Frame: '+page };
var body = '';
body += text+"\r\n";
body += "--- " + js.exec_file + " " + '1.0' + "\r\n";
return msgbase.save_msg(hdr, body);
}
// Right Pad a string with char c
function padright(n,width,c) {
c = c || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(c) + n;
}
// Left Pad a string with char c
function padleft(n,width,c) {
c = c || '0';
n = n + '';
return n.length >= width ? n : n+new Array(width - n.length + 1).join(c);
}
/**
* Return the frame as a string
*/
function pageStr(page) {
if (page.frame==null)
return null;
if (! page.index)
page.index = 'a';
return page.frame.toString()+page.index;
}
/**
* Read our videotex.ini configuration and determine who owns a page.
* If there is no prefix for the page, it is owned by the system '0'
*
* @param page
* @returns {undefined}
*/
function pageOwner(page) {
var BreakException = {};
var o = null;
try {
getPageOwners().forEach(function(owner) {
var p = owner.prefix.toString();
o = owner;
var re = new RegExp('^' + p, 'g');
if (page.toString().match(re)) {
//log(LOG_DEBUG,'= pageOwner: p='+p+',o: '+o);
throw BreakException;
}
});
} catch (e) {
if (e !== BreakException) throw e;
}
//log(LOG_DEBUG,'+ pageOwner: page='+page+', owner: '+JSON.stringify(o));
return o;
}
/**
* Can the user edit the frame
*
* @param page
* @param user
*/
function pageEditor(page) {
//log(LOG_DEBUG,'+ pageEditor: page='+page+', user #'+user.number);
var BreakException = {};
var pageditor = false;
try {
getPageOwners().forEach(function(owner) {
var p = owner.prefix.toString();
//log(LOG_DEBUG,' - pageEditor: '+JSON.stringify(owner));
frameusers = owner.user ? owner.user.toString().split(',') : [1];
log(LOG_DEBUG,' - pageEditor: p='+p+'('+p.length+') user ['+JSON.stringify(frameusers)+'] - :'+frameusers.indexOf(user.number.toString()));
var re = new RegExp('^' + p, 'g');
if (page.toString().match(re) && (frameusers.indexOf(user.number.toString()) !== -1)) {
pageditor = true;
throw BreakException;
}
});
} catch (e) {
if (e !== BreakException) throw e;
}
log(LOG_DEBUG,'+ pageEditor: page='+page+', editor: '+JSON.stringify(pageditor));
return pageditor;
}
/**
* This function returns a list of zones used by this system.
*/
function zones() {
var z = [];
var ftn = /([0-9]+):([0-9]+)\/([0-9]+)(\.([0-9]+))?/;
for(var g=0;g<system.fido_addr_list.length;g++) {
z.push(parseInt(system.fido_addr_list[g].match(ftn)[1]));
}
return z.sort(function(a,b) {
return (a>b);
});
}
this;

706
load/msgbases.js Normal file
View File

@ -0,0 +1,706 @@
'use strict';
require('ansitex/load/defs.js','ANSITEX_HOME');
// Our message bases that are enabled to render Videotex Messages
/*
* If a message area doesnt have:
* + a zone_id, then the group isnt Videotex enabled
* + a area_id, then the area isnt Videotex enabled
*/
function MsgAreas() {
this.areas = []; // Message areas
var zone_id;
var zone_name;
var ma;
// Load the message areas
for (var g in msg_area.grp_list) {
for (var a in msg_area.grp_list[g].sub_list) {
//writeln('group:'+g+' name:'+msg_area.grp_list[g].name+' FIDO:'+JSON.stringify(msg_area.grp_list[g].sub_list[a].fidonet_addr)+' INDEX:'+JSON.stringify(msg_area.grp_list[g].sub_list[a].ptridx));
ma = new MsgArea();
ma.group_id = g;
ma.sub_id = a;
if (msg_area.grp_list[g].name.indexOf(':') === -1) {
// If the sub is enabled for FTN, and the zone < 9999, then we'll us that if zone_id is undefined
if ((msg_area.grp_list[g].sub_list[a].settings & SUB_FIDO) && msg_area.grp_list[g].sub_list[a].fidonet_addr) {
var x = msg_area.grp_list[g].sub_list[a].fidonet_addr.split(/([0-9]+):/)[1];
if ((x > 0) && (x < 9999)) {
zone_id = x.padStart(4,'0');
zone_name = msg_area.grp_list[g].name;
}
}
} else {
zone_id = msg_area.grp_list[g].name.split(':')[0];
zone_name = msg_area.grp_list[g].name.split(':')[1];
}
if (zone_id) {
ma.zone_id = zone_id;
ma.zone_name = zone_name;
if (msg_area.grp_list[g].sub_list[a].name.indexOf(':') !== -1) {
var sublist = msg_area.grp_list[g].sub_list[a].name.split(':');
ma.area_id = sublist[0];
ma.area_name = sublist[1];
/*
writeln(' code:'+ma.code);
writeln(' fullname:'+ma.full_name);
writeln(' pageprefix:'+ma.page_prefix)
writeln();
*/
}
}
this.areas.push(ma);
}
}
/* List areas that we do and dont manage */
Object.defineProperty(this,'list',{
get: function() {
var areas = this.managed;
writeln('Areas that we ARE managing mail:'+areas.length);
for (var x in areas.sort(function(a,b) { return a.page_prefix > b.page_prefix; })) {
writeln(x+':'+areas[x].code+' prefix:'+areas[x].page_prefix);
}
areas = this.unmanaged;
writeln('Areas that we are NOT managing mail:'+areas.length);
for (var x in areas)
writeln(x+':'+areas[x].code);
}
});
/* List of message areas managed */
Object.defineProperty(this,'managed',{
get: function() {
return this.areas.filter(function(item) {
return item.managed;
});
}
});
/* List of message areas unmanaged */
Object.defineProperty(this,'unmanaged',{
get: function() {
return this.areas.filter(function(item) {
return ! item.managed;
})
}
});
/* Fetch a specific message area */
MsgAreas.prototype.getArea = function(area) {
if (area === undefined)
return undefined;
return this.areas.filter(function(x) {
// If the area is a 6 digits, then its a page prefix, otherwise its an area code name
if ((area.length === 6) && (NUMERIC_REGEX.test(area)))
return x.page_prefix === area;
else
return x.code === area;
}).pop();
}
// @todo review
MsgAreas.prototype.getMessage = function(page) {
var area = this.getArea(page);
log(LOG_DEBUG,' - msg:'+JSON.stringify(page.substr(7,4)));
return area ? area.getMessage(page.substr(7,4)) : undefined;
}
// @todo review
MsgAreas.prototype.getUserStats = function(page) {
var area = this.getArea(page);
return area ? msg_area.grp_list[area.msgbase.cfg.grp_number].sub_list[msg_area.sub[area.msgbase.cfg.code].index] : undefined;
}
}
function MsgArea() {
this.zone_id = undefined; // Zone this message area belongs to, eg: 0516
this.zone_name = undefined; // Zone Name, eg: VIDEOTEX
this.area_id = undefined; // Sub Area ID for this message area, eg: 01
this.area_name = undefined; // Sub Area Name for this message area, eg: CHAT
this.group_id = undefined; // SBBS Message Group ID
this.sub_id = undefined; // SBBS Message Sub ID
// MSG Base Code
Object.defineProperty(this,'code',{
get: function() {
//writeln('group_id:'+this.group_id+' sub_id:'+this.sub_id);
return msg_area.grp_list[this.group_id].sub_list[this.sub_id].code;
}
});
// Get all frames that are tagged
Object.defineProperty(this,'frames',{
get: function() {
var msgbase;
var regex = false;
if (this.code === FRAMES_MSG_BASE) {
msgbase = this.msgbase;
} else if (this.managed) {
msgbase = new MsgAreas().getArea(FRAMES_MSG_BASE).msgbase;
regex = this.page_prefix_regex;
} else
return undefined;
var frames = [];
try {
if (msgbase.open()) {
var headers = msgbase.get_all_msg_headers(false,false) || [];
for (var i in headers) {
if ((! (headers[i].attr&MSG_DELETE)) && (headers[i].from === 'SYSTEM') && ((! regex) || regex.test(headers[i].to)))
frames.push(headers[i]);
}
msgbase.close();
} else {
writeln('*** NOPE WONT OPEN msgbase:'+JSON.stringify(msgbase));
}
} catch (e) {
log(LOG_ERROR,this.code+' cannot be opened (frames):'+e.message);
return undefined;
}
return frames.sort(function(a,b) {
return (a.when_imported_time !== b.when_imported_time)
? a.when_imported_time > b.when_imported_time
: a.number > b.number;
});
}
});
// Get Area's full name
Object.defineProperty(this,'full_name',{
get: function() {
return this.zone_name+':'+this.area_name;
}
});
// @deprecated use frames instead
// Total tagged messages
Object.defineProperty(this,'list_tagged',{
get: function() {
if (this.tagged_list === undefined) {
this.tagged_list = [];
if (! this.headers)
return this.tagged_list;
for(var x in this.headers) {
if (this.headers[x].tags && (this.headers[x].tags.length === PAGE_LENGTH)) {
this.tagged_list.push(this.headers[x]);
write(); // @todo This is needed for this to work?
}
}
}
return this.tagged_list;
}
});
// Retrieve the last tagged frame
Object.defineProperty(this,'last_tagged_message',{
get: function() {
var last_tag = this.frames.sort(function(a,b) {
if (a.when_imported_time === b.when_imported_time)
return a.number > b.number
else
return (a.when_imported_time > b.when_imported_time);
}).pop();
if (last_tag === undefined)
return undefined;
var msgbase = new MsgAreas().getArea(FRAMES_MSG_BASE).msgbase;
try {
if (msgbase.open()) {
var body = JSON.parse(LZString.decompressFromBase64(msgbase.get_msg_body(last_tag.number)));
msgbase.close();
} else {
writeln('*** NOPE WONT OPEN msgbase:'+JSON.stringify(msgbase));
}
} catch (e) {
log(LOG_ERROR,this.code+' cannot be opened (last_tagged_message):'+e.message);
return undefined;
}
msgbase = this.msgbase;
try {
if (msgbase.open()) {
var index = msgbase.get_msg_header(body.id);
msgbase.close();
} else {
writeln('*** NOPE WONT OPEN msgbase:'+JSON.stringify(msgbase));
}
} catch (e) {
log(LOG_ERROR,this.code+' cannot be opened (last_tagged_message_base):'+e.message);
return undefined;
}
return index;
}
})
// Is this area defined for ansitex messages
Object.defineProperty(this,'managed',{
get: function() {
return (this.zone_id !== undefined) && (this.area_id !== undefined);
}
});
Object.defineProperty(this,'max_page',{
get: function() {
return parseInt('9'.repeat(PAGE_LENGTH),10);
}
});
Object.defineProperty(this,'msgbase',{
get: function() {
return new MsgBase(this.code);
}
});
// Get Next page number
Object.defineProperty(this,'page_next',{
get: function() {
if (! this.managed)
return undefined;
var next_tag = this.frames.pop();
if (next_tag !== undefined) {
next_tag = next_tag.tags;
if (next_tag.indexOf('1'+this.page_prefix) === 0)
next_tag = next_tag.slice(this.page_prefix.length+1);
next_tag = parseInt(next_tag,10)+1;
if (next_tag > this.max_page)
next_tag = 0;
} else {
next_tag = 0;
}
return (''+next_tag).padStart(4,'0');
},
/*
set: function(page) {
var f = new File(file_cfgname(system.mods_dir,'ansitex/ctrl/videotex.ini'));
if (! f.open('r+')) {
writeln('Unable to open ini file');
exit(2);
}
f.iniSetValue('prefix:'+this.page_prefix,PAGE_LAST_KEY,(''+page).padStart(4,'0'));
f.close();
}
*/
});
// Our page prefix for this msg area
Object.defineProperty(this,'page_prefix',{
get: function() {
return ''+this.zone_id+this.area_id;
},
});
// Return a REG to test if this frame is part of this msgbase
Object.defineProperty(this,'page_prefix_regex',{
get: function() {
return new RegExp( '^1'+this.page_prefix);
},
});
// Total Messages in a msgbase
Object.defineProperty(this,'total_msgs',{
get: function() {
if (! this.managed)
return undefined;
var msgbase = this.msgbase;
try {
if (msgbase.open()) {
var index = msgbase.get_index() || [];
msgbase.close();
} else {
writeln('*** NOPE WONT OPEN msgbase:'+JSON.stringify(msgbase));
}
} catch (e) {
log(LOG_ERROR,this.code+' cannot be opened (total_msgs):'+e.message);
return undefined;
}
return index.length;
}
});
// List untagged messages
Object.defineProperty(this,'untagged', {
get: function() {
if (! this.managed)
return undefined;
var msgbase = this.msgbase;
var last_tag = this.last_tagged_message;
var msgs = [];
try {
if (msgbase.open()) {
var headers = msgbase.get_all_msg_headers(false,false);
for (var x in headers) {
if (last_tag
&& ((headers[x].when_imported_time < last_tag.when_imported_time)
|| ((headers[x].when_imported_time === last_tag.when_imported_time) && (headers[x].number <= last_tag.number))))
continue;
msgs.push(headers[x]);
}
msgbase.close();
} else {
writeln('*** NOPE WONT OPEN msgbase:'+JSON.stringify(msgbase));
}
} catch (e) {
log(LOG_ERROR,this.code+' cannot be opened (untagged):'+e.message);
return undefined;
}
return msgs.sort(function(a,b) {
return (a.when_imported_time !== b.when_imported_time)
? a.when_imported_time > b.when_imported_time
: a.number > b.number;
});
}
});
// Get frame content
MsgArea.prototype.getContent = function(id) {
// @todo If this is for a echomail/netmail content, then we need to switch message bases
if (this.code !== FRAMES_MSG_BASE)
return undefined;
var msgbase = this.msgbase;
try {
if (msgbase.open()) {
var raw = msgbase.get_msg_body(false,id,false,false,true,true);
//log(LOG_DEBUG,'RAW:'+JSON.stringify(raw));
// Our messages are terminated with FRAMES_EOF_MARKER
var regex = new RegExp('^(.*)'+FRAMES_EOF_MARKER);
//log(LOG_DEBUG,'MARKER:'+regex.test(raw));
if (! regex.test(raw))
return undefined;
var regex = new RegExp(FRAMES_EOF_MARKER+'[.\\s\\S]*$');
var content = JSON.parse(LZString.decompressFromBase64(raw.replace(regex,'')));
//log(LOG_DEBUG,'CONTENT:'+JSON.stringify(content));
msgbase.close();
} else {
writeln('*** NOPE WONT OPEN msgbase:'+JSON.stringify(msgbase));
}
} catch (e) {
log(LOG_ERROR,this.code+' cannot be opened (frames):'+e.message);
return undefined;
}
return content;
};
// @todo review
/**
* Unread messages [1..]
* Array key 0 returns the last read message
*
* @returns {*[]}
*/
MsgArea.prototype.newMsgs = function() {
var msgs = [];
var stats = this.getUserStats();
//log(LOG_DEBUG,'Users last_read pointer: '+JSON.stringify(stats.last_read));
for(var x in this.list_tagged) {
// Advance past our last scan_ptr
if (this.list_tagged[x].number <= stats.last_read)
continue;
msgs.push(this.list_tagged[x]);
write(); // @todo This is needed for this to work?
}
return msgs;
}
// @todo review
/**
* New Messages for the logged in user
*/
MsgArea.prototype.newMsgsToMe = function() {
var msgs = [];
var stats = this.getUserStats();
var last = null;
//log(LOG_DEBUG,'Users scan_ptr pointer: '+JSON.stringify(stats.scan_ptr));
for(var x in this.list_tagged) {
// Advance past our last scan_ptr
if (this.list_tagged[x].number <= stats.scan_ptr) {
if ((this.list_tagged[x].to === user.name) || (this.list_tagged[x].to === user.alias))
last = x;
continue;
}
// Add our previous to me message
if (msgs.length === 0)
msgs.push(last !== null ? this.list_tagged[last] : []);
if ((this.list_tagged[x].to === user.name) || (this.list_tagged[x].to === user.alias))
msgs.push(this.list_tagged[x]);
write(); // @todo This is needed for this to work?
}
return msgs;
}
// @todo review
/**
* Get a specific message with a tag
*/
MsgArea.prototype.getMessage = function(page) {
var msg = undefined;
for(var x in this.list_tagged) {
if (this.list_tagged[x].tags === page) {
msg = this.list_tagged[x];
break;
}
write(); // @todo This is needed for this to work?
}
if (! msg)
return undefined;
if (! this.msgbase.open()) {
writeln(code+' cannot be opened (getMessage):'+this.msgbase.error);
return undefined;
}
msg.grp_number = this.msgbase.cfg.grp_number;
var cfg = this.msgbase.cfg;
msg.subnum = msg_area.grp_list[cfg.grp_number].sub_list.filter(function(x) { return x.number === cfg.number; }).pop().index;
msg.content = this.msgbase.get_msg_body(false,msg.number,false,false,true,true);
this.msgbase.close();
return msg;
}
// @todo review
/**
* Get a message page by pointer
*
* @param number
* @returns {string}
*/
MsgArea.prototype.getMessagePage = function(number) {
log(LOG_DEBUG,'Get Message Page with number ['+number+']');
var r;
for (var x in this.list_tagged) {
if (this.list_tagged[x].number === number) {
r = this.list_tagged[x];
break;
}
}
if (! r || r.tags === undefined)
return null;
return '1'+this.zone_id+this.area_id+r.tags;
}
// @todo review
MsgArea.prototype.getUserStats = function() {
return this.msgbase.cfg ? msg_area.grp_list[this.msgbase.cfg.grp_number].sub_list[msg_area.sub[this.msgbase.cfg.code].index] : [];
}
// @todo review
MsgArea.prototype.MessageNext = function(page) {
var x = null;
if (! page)
return undefined;
var msgid = page.substr(7,4);
for(var x in this.list_tagged) {
if (this.list_tagged[x].tags === msgid) {
break;
}
write(); // @todo This is needed for this to work?
}
//log(LOG_DEBUG,'- Next Message is:'+JSON.stringify(this.list_tagged[(parseInt(x)+1)])+', msgid:'+msgid+', page:'+page+', x:'+x);
/*
= Our next message is either
+ x+1 if x < this.list_tagged.length
+ x=0 if x == this.list_tagged.length (-1)
+ null if this.list_tagged.length == null; (thus no messages)
*/
return x === null ? null : this.list_tagged[(parseInt(x) === this.list_tagged.length-1) ? 0 : (parseInt(x)+1)];
}
// @todo review
MsgArea.prototype.MessagePrev = function(page) {
var prev = null;
var x = null;
if (! page)
return undefined;
var msgid = page.substr(7,4);
for(var x in this.list_tagged) {
if (this.list_tagged[x].tags === msgid) {
break;
} else {
prev = x;
}
write(); // @todo This is needed for this to work?
}
/*
= Our previous message is either
+ prev if a tag was found, unless
+ prev is null, in which case it is this.list_tagged.length -1
+ null if x is still null (thus no messages)
*/
// If prev is still null, then our last message must be the last one, unless x is null then there are no messages
return x === null ? null : this.list_tagged[(prev === null) ? this.list_tagged.length-1 : parseInt(prev)];
}
/**
* Tag messages with a frame number
* @note: May need to run jsexec with -m 32MB to overcome memory issues
*
* @returns {boolean}
*/
MsgArea.prototype.tag_msgs = function() {
var msgs = this.untagged;
var msgbase = new MsgAreas().getArea(FRAMES_MSG_BASE).msgbase;
var page_next = this.page_next;
writeln('* We have '+msgs.length+' messages to tag, starting at '+page_next);
// See if we need to tag something
if (! msgs.length)
return;
if (! msgbase.open()) {
writeln(code+' cannot be opened (tag_msgs):'+msgbase.error);
return false;
}
writeln('Starting at:'+page_next+' (max:'+this.max_page+')');
for (var x in msgs) {
//writeln('- '+msgs[x].when_imported_time+', #:'+msgs[x].number);
var frame = '1'+this.page_prefix+page_next;
var hdr = {
to: frame+'a',
from: 'SYSTEM',
tags: frame,
date: msgs[x].date,
subject: this.code+':'+msgs[x].id,
};
var page = {
id: msgs[x].number,
area_id: this.area_id,
group_id: this.group_id,
date: msgs[x].date,
msgid: msgs[x].id,
imported: msgs[x].when_imported_time,
};
var body = LZString.compressToBase64(JSON.stringify(page));
writeln('Tagging:'+page.id+' Tag:'+page_next+' MSGID:'+page.msgid);
if (! msgbase.save_msg(hdr,body)) {
writeln('! ERROR: Failed to tag '+msgs[x].number);
exit(1);
}
page_next++;
if (page_next > this.max_page)
page_next = 0;
page_next = (''+page_next).padStart(4,'0');
}
msgbase.close();
return true;
}
// @todo review
MsgArea.prototype.page = function(msgid) {
return '1'+this.page_prefix+msgid;
}
}

1418
load/page.js Normal file

File diff suppressed because it is too large Load Diff

995
load/qrcode-make.js Normal file
View File

@ -0,0 +1,995 @@
'use strict';
/*
* Module "qrcodegen", public members:
* - Class QrCode:
* - Function encodeText(str text, QrCode.Ecc ecl) -> QrCode
* - Function encodeBinary(list<byte> data, QrCode.Ecc ecl) -> QrCode
* - Function encodeSegments(list<QrSegment> segs, QrCode.Ecc ecl,
* int minVersion=1, int maxVersion=40, mask=-1, boostEcl=true) -> QrCode
* - Constants int MIN_VERSION, MAX_VERSION
* - Constructor QrCode(int version, QrCode.Ecc ecl, list<byte> dataCodewords, int mask)
* - Fields int version, size, mask
* - Field QrCode.Ecc errorCorrectionLevel
* - Method getModule(int x, int y) -> bool
* - Method drawCanvas(int scale, int border, HTMLCanvasElement canvas) -> void
* - Method toSvgString(int border) -> str
* - Enum Ecc:
* - Constants LOW, MEDIUM, QUARTILE, HIGH
* - Field int ordinal
* - Class QrSegment:
* - Function makeBytes(list<byte> data) -> QrSegment
* - Function makeNumeric(str data) -> QrSegment
* - Function makeAlphanumeric(str data) -> QrSegment
* - Function makeSegments(str text) -> list<QrSegment>
* - Function makeEci(int assignVal) -> QrSegment
* - Constructor QrSegment(QrSegment.Mode mode, int numChars, list<int> bitData)
* - Field QrSegment.Mode mode
* - Field int numChars
* - Method getData() -> list<int>
* - Constants RegExp NUMERIC_REGEX, ALPHANUMERIC_REGEX
* - Enum Mode:
* - Constants NUMERIC, ALPHANUMERIC, BYTE, KANJI, ECI
*/
var qrcodegen = new function() {
this.QrCode = function(version, errCorLvl, dataCodewords, mask) {
/*---- Constructor (low level) ----*/
// Check scalar arguments
if (version < MIN_VERSION || version > MAX_VERSION)
throw 'Version value out of range';
if (mask < -1 || mask > 7)
throw 'Mask value out of range';
if (!(errCorLvl instanceof Ecc))
throw 'QrCode.Ecc expected';
var size = version * 4 + 17;
// Initialize both grids to be size*size arrays of Boolean false
var row = [];
for (var i = 0; i < size; i++)
row.push(false);
var modules = []; // Initially all white
var isFunction = [];
for (var i = 0; i < size; i++) {
modules .push(row.slice());
isFunction.push(row.slice());
}
// Compute ECC, draw modules
drawFunctionPatterns();
var allCodewords = addEccAndInterleave(dataCodewords);
drawCodewords(allCodewords);
// Do masking
if (mask === -1) { // Automatically choose best mask
var minPenalty = Infinity;
for (var i = 0; i < 8; i++) {
applyMask(i);
drawFormatBits(i);
var penalty = getPenaltyScore();
if (penalty < minPenalty) {
mask = i;
minPenalty = penalty;
}
applyMask(i); // Undoes the mask due to XOR
}
}
if (mask < 0 || mask > 7)
throw 'Assertion error';
applyMask(mask); // Apply the final choice of mask
drawFormatBits(mask); // Overwrite old format bits
isFunction = null;
/*---- Read-only instance properties ----*/
// The version number of this QR Code, which is between 1 and 40 (inclusive).
// This determines the size of this barcode.
Object.defineProperty(this, 'version', {value:version});
// The width and height of this QR Code, measured in modules, between
// 21 and 177 (inclusive). This is equal to version * 4 + 17.
Object.defineProperty(this, 'size', {value:size});
// The error correction level used in this QR Code.
Object.defineProperty(this, 'errorCorrectionLevel', {value:errCorLvl});
// The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
// Even if a QR Code is created with automatic masking requested (mask = -1),
// the resulting object still has a mask value between 0 and 7.
Object.defineProperty(this, 'mask', {value:mask});
/*---- Accessor methods ----*/
// Returns the color of the module (pixel) at the given coordinates, which is false
// for white or true for black. The top left corner has the coordinates (x=0, y=0).
// If the given coordinates are out of bounds, then false (white) is returned.
this.getModule = function(x, y) {
return 0 <= x && x < size && 0 <= y && y < size && modules[y][x];
};
/*---- Public instance methods ----*/
// Draws this QR Code, with the given module scale and border modules, onto the given HTML
// canvas element. The canvas's width and height is resized to (this.size + border * 2) * scale.
// The drawn image is be purely black and white, and fully opaque.
// The scale must be a positive integer and the border must be a non-negative integer.
this.drawCanvas = function(scale, border, canvas) {
if (scale <= 0 || border < 0)
throw 'Value out of range';
var width = (size + border * 2) * scale;
canvas.width = width;
canvas.height = width;
var ctx = canvas.getContext('2d');
for (var y = -border; y < size + border; y++) {
for (var x = -border; x < size + border; x++) {
ctx.fillStyle = this.getModule(x, y) ? '#000000' : '#FFFFFF';
ctx.fillRect((x + border) * scale, (y + border) * scale, scale, scale);
}
}
};
// Returns a string of SVG code for an image depicting this QR Code, with the given number
// of border modules. The string always uses Unix newlines (\n), regardless of the platform.
this.toSvgString = function(border) {
if (border < 0)
throw 'Border must be non-negative';
var parts = [];
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
if (this.getModule(x, y))
parts.push('M' + (x + border) + ',' + (y + border) + 'h1v1h-1z');
}
}
return '<?xml version="1.0" encoding="UTF-8"?>\n' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 ' +
(size + border * 2) + ' ' + (size + border * 2) + '" stroke="none">\n' +
'\t<rect width="100%" height="100%" fill="#FFFFFF"/>\n' +
'\t<path d="' + parts.join(" ") + '" fill="#000000"/>\n' +
'</svg>\n';
};
/*---- Private helper methods for constructor: Drawing function modules ----*/
// Reads this object's version field, and draws and marks all function modules.
function drawFunctionPatterns() {
// Draw horizontal and vertical timing patterns
for (var i = 0; i < size; i++) {
setFunctionModule(6, i, i % 2 === 0);
setFunctionModule(i, 6, i % 2 === 0);
}
// Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
drawFinderPattern(3, 3);
drawFinderPattern(size - 4, 3);
drawFinderPattern(3, size - 4);
// Draw numerous alignment patterns
var alignPatPos = getAlignmentPatternPositions();
var numAlign = alignPatPos.length;
for (var i = 0; i < numAlign; i++) {
for (var j = 0; j < numAlign; j++) {
// Don't draw on the three finder corners
if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
}
}
// Draw configuration data
drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
drawVersion();
}
// Draws two copies of the format bits (with its own error correction code)
// based on the given mask and this object's error correction level field.
function drawFormatBits(mask) {
// Calculate error correction code and pack bits
var data = errCorLvl.formatBits << 3 | mask; // errCorrLvl is uint2, mask is uint3
var rem = data;
for (var i = 0; i < 10; i++)
rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
var bits = (data << 10 | rem) ^ 0x5412; // uint15
if (bits >>> 15 !== 0)
throw 'Assertion error';
// Draw first copy
for (var i = 0; i <= 5; i++)
setFunctionModule(8, i, getBit(bits, i));
setFunctionModule(8, 7, getBit(bits, 6));
setFunctionModule(8, 8, getBit(bits, 7));
setFunctionModule(7, 8, getBit(bits, 8));
for (var i = 9; i < 15; i++)
setFunctionModule(14 - i, 8, getBit(bits, i));
// Draw second copy
for (var i = 0; i < 8; i++)
setFunctionModule(size - 1 - i, 8, getBit(bits, i));
for (var i = 8; i < 15; i++)
setFunctionModule(8, size - 15 + i, getBit(bits, i));
setFunctionModule(8, size - 8, true); // Always black
}
// Draws two copies of the version bits (with its own error correction code),
// based on this object's version field, iff 7 <= version <= 40.
function drawVersion() {
if (version < 7)
return;
// Calculate error correction code and pack bits
var rem = version; // version is uint6, in the range [7, 40]
for (var i = 0; i < 12; i++)
rem = (rem << 1) ^ ((rem >>> 11) * 0x1F25);
var bits = version << 12 | rem; // uint18
if (bits >>> 18 !== 0)
throw 'Assertion error';
// Draw two copies
for (var i = 0; i < 18; i++) {
var bit = getBit(bits, i);
var a = size - 11 + i % 3;
var b = Math.floor(i / 3);
setFunctionModule(a, b, bit);
setFunctionModule(b, a, bit);
}
}
// Draws a 9*9 finder pattern including the border separator,
// with the center module at (x, y). Modules can be out of bounds.
function drawFinderPattern(x, y) {
for (var dy = -4; dy <= 4; dy++) {
for (var dx = -4; dx <= 4; dx++) {
var dist = Math.max(Math.abs(dx), Math.abs(dy)); // Chebyshev/infinity norm
var xx = x + dx, yy = y + dy;
if (0 <= xx && xx < size && 0 <= yy && yy < size)
setFunctionModule(xx, yy, dist !== 2 && dist !== 4);
}
}
}
// Draws a 5*5 alignment pattern, with the center module
// at (x, y). All modules must be in bounds.
function drawAlignmentPattern(x, y) {
for (var dy = -2; dy <= 2; dy++) {
for (var dx = -2; dx <= 2; dx++)
setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
}
}
// Sets the color of a module and marks it as a function module.
// Only used by the constructor. Coordinates must be in bounds.
function setFunctionModule(x, y, isBlack) {
modules[y][x] = isBlack;
isFunction[y][x] = true;
}
/*---- Private helper methods for constructor: Codewords and masking ----*/
// Returns a new byte string representing the given data with the appropriate error correction
// codewords appended to it, based on this object's version and error correction level.
function addEccAndInterleave(data) {
if (data.length !== QrCode.getNumDataCodewords(version, errCorLvl))
throw 'Invalid argument';
// Calculate parameter numbers
var numBlocks = QrCode.NUM_ERROR_CORRECTION_BLOCKS[errCorLvl.ordinal][version];
var blockEccLen = QrCode.ECC_CODEWORDS_PER_BLOCK [errCorLvl.ordinal][version];
var rawCodewords = Math.floor(QrCode.getNumRawDataModules(version) / 8);
var numShortBlocks = numBlocks - rawCodewords % numBlocks;
var shortBlockLen = Math.floor(rawCodewords / numBlocks);
// Split data into blocks and append ECC to each block
var blocks = [];
var rs = new ReedSolomonGenerator(blockEccLen);
for (var i = 0, k = 0; i < numBlocks; i++) {
var dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
k += dat.length;
var ecc = rs.getRemainder(dat);
if (i < numShortBlocks)
dat.push(0);
blocks.push(dat.concat(ecc));
}
// Interleave (not concatenate) the bytes from every block into a single sequence
var result = [];
for (var i = 0; i < blocks[0].length; i++) {
for (var j = 0; j < blocks.length; j++) {
// Skip the padding byte in short blocks
if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
result.push(blocks[j][i]);
}
}
if (result.length !== rawCodewords)
throw 'Assertion error';
return result;
}
// Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
// data area of this QR Code. Function modules need to be marked off before this is called.
function drawCodewords(data) {
if (data.length !== Math.floor(QrCode.getNumRawDataModules(version) / 8))
throw 'Invalid argument';
var i = 0; // Bit index into the data
// Do the funny zigzag scan
for (var right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair
if (right === 6)
right = 5;
for (var vert = 0; vert < size; vert++) { // Vertical counter
for (var j = 0; j < 2; j++) {
var x = right - j; // Actual x coordinate
var upward = ((right + 1) & 2) === 0;
var y = upward ? size - 1 - vert : vert; // Actual y coordinate
if (!isFunction[y][x] && i < data.length * 8) {
modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
i++;
}
// If this QR Code has any remainder bits (0 to 7), they were assigned as
// 0/false/white by the constructor and are left unchanged by this method
}
}
}
if (i !== data.length * 8)
throw 'Assertion error';
}
// XORs the codeword modules in this QR Code with the given mask pattern.
// The function modules must be marked and the codeword bits must be drawn
// before masking. Due to the arithmetic of XOR, calling applyMask() with
// the same mask value a second time will undo the mask. A final well-formed
// QR Code needs exactly one (not zero, two, etc.) mask applied.
function applyMask(mask) {
if (mask < 0 || mask > 7)
throw 'Mask value out of range';
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
var invert;
switch (mask) {
case 0: invert = (x + y) % 2 === 0; break;
case 1: invert = y % 2 === 0; break;
case 2: invert = x % 3 === 0; break;
case 3: invert = (x + y) % 3 === 0; break;
case 4: invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0; break;
case 5: invert = x * y % 2 + x * y % 3 === 0; break;
case 6: invert = (x * y % 2 + x * y % 3) % 2 === 0; break;
case 7: invert = ((x + y) % 2 + x * y % 3) % 2 === 0; break;
default: throw 'Assertion error';
}
if (!isFunction[y][x] && invert)
modules[y][x] = !modules[y][x];
}
}
}
// Calculates and returns the penalty score based on state of this QR Code's current modules.
// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
function getPenaltyScore() {
var result = 0;
// Adjacent modules in row having same color, and finder-like patterns
for (var y = 0; y < size; y++) {
var runHistory = [0,0,0,0,0,0,0];
var color = false;
var runX = 0;
for (var x = 0; x < size; x++) {
if (modules[y][x] === color) {
runX++;
if (runX === 5)
result += QrCode.PENALTY_N1;
else if (runX > 5)
result++;
} else {
QrCode.addRunToHistory(runX, runHistory);
if (!color && QrCode.hasFinderLikePattern(runHistory))
result += QrCode.PENALTY_N3;
color = modules[y][x];
runX = 1;
}
}
QrCode.addRunToHistory(runX, runHistory);
if (color)
QrCode.addRunToHistory(0, runHistory); // Dummy run of white
if (QrCode.hasFinderLikePattern(runHistory))
result += QrCode.PENALTY_N3;
}
// Adjacent modules in column having same color, and finder-like patterns
for (var x = 0; x < size; x++) {
var runHistory = [0,0,0,0,0,0,0];
var color = false;
var runY = 0;
for (var y = 0; y < size; y++) {
if (modules[y][x] === color) {
runY++;
if (runY === 5)
result += QrCode.PENALTY_N1;
else if (runY > 5)
result++;
} else {
QrCode.addRunToHistory(runY, runHistory);
if (!color && QrCode.hasFinderLikePattern(runHistory))
result += QrCode.PENALTY_N3;
color = modules[y][x];
runY = 1;
}
}
QrCode.addRunToHistory(runY, runHistory);
if (color)
QrCode.addRunToHistory(0, runHistory); // Dummy run of white
if (QrCode.hasFinderLikePattern(runHistory))
result += QrCode.PENALTY_N3;
}
// 2*2 blocks of modules having same color
for (var y = 0; y < size - 1; y++) {
for (var x = 0; x < size - 1; x++) {
var color = modules[y][x];
if (color === modules[y][x + 1] && color === modules[y + 1][x] && color === modules[y + 1][x + 1])
result += QrCode.PENALTY_N2;
}
}
// Balance of black and white modules
var black = 0;
modules.forEach(function(row) {
row.forEach(function(color) {
if (color)
black++;
});
});
var total = size * size; // Note that size is odd, so black/total !== 1/2
// Compute the smallest integer k >= 0 such that (45-5k)% <= black/total <= (55+5k)%
var k = Math.ceil(Math.abs(black * 20 - total * 10) / total) - 1;
result += k * QrCode.PENALTY_N4;
return result;
}
// Returns an ascending list of positions of alignment patterns for this version number.
// Each position is in the range [0,177), and are used on both the x and y axes.
// This could be implemented as lookup table of 40 variable-length lists of integers.
function getAlignmentPatternPositions() {
if (version === 1)
return [];
else {
var numAlign = Math.floor(version / 7) + 2;
var step = (version === 32) ? 26 :
Math.ceil((size - 13) / (numAlign*2 - 2)) * 2;
var result = [6];
for (var pos = size - 7; result.length < numAlign; pos -= step)
result.splice(1, 0, pos);
return result;
}
}
// Returns true iff the i'th bit of x is set to 1.
function getBit(x, i) {
return ((x >>> i) & 1) !== 0;
}
};
/*---- Static factory functions (high level) for QrCode ----*/
/*
* Returns a QR Code representing the given Unicode text string at the given error correction level.
* As a conservative upper bound, this function is guaranteed to succeed for strings that have 738 or fewer
* Unicode code points (not UTF-16 code units) if the low error correction level is used. The smallest possible
* QR Code version is automatically chosen for the output. The ECC level of the result may be higher than the
* ecl argument if it can be done without increasing the version.
*/
this.QrCode.encodeText = function(text, ecl) {
var segs = qrcodegen.QrSegment.makeSegments(text);
return this.encodeSegments(segs, ecl);
};
/*
* Returns a QR Code representing the given binary data at the given error correction level.
* This function always encodes using the binary segment mode, not any text mode. The maximum number of
* bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output.
* The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version.
*/
this.QrCode.encodeBinary = function(data, ecl) {
var seg = qrcodegen.QrSegment.makeBytes(data);
return this.encodeSegments([seg], ecl);
};
/*---- Static factory functions (mid level) for QrCode ----*/
/*
* Returns a QR Code representing the given segments with the given encoding parameters.
* The smallest possible QR Code version within the given range is automatically
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
* may be higher than the ecl argument if it can be done without increasing the
* version. The mask number is either between 0 to 7 (inclusive) to force that
* mask, or -1 to automatically choose an appropriate mask (which may be slow).
* This function allows the user to create a custom sequence of segments that switches
* between modes (such as alphanumeric and byte) to encode text in less space.
* This is a mid-level API; the high-level API is encodeText() and encodeBinary().
*/
this.QrCode.encodeSegments = function(segs, ecl, minVersion, maxVersion, mask, boostEcl) {
if (minVersion === undefined) minVersion = MIN_VERSION;
if (maxVersion === undefined) maxVersion = MAX_VERSION;
if (mask === undefined) mask = -1;
if (boostEcl === undefined) boostEcl = true;
if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7)
throw 'Invalid value';
// Find the minimal version number to use
var version, dataUsedBits;
for (version = minVersion; ; version++) {
var dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available
dataUsedBits = qrcodegen.QrSegment.getTotalBits(segs, version);
if (dataUsedBits <= dataCapacityBits)
break; // This version number is found to be suitable
if (version >= maxVersion) // All versions in the range could not fit the given data
throw 'Data too long';
}
// Increase the error correction level while the data still fits in the current version number
[this.Ecc.MEDIUM, this.Ecc.QUARTILE, this.Ecc.HIGH].forEach(function(newEcl) { // From low to high
if (boostEcl && dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8)
ecl = newEcl;
});
// Concatenate all segments to create the data bit string
var bb = new BitBuffer();
segs.forEach(function(seg) {
bb.appendBits(seg.mode.modeBits, 4);
bb.appendBits(seg.numChars, seg.mode.numCharCountBits(version));
seg.getData().forEach(function(bit) {
bb.push(bit);
});
});
if (bb.length !== dataUsedBits)
throw 'Assertion error';
// Add terminator and pad up to a byte if applicable
var dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8;
if (bb.length > dataCapacityBits)
throw 'Assertion error';
bb.appendBits(0, Math.min(4, dataCapacityBits - bb.length));
bb.appendBits(0, (8 - bb.length % 8) % 8);
if (bb.length % 8 !== 0)
throw 'Assertion error';
// Pad with alternating bytes until data capacity is reached
for (var padByte = 0xEC; bb.length < dataCapacityBits; padByte ^= 0xEC ^ 0x11)
bb.appendBits(padByte, 8);
// Pack bits into bytes in big endian
var dataCodewords = [];
while (dataCodewords.length * 8 < bb.length)
dataCodewords.push(0);
bb.forEach(function(bit, i) {
dataCodewords[i >>> 3] |= bit << (7 - (i & 7));
});
// Create the QR Code object
return new this(version, ecl, dataCodewords, mask);
};
/*---- Private static helper functions for QrCode ----*/
var QrCode = {}; // Private object to assign properties to. Not the same object as 'this.QrCode'.
// Returns the number of data bits that can be stored in a QR Code of the given version number, after
// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
QrCode.getNumRawDataModules = function(ver) {
if (ver < MIN_VERSION || ver > MAX_VERSION)
throw 'Version number out of range';
var result = (16 * ver + 128) * ver + 64;
if (ver >= 2) {
var numAlign = Math.floor(ver / 7) + 2;
result -= (25 * numAlign - 10) * numAlign - 55;
if (ver >= 7)
result -= 36;
}
return result;
};
// Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
// QR Code of the given version number and error correction level, with remainder bits discarded.
// This stateless pure function could be implemented as a (40*4)-cell lookup table.
QrCode.getNumDataCodewords = function(ver, ecl) {
return Math.floor(QrCode.getNumRawDataModules(ver) / 8) -
QrCode.ECC_CODEWORDS_PER_BLOCK [ecl.ordinal][ver] *
QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
};
// Inserts the given value to the front of the given array, which shifts over the
// existing values and deletes the last value. A helper function for getPenaltyScore().
QrCode.addRunToHistory = function(run, history) {
history.pop();
history.unshift(run);
};
// Tests whether the given run history has the pattern of ratio 1:1:3:1:1 in the middle, and
// surrounded by at least 4 on either or both ends. A helper function for getPenaltyScore().
// Must only be called immediately after a run of white modules has ended.
QrCode.hasFinderLikePattern = function(runHistory) {
var n = runHistory[1];
return n > 0 && runHistory[2] === n && runHistory[4] === n && runHistory[5] === n
&& runHistory[3] === n * 3 && Math.max(runHistory[0], runHistory[6]) >= n * 4;
};
/*---- Constants and tables for QrCode ----*/
var MIN_VERSION = 1; // The minimum version number supported in the QR Code Model 2 standard
var MAX_VERSION = 40; // The maximum version number supported in the QR Code Model 2 standard
Object.defineProperty(this.QrCode, 'MIN_VERSION', {value:MIN_VERSION});
Object.defineProperty(this.QrCode, 'MAX_VERSION', {value:MAX_VERSION});
// For use in getPenaltyScore(), when evaluating which mask is best.
QrCode.PENALTY_N1 = 3;
QrCode.PENALTY_N2 = 3;
QrCode.PENALTY_N3 = 40;
QrCode.PENALTY_N4 = 10;
QrCode.ECC_CODEWORDS_PER_BLOCK = [
// Version: (note that index 0 is for padding, and is set to an illegal value)
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
[null, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // Low
[null, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28], // Medium
[null, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // Quartile
[null, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // High
];
QrCode.NUM_ERROR_CORRECTION_BLOCKS = [
// Version: (note that index 0 is for padding, and is set to an illegal value)
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
[null, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25], // Low
[null, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49], // Medium
[null, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68], // Quartile
[null, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81], // High
];
/*---- Public helper enumeration ----*/
/*
* The error correction level in a QR Code symbol. Immutable.
*/
this.QrCode.Ecc = {
LOW : new Ecc(0, 1), // The QR Code can tolerate about 7% erroneous codewords
MEDIUM : new Ecc(1, 0), // The QR Code can tolerate about 15% erroneous codewords
QUARTILE: new Ecc(2, 3), // The QR Code can tolerate about 25% erroneous codewords
HIGH : new Ecc(3, 2), // The QR Code can tolerate about 30% erroneous codewords
};
// Private constructor.
function Ecc(ord, fb) {
// (Public) In the range 0 to 3 (unsigned 2-bit integer)
Object.defineProperty(this, 'ordinal', {value:ord});
// (Package-private) In the range 0 to 3 (unsigned 2-bit integer)
Object.defineProperty(this, 'formatBits', {value:fb});
}
/*---- Data segment class ----*/
/*
* A segment of character/binary/control data in a QR Code symbol.
* Instances of this class are immutable.
* The mid-level way to create a segment is to take the payload data
* and call a static factory function such as QrSegment.makeNumeric().
* The low-level way to create a segment is to custom-make the bit buffer
* and call the QrSegment() constructor with appropriate values.
* This segment class imposes no length restrictions, but QR Codes have restrictions.
* Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
* Any segment longer than this is meaningless for the purpose of generating QR Codes.
* This constructor creates a QR Code segment with the given attributes and data.
* The character count (numChars) must agree with the mode and the bit buffer length,
* but the constraint isn't checked. The given bit buffer is cloned and stored.
*/
this.QrSegment = function(mode, numChars, bitData) {
/*---- Constructor (low level) ----*/
if (numChars < 0 || !(mode instanceof Mode))
throw 'Invalid argument';
// The data bits of this segment. Accessed through getData().
bitData = bitData.slice(); // Make defensive copy
// The mode indicator of this segment.
Object.defineProperty(this, 'mode', {value:mode});
// The length of this segment's unencoded data. Measured in characters for
// numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
// Always zero or positive. Not the same as the data's bit length.
Object.defineProperty(this, 'numChars', {value:numChars});
// Returns a new copy of the data bits of this segment.
this.getData = function() {
return bitData.slice(); // Make defensive copy
};
};
/*---- Static factory functions (mid level) for QrSegment ----*/
/*
* Returns a segment representing the given binary data encoded in
* byte mode. All input byte arrays are acceptable. Any text string
* can be converted to UTF-8 bytes and encoded as a byte mode segment.
*/
this.QrSegment.makeBytes = function(data) {
var bb = new BitBuffer();
data.forEach(function(b) {
bb.appendBits(b, 8);
});
return new this(this.Mode.BYTE, data.length, bb);
};
/*
* Returns a segment representing the given string of decimal digits encoded in numeric mode.
*/
this.QrSegment.makeNumeric = function(digits) {
if (!this.NUMERIC_REGEX.test(digits))
throw 'String contains non-numeric characters';
var bb = new BitBuffer();
for (var i = 0; i < digits.length; ) { // Consume up to 3 digits per iteration
var n = Math.min(digits.length - i, 3);
bb.appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1);
i += n;
}
return new this(this.Mode.NUMERIC, digits.length, bb);
};
/*
* Returns a segment representing the given text string encoded in alphanumeric mode.
* The characters allowed are: 0 to 9, A to Z (uppercase only), space,
* dollar, percent, asterisk, plus, hyphen, period, slash, colon.
*/
this.QrSegment.makeAlphanumeric = function(text) {
if (!this.ALPHANUMERIC_REGEX.test(text))
throw 'String contains unencodable characters in alphanumeric mode';
var bb = new BitBuffer();
var i;
for (i = 0; i + 2 <= text.length; i += 2) { // Process groups of 2
var temp = QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
temp += QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
bb.appendBits(temp, 11);
}
if (i < text.length) // 1 character remaining
bb.appendBits(QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6);
return new this(this.Mode.ALPHANUMERIC, text.length, bb);
};
/*
* Returns a new mutable list of zero or more segments to represent the given Unicode text string.
* The result may use various segment modes and switch modes to optimize the length of the bit stream.
*/
this.QrSegment.makeSegments = function(text) {
// Select the most efficient segment encoding automatically
if (text === '')
return [];
else if (this.NUMERIC_REGEX.test(text))
return [this.makeNumeric(text)];
else if (this.ALPHANUMERIC_REGEX.test(text))
return [this.makeAlphanumeric(text)];
else
return [this.makeBytes(toUtf8ByteArray(text))];
};
/*
* Returns a segment representing an Extended Channel Interpretation
* (ECI) designator with the given assignment value.
*/
this.QrSegment.makeEci = function(assignVal) {
var bb = new BitBuffer();
if (assignVal < 0)
throw 'ECI assignment value out of range';
else if (assignVal < (1 << 7))
bb.appendBits(assignVal, 8);
else if (assignVal < (1 << 14)) {
bb.appendBits(2, 2);
bb.appendBits(assignVal, 14);
} else if (assignVal < 1000000) {
bb.appendBits(6, 3);
bb.appendBits(assignVal, 21);
} else
throw 'ECI assignment value out of range';
return new this(this.Mode.ECI, 0, bb);
};
// (Package-private) Calculates and returns the number of bits needed to encode the given segments at the
// given version. The result is infinity if a segment has too many characters to fit its length field.
this.QrSegment.getTotalBits = function(segs, version) {
var result = 0;
for (var i = 0; i < segs.length; i++) {
var seg = segs[i];
var ccbits = seg.mode.numCharCountBits(version);
if (seg.numChars >= (1 << ccbits))
return Infinity; // The segment's length doesn't fit the field's bit width
result += 4 + ccbits + seg.getData().length;
}
return result;
};
/*---- Constants for QrSegment ----*/
var QrSegment = {}; // Private object to assign properties to. Not the same object as 'this.QrSegment'.
// (Public) Describes precisely all strings that are encodable in numeric mode.
// To test whether a string s is encodable: var ok = NUMERIC_REGEX.test(s);
// A string is encodable iff each character is in the range 0 to 9.
this.QrSegment.NUMERIC_REGEX = /^[0-9]*$/;
// (Public) Describes precisely all strings that are encodable in alphanumeric mode.
// To test whether a string s is encodable: var ok = ALPHANUMERIC_REGEX.test(s);
// A string is encodable iff each character is in the following set: 0 to 9, A to Z
// (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
this.QrSegment.ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+.\/:-]*$/;
// (Private) The set of all legal characters in alphanumeric mode,
// where each character value maps to the index in the string.
QrSegment.ALPHANUMERIC_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
/*---- Public helper enumeration ----*/
/*
* Describes how a segment's data bits are interpreted. Immutable.
*/
this.QrSegment.Mode = { // Constants
NUMERIC : new Mode(0x1, [10, 12, 14]),
ALPHANUMERIC: new Mode(0x2, [ 9, 11, 13]),
BYTE : new Mode(0x4, [ 8, 16, 16]),
KANJI : new Mode(0x8, [ 8, 10, 12]),
ECI : new Mode(0x7, [ 0, 0, 0]),
};
// Private constructor.
function Mode(mode, ccbits) {
// (Package-private) The mode indicator bits, which is a uint4 value (range 0 to 15).
Object.defineProperty(this, 'modeBits', {value:mode});
// (Package-private) Returns the bit width of the character count field for a segment in
// this mode in a QR Code at the given version number. The result is in the range [0, 16].
this.numCharCountBits = function(ver) {
return ccbits[Math.floor((ver + 7) / 17)];
};
}
/*---- Private helper functions and classes ----*/
// Returns a new array of bytes representing the given string encoded in UTF-8.
function toUtf8ByteArray(str) {
str = encodeURI(str);
var result = [];
for (var i = 0; i < str.length; i++) {
if (str.charAt(i) !== '%')
result.push(str.charCodeAt(i));
else {
result.push(parseInt(str.substring(i + 1, i + 3), 16));
i += 2;
}
}
return result;
}
/*
* A private helper class that computes the Reed-Solomon error correction codewords for a sequence of
* data codewords at a given degree. Objects are immutable, and the state only depends on the degree.
* This class exists because each data block in a QR Code shares the same the divisor polynomial.
* This constructor creates a Reed-Solomon ECC generator for the given degree. This could be implemented
* as a lookup table over all possible parameter values, instead of as an algorithm.
*/
function ReedSolomonGenerator(degree) {
if (degree < 1 || degree > 255)
throw 'Degree out of range';
// Coefficients of the divisor polynomial, stored from highest to lowest power, excluding the leading term which
// is always 1. For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}.
var coefficients = [];
// Start with the monomial x^0
for (var i = 0; i < degree - 1; i++)
coefficients.push(0);
coefficients.push(1);
// Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
// drop the highest term, and store the rest of the coefficients in order of descending powers.
// Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
var root = 1;
for (var i = 0; i < degree; i++) {
// Multiply the current product by (x - r^i)
for (var j = 0; j < coefficients.length; j++) {
coefficients[j] = ReedSolomonGenerator.multiply(coefficients[j], root);
if (j + 1 < coefficients.length)
coefficients[j] ^= coefficients[j + 1];
}
root = ReedSolomonGenerator.multiply(root, 0x02);
}
// Computes and returns the Reed-Solomon error correction codewords for the given
// sequence of data codewords. The returned object is always a new byte array.
// This method does not alter this object's state (because it is immutable).
this.getRemainder = function(data) {
// Compute the remainder by performing polynomial division
var result = coefficients.map(function() { return 0; });
data.forEach(function(b) {
var factor = b ^ result.shift();
result.push(0);
coefficients.forEach(function(coef, i) {
result[i] ^= ReedSolomonGenerator.multiply(coef, factor);
});
});
return result;
};
}
// This static function returns the product of the two given field elements modulo GF(2^8/0x11D). The arguments and
// result are unsigned 8-bit integers. This could be implemented as a lookup table of 256*256 entries of uint8.
ReedSolomonGenerator.multiply = function(x, y) {
if (x >>> 8 !== 0 || y >>> 8 !== 0)
throw 'Byte out of range';
// Russian peasant multiplication
var z = 0;
for (var i = 7; i >= 0; i--) {
z = (z << 1) ^ ((z >>> 7) * 0x11D);
z ^= ((y >>> i) & 1) * x;
}
if (z >>> 8 !== 0)
throw 'Assertion error';
return z;
};
/*
* A private helper class that represents an appendable sequence of bits (0s and 1s).
* Mainly used by QrSegment. This constructor creates an empty bit buffer (length 0).
*/
function BitBuffer() {
Array.call(this);
// Appends the given number of low-order bits of the given value
// to this buffer. Requires 0 <= len <= 31 and 0 <= val < 2^len.
this.appendBits = function(val, len) {
if (len < 0 || len > 31 || val >>> len !== 0)
throw 'Value out of range';
for (var i = len - 1; i >= 0; i--) // Append bit by bit
this.push((val >>> i) & 1);
};
}
BitBuffer.prototype = Object.create(Array.prototype);
BitBuffer.prototype.constructor = BitBuffer;
};

187
load/session.js Normal file
View File

@ -0,0 +1,187 @@
/**
* This handles session specific aspects of each frame type, eg: sending to the baseline, clearing, accepting
* input, moving around the frame (and windows within one if any)
*
* @constructor
*/
function Session() {
'use strict';
// Our page object
this.page = undefined;
this.previous = undefined;
/* Frame type settings */
this.settings = {};
/**
* Enable pulling out submitted value by its name
*
* @param key
* @returns {null|string|*}
*/
this.fieldValue = function(key) {
for each (var k in this.page.input_fields) {
if (k.name === key)
return k.value;
}
return null;
}
this.get = function(page) {
if (!(page instanceof PageObject))
throw new Error('page must be a PageObject');
this.baselineSend('LOADING');
// Just in case our new page doesnt exist
this.previous = this.page;
this.page = new Page();
var result = this.page.get(page);
// If our new page doesnt exist, reset our page back to the current
if (! result)
this.page = this.previous;
this.previous = undefined;
this.baselineClear();
return result;
}
/**
* Load a message frame
*
* @param pagenum
*/
this.loadMessage = function(pagenum,ext) {
log(LOG_ERROR,'Loading Message :['+pagenum+']');
// Load our message
var ma = new MsgAreas()
var area = ma.getArea(pagenum);
var msg = ma.getMessage(pagenum);
var msg_header;
if (! msg)
return false;
// @todo Search 1zzzzEE..., 1zzzz...
if (! this.get(new PageObject(MAIL_TEMPLATE_FRAME),ext)) {
this.page = new Page();
log(LOG_ERROR,'Echomail template missing :['+JSON.stringify(MAIL_TEMPLATE_FRAME)+'] ?');
msg_header = 'TO: '+msg.to.substr(0,72)+"\r\n";
msg_header += 'FROM: '+msg.from.substr(0,72)+"\r\n";
msg_header += 'DATE: '+msg.date.substr(0,72)+"\r\n";
msg_header += 'SUBJECT: '+msg.subject.substr(0,72)+"\r\n";
} else {
// @todo change this to use atcode()
msg_header = this.page.raw.replace(/@(.*)@/g,
function (str, code, offset, s) {
var length = code.split(':')[1];
switch(code.split(':')[0]) {
case 'DATE': return msg.date.substr(0,length);
case 'FROM': return msg.from.substr(0,length);
case 'SUBJECT': return msg.subject.substr(0,length);
case 'TO': return msg.to.substr(0,length);
}
}
);
}
this.page.name = new PageObject(pagenum,'a');
this.page.owner = 1;
// @todo Keys should map to next/previous/send, etc as indicated in the template frame.
this.page.key = [this.page.name.frame.substr(0,7)+'1',null,null,null,null,null,null,null,null,null];
// @todo validate that FRAME_TYPE_MESSAGE is a message template
this.page.type = FRAME_TYPE_MESSAGE;
// @todo Take the cost from the template
this.page.cost = 5;
// @todo Take the key values from the template
this.page.__properties__.isAccessible = true;
this.page.__properties__.isPublic = true;
this.page.preload(msg_header+msg.content,'txt');
// Update the user's pointers
var stats = ma.getUserStats(this.page.name.frame);
// if this message is to the user, and the msg number > scan_ptr and it is the next message on the user's new mail list
var newmsgs = area.newMsgsToMe();
var next;
log(LOG_DEBUG,'User has: '+newmsgs.length-1+' msgs to read to ME');
if (newmsgs.length) {
next = newmsgs[1];
log(LOG_DEBUG,'- NEXT is: '+next.tags+', this is: '+msg.tags);
if (next && (next.tags === msg.tags)) {
log(LOG_DEBUG,'- Updating scan_ptr to: '+next.number);
stats.scan_ptr = next.number;
}
// Last message
next = newmsgs[0];
if (next !== undefined) {
log(LOG_DEBUG,'- LAST TO ME is: '+next.tags);
this.page.key[1] = area.page(next.tags);
}
// Next new message
next = newmsgs[2];
if (next !== undefined) {
log(LOG_DEBUG,'- NEXT TO ME is: '+next.tags);
this.page.key[2] = area.page(next.tags);
}
}
// if this message is the next message, update last_read
newmsgs = area.newMsgs();
log(LOG_DEBUG,'User has: '+newmsgs.length+' msgs to read');
if (newmsgs.length) {
next = newmsgs[0];
log(LOG_DEBUG,'- NEXT is: '+next.tags+', this is: '+msg.tags);
if (next.tags === msg.tags) {
log(LOG_DEBUG,'- Updating last_read to: '+next.number);
stats.last_read = next.number;
}
}
// Previous Message
x = area.MessagePrev(this.page.name.frame);
if (x)
this.page.key[4] = area.page(x.tags);
// Next Message
x = area.MessageNext(this.page.name.frame);
if (x)
this.page.key[6] = area.page(x.tags);
log(LOG_DEBUG,'Built frame: ['+this.page.name.frame+']['+this.page.name.index+'] ('+this.page.name.toString()+')');
return true;
}
// Render the page
this.render = function() {
this.gotoxy(0,0);
//console.clear(null,false);
write(so.page.display().join(''));
}
}
/**
* Return a system message for a index
*
* @param index
* @returns {string|*}
* @see SessionProtocol()
*/
Session.prototype.getMessage = function(index) {
eval('var msg = this.settings.'+index);
return msg;
}

596
load/session/ansitex.js Normal file
View File

@ -0,0 +1,596 @@
const SESSION_ANSITEX = (1<<1);
var CONTENT_EXT = 'ans';
var FRAME_WIDTH = 80;
var FRAME_HEIGHT = 22;
var FRAME_PROVIDER_LENGTH = 55;
var FRAME_PAGE_LENGTH = 13;
var FRAME_COST_LENGTH = 10;
var FRAME_ATTR_LENGTH = 0; // Space that an attribute takes
/**
* This function converts ANSI text into an array of attributes
*
* We include the attribute for every character, so that if a window is placed on top of this window, the edges
* render correctly.
*
* @param contents - Our ANSI content to convert
* @param width - The width before wrapping to the next line
* @param yoffset - fields offset as discovered
* @param xoffset - fields offset as discovered
* @param debug - Enable debug mode
*/
function rawtoattrs(contents,width,yoffset,xoffset,debug) {
if (debug)
writeln('DEBUG active: '+debug);
lines = (''+contents).split(/\r\n/);
var i = 0;
var bg = BG_BLACK;
var fg = LIGHTGRAY;
var attr = fg + bg + i;
var y = 0;
// Saving cursor positions
var saved = {};
var frame = {
content: [],
dynamic_fields: [],
input_fields: [],
};
// @todo temp hack, rework ansi variable - perhaps have a function that converts an attribute back into an ANSI sequence
var ansi = { i: 0, f: 37, b: 40 };
var line;
var x;
while (lines.length > 0) {
x = 0;
line = lines.shift();
if ((debug !== undefined) && (y > debug)) {
exit(1);
}
if (debug) {
log(LOG_DEBUG,'y:'+y);
log(LOG_DEBUG,'line:'+line);
}
while (line.length > 0) {
/* parse an attribute sequence*/
var m = line.match(/^\x1b\[((\d+)(;?(\d+))*)+m/);
if (m !== null) {
line = line.substr(m.shift().length);
m = m.shift().split(';').sort(function(a,b) { return Number(a) < Number(b) ? -1 : 1; });
if ((debug !== undefined) && debug === y) {
//writeln();
log(LOG_DEBUG,'m:'+JSON.stringify(m));
}
// Reset
if (Number(m[0]) === 0) {
bg = BG_BLACK;
fg = LIGHTGRAY;
i = 0;
ansi = { i:0, b:37, f:40};
m.shift();
if (debug)
log(LOG_DEBUG,' - RESET');
}
// High Intensity
if (Number(m[0]) === 1) {
i += (((i === 0) || (i === BLINK)) ? HIGH : 0);
m.shift();
ansi.i = 1;
if (debug && (debug === y))
writeln('fg:'+fg+', bg:'+bg+' i:'+i);
}
// Blink
if (Number(m[0]) === 5) {
i += (((i === 0) || (i === HIGH)) ? BLINK : 0);
m.shift();
}
// Foreground
if ((Number(m[0]) >= 30) && (Number(m[0]) <= 37)) {
ansi.f = Number(m[0]);
switch(Number(m.shift())) {
case 30:
fg = BLACK;
break;
case 31:
fg = RED;
break;
case 32:
fg = GREEN;
break;
case 33:
fg = BROWN;
break;
case 34:
fg = BLUE;
break;
case 35:
fg = MAGENTA;
break;
case 36:
fg = CYAN;
break;
case 37:
fg = LIGHTGRAY;
break;
}
}
// Background
if ((Number(m[0]) >= 40) && (Number(m[0]) <= 47)) {
ansi.b = Number(m[0]);
switch (Number(m.shift())) {
case 40:
bg = BG_BLACK;
break;
case 41:
bg = BG_RED;
break;
case 42:
bg = BG_GREEN;
break;
case 43:
bg = BG_BROWN;
break;
case 44:
bg = BG_BLUE;
break;
case 45:
bg = BG_MAGENTA;
break;
case 46:
bg = BG_CYAN;
break;
case 47:
bg = BG_LIGHTGRAY;
break;
}
}
if (debug) {
log(LOG_DEBUG,'fg:'+fg+', bg:'+bg+' i:'+i);
log(LOG_DEBUG,'ansi:'+JSON.stringify(ansi));
}
attr = bg + fg + i;
continue;
}
if (debug)
log(LOG_DEBUG,'= Current attr:'+attr);
/* parse absolute character position */
var m = line.match(/^\x1b\[(\d*);?(\d*)[Hf]/);
if (m !== null) {
line = line.substr(m.shift().length);
if (m.length === 0) {
x = 0;
y = 0;
} else {
if(m[0])
y = Number(m.shift())-1;
if(m[0])
x = Number(m.shift())-1;
}
continue;
}
/* ignore a bullshit sequence */
var n = line.match(/^\x1b\[\?7h/);
if (n !== null) {
line = line.substr(n.shift().length);
continue;
}
/* parse an up positional sequence */
var n = line.match(/^\x1b\[(\d*)A/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift();
y -= (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a down positional sequence */
var n = line.match(/^\x1b\[(\d*)B/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift();
y += (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a forward positional sequence */
var n = line.match(/^\x1b\[(\d*)C/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift();
x += (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a backward positional sequence */
var n = line.match(/^\x1b\[(\d*)D/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift()
x -= (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a clear screen sequence */
var n = line.match(/^\x1b\[2J/);
if (n !== null) {
line = line.substr(n.shift().length);
continue;
}
/* parse save cursor sequence */
var n = line.match(/^\x1b\[s/);
if (n !== null) {
line = line.substr(n.shift().length);
saved.x = x;
saved.y = y;
continue;
}
/* parse restore cursor sequence */
var n = line.match(/^\x1b\[u/);
if (n !== null) {
line = line.substr(n.shift().length);
x = saved.x;
y = saved.y;
continue;
}
/* parse an input field */
// Input field 'FIELD;valueTYPE;input char'
// @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03
var m = line.match(/^\x1b_(([A-Z]+;[0-9a-z]+)([;]?.+)?)\x1b\\/);
if (m !== null) {
log(LOG_DEBUG,'|-- Got input field: '+JSON.stringify(m));
//log(LOG_DEBUG,'|-- + ansi:'+JSON.stringify(ansi));
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var sos = m.shift().split(';');
//writeln('sos:'+JSON.stringify(sos));
for (var num in sos) {
switch (num) {
// First value is the field name
case '0':
field = sos[num];
break;
// Second value is the length/type of the field, nnX nn=size in chars, T=type (lower case)
case '1':
var c = sos[num].match(/([0-9]+)([a-z])/);
if (! c) {
log(LOG_ERROR,'SOS FAILED PARSING FIELD LENGTH/TYPE. ['+r+'x'+c+'] '+sos[num]);
break;
}
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] NUM CHARS: '+x[1]+', TYPE: '+x[2]);
fieldlen = c[1];
fieldtype = c[2];
break;
// Third field is the char to to use
case '2':
fieldchar = sos[num];
break;
default:
log(LOG_ERROR,'IGNORING ADDITIONAL SOS FIELDS. ['+r+'x'+c+'] '+sos[num]);
}
}
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
if (fieldlen)
line = fieldchar.repeat(fieldlen)+line;
frame.input_fields.push({
type: fieldtype,
length: Number(fieldlen),
char: fieldchar,
name: field,
attribute: JSON.parse(JSON.stringify(ansi)),
x: Number(x+(xoffset !== undefined ? xoffset : 0)),
y: Number(y+(yoffset !== undefined ? yoffset : 0)),
value: '',
});
log(LOG_DEBUG,'|-- * IF found at ['+x+'x'+y+'], Type: '+fieldtype+', Name: '+field+', Char: '+fieldchar+', Length: '+fieldlen+', Attribute: '+JSON.stringify(ansi));
}
/* parse dynamic value field */
// @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute)
var m = line.match(/^\x1bX(([a-zA-Z._:\-%^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?)\x1b\\/);
if (m !== null) {
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var df = m.shift().split(';');
log(LOG_DEBUG,'|-- * DF found at ['+x+'x'+y+'], Field: '+df[0]+', Length: '+df[1]+', Pad:'+df[2]);
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
line = (df[2] ? df[2] : '_').repeat(Math.abs(df[1]))+line;
frame.dynamic_fields.push({
name: df[0],
length: df[1],
pad: df[2],
x: x+(xoffset !== undefined ? xoffset : 0),
y: y+(yoffset !== undefined ? yoffset : 0),
value: undefined,
});
}
/* set character and attribute */
var ch = line[0];
line = line.substr(1);
if (debug && debug === y) {
log(LOG_DEBUG,'Got char: '+ch);
//write(ch);
}
/* validate position */
if (y < 0)
y = 0;
if (x < 0)
x = 0;
if (x >= width) {
x = 0;
y++;
}
/* set character and attribute */
if (! frame.content[y+1])
frame.content[y+1]=[];
if (attr === null)
throw new Error('Attribute is null?');
frame.content[y+1][x+1] = new Char(ch,attr);
x++;
}
// If we got a BG_BLACK|LIGHTGRAY ESC [0m, but not character, we include it as it resets any background that was going on
if ((attr === BG_BLACK|LIGHTGRAY) && frame.content[y+1] && (frame.content[y+1][x].__properties__.attr !== attr))
frame.content[y+1][x+1] = new Char(undefined,attr);
y++;
}
return frame;
}
load('ansitex/load/session.js');
// Our frame object
function SessionProtocol() {
Session.apply(this,arguments);
this.settings.MSG_SENDORNOT = '\1n\1h\1GKEY 1 TO SEND, 2 NOT TO SEND';
this.settings.MSG_LOGON = '\1n\1h\1GKEY 1 TO LOGON, 2 TO RETURN';
this.settings.MSG_SENT = '\1n\1h\1GMESSAGE SENT - KEY # TO CONTINUE';
this.settings.MSG_NOTSENT = '\1n\1h\1GMESSAGE NOT SENT - KEY # TO CONTINUE';
this.settings.ERR_NO_PARENT = '\1n\1h\1RPARENT FRAME DOESNT EXIST';
this.settings.ERR_NOT_IMPLEMENTED = '\1n\1h\1RNOT IMPLEMENTED YET?';
this.settings.ERR_ROUTE = '\1n\1h\1WMISTAKE? \1GTRY AGAIN OR TELL US ON *08';
this.settings.ERR_METHOD_NOT_EXIST = '\1n\1h\1WMISTAKE? \1GTRY AGAIN OR TELL US ON *08';
this.settings.ACCESS_DENIED = '\1n\1h\1RACCESS DENIED. MISTAKE? TRY AGAIN OR TELL US *08';
this.settings.ALREADY_MEMBER = '\1n\1h\1RALREADY MEMBER OF CUG'
this.settings.INACTIVITY = '\1n\1h\1RINACTIVITY ALERT, DISCONNECT PENDING...';
this.settings.INACTIVE = '\1n\1h\1RINACTIVITY DISCONNECT';
this.settings.NOACTION = '\1n\1h\1RNO ACTION PERFORMED';
this.settings.BASESTAR = '\1N\1G\1H*';
this.settings.INVALID_CODE = '\1n\1h\1RINVAID CODE, PLEASE TRY AGAIN **';
this.settings.TOKEN_EMAIL = '\1n\1h\1RTOKEN EMAILED TO YOU...';
this.settings.TOKEN_SENT = '\1n\1h\1RTOKEN SENT, PLEASE ENTER TOKEN';
this.settings.INVALID_EMAIL = '\1n\1h\1RINVAID EMAIL, PLEASE TRY AGAIN *00';
this.settings.INVALID_UID = '\1n\1h\1RINVAID USER ID, PLEASE TRY AGAIN *00';
this.settings.CANNOT_SEND_TOKEN = '\1n\1h\1RCANNOT SEND VALIDATION CODE, PLEASE TRY AGAIN *00';
this.settings.USER_EXISTS = '\1n\1h\1RERROR USER EXISTS, PLEASE TRY AGAIN *00';
this.settings.USER_CREATE_ERROR = '\1n\1h\1RERROR CREATING USER, PLEASE TRY AGAIN *00';
this.settings.LOGIN_ERROR = '\1n\1h\1RERROR LOGGING IN, PLEASE TRY AGAIN *00';
this.settings.CANCEL_MSG = '\1n\1h\1GPRESS 2 TO CANCEL';
this.settings.SYS_ERROR = '\1n\1h\1RSYSTEM ERROR DETECTED - TRY AGAIN OR TELL US *08';
this.settings.LOADING = '\1n\1h\1Kloading...';
this.settings.PROCESSING = '\1n\1h\1Kprocessing...';
/**
* Set the attribute at the current position
*/
this.attr = function(field) {
write('\x1b['+field.i+';'+field.f+';'+field.b+'m');
}
this.baselineClear = function(reposition) {
this.cursorSave();
this.gotoxy(0,24);
this.cleareol();
if (! reposition)
this.cursorRestore();
}
/**
* Send a message to the baseline.
*
* @param text
* @param reposition
*/
this.baselineSend = function(text,reposition) {
this.cursorSave();
this.gotoxy(0,24);
write(this.getMessage(text));
this.cleareol();
if (! reposition)
this.cursorRestore();
}
/**
* Turn off the cursor
*/
this.cursorOff = function() {
write('\x1b[?25l');
write('\x1b[24;0H');
}
/**
* Turn on cursor
* @param x
* @param y
*/
this.cursorOn = function(x,y) {
write('\x1b[?25h');
if (x && y)
this.gotoxy(x,y);
}
this.cursorRestore = function() {
write('\x1b[u');
}
this.cursorSave = function() {
write('\x1b[s');
}
this.cleareol = function() {
write('\x1b[0K');
}
// Field backspace, that leaves the field filler char
this.fieldbs = function(char) {
write('\x1b[D'+char+'\x1b[D');
}
/**
* Position the cursor in a specific location
*
* @param x
* @param y
*/
this.gotoxy = function(x,y) {
write('\x1b['+y+';'+x+'H');
}
this.qrcode = function(qr,subframe) {
// SMALL Image
var full = ascii(0xdb);
var top = ascii(0xdf);
var bot = ascii(0xdc);
var blank = ' ';
var qrcode = '';
/*
// Render the top line
var line = ascii(27)+'[1;37m'+bot;
for (var y = 0; y < qr.size; y++) {
line += bot;
}
qrcode += line+bot+bot+ascii(27)+'[0m'+"\r\n";
*/
// Render the body
for (var x = -1; x < qr.size; x=x+2) {
line = ascii(27)+'[1;37m'+full;
for (var y = 0; y < qr.size; y++) {
// Top is white
if (! ((x===-1)? 0 : qr.getModule(x, y))) {
line += (qr.getModule(x+1, y)) ? top : full;
// Top is black
} else {
line += (qr.getModule(x+1, y)) ? blank : bot;
}
}
qrcode += line+full+ascii(27)+'[0m'+"\r\n";
}
// Render the bottom
line = ascii(27)+'[1;37m'+top;
for (var y = 0; y < qr.size; y++) {
line += top;
}
qrcode += line+top+ascii(27)+'[0m'+"\r\n";
ans2bin(fo.parse(qrcode),subframe);
subframe.open();
subframe.cycle();
};
}
SessionProtocol.prototype = Session.prototype;
SessionProtocol.prototype.constructor = SessionProtocol;

650
load/session/viewdata.js Normal file
View File

@ -0,0 +1,650 @@
const SESSION_VIEWDATA = (1<<2);
var CONTENT_EXT = 'bin';
var FRAME_WIDTH = 40;
var FRAME_HEIGHT = 22;
var FRAME_PROVIDER_LENGTH = 23;
var FRAME_PAGE_LENGTH = 11;
var FRAME_COST_LENGTH = 6;
var FRAME_ATTR_LENGTH = 1; // Space that an attribute takes
const VIEWDATA_LEFT = '\x08';
const VIEWDATA_RIGHT = '\x09';
const VIEWDATA_DOWN = '\x0a'; // \n
const VIEWDATA_UP = '\x0b';
const VIEWDATA_CLS = '\x0c';
const VIEWDATA_CR = '\x0d'; // \r
const VIEWDATA_CON = '\x11';
const VIEWDATA_COFF = '\x14';
const VIEWDATA_HOME = '\x1e';
const VIEWDATA_BLINK = '\x48';
const VIEWDATA_STEADY = '\x49';
const VIEWDATA_NORMAL = '\x4c';
const VIEWDATA_DOUBLE = '\x4d';
const VIEWDATA_CONCEAL = '\x58';
const VIEWDATA_BLOCKS = '\x59';
const VIEWDATA_SEPARATED = '\x5a';
const VIEWDATA_BLACKBACK = '\x5c';
const VIEWDATA_NEWBACK = '\x5d';
const VIEWDATA_HOLD = '\x5e';
const VIEWDATA_REVEAL = '\x5f';
const VIEWDATA_RED = '\x41';
const VIEWDATA_GREEN = '\x42';
const VIEWDATA_YELLOW = '\x43'; // C
const VIEWDATA_BLUE = '\x44';
const VIEWDATA_MAGENTA = '\x45';
const VIEWDATA_CYAN = '\x46';
const VIEWDATA_WHITE = '\x47';
const VIEWDATA_MOSIAC_RED = '\x51';
const VIEWDATA_MOSIAC_GREEN = '\x52';
const VIEWDATA_MOSIAC_YELLOW = '\x53';
const VIEWDATA_MOSIAC_BLUE = '\x54';
const VIEWDATA_MOSIAC_MAGENTA = '\x55';
const VIEWDATA_MOSIAC_CYAN = '\x56';
const VIEWDATA_MOSIAC_WHITE = '\x57'; // W
/* BINARY DUMP LEVEL 1 ATTRIBUTES */
const VIEWDATA_BIN_RED = '\x01';
const VIEWDATA_BIN_GREEN = '\x02';
const VIEWDATA_BIN_YELLOW = '\x03';
const VIEWDATA_BIN_BLUE = '\x04';
const VIEWDATA_BIN_MAGENTA = '\x05';
const VIEWDATA_BIN_CYAN = '\x06';
const VIEWDATA_BIN_WHITE = '\x07';
/**
* ViewData characters are 7bit (0x00-0x7f)
*
* Chars 0x00-0x1f are control characters (display attributes) and are sent to the terminal with 0x1b
* + 0x00-0x07 are foreground colors
* + 0x08-0x09 flash/steady
* + 0x0a-0x0b end/start box (?) *
* + 0x0c-0x0d normal/double height
* + 0x0e-0x0f double width (?) *
* + 0x10-0x17 are foreground graphics (mosiac) colors
* + 0x18/0x1f conceal/reveal
* + 0x19-0x1a solid/seperated graphics
* + 0x1b unused
* + 0x1c-1x1d Black/New Background (new background converts color foreground to background)
* + 0x1e-0x1f graphics hold/release (enables changing color and repeats previous graphics char)
* Chars 0x20-0x7f are normal printed ASCII chars
* Chars 0x20-0x3f & 0x60-0x7f when activated with a MOSIAC color sends a 2x3 pixel character
*
* We can map these into cga_defs with the following amendments:
* 0x00-0x0f = foreground/background colors (4 bits) (8 foreground/8 background colors)
* 0x10 - mosiac (bit 4)
* 0x20 - conceal (bit 5)
* 0x40 - seperated graphics (bit 6)
* 0x80 - flash (bit 7)
* 0x100 - double height (bit 8)
* 0x200 - hold (bit 9)
* 0x400 - new background (bits 10/11)
* 0x800 - black background (bits 10/11)
* 0xc00 - unused (bits 10/11)
* bits (12-15) unused
*
* @type {number}
*/
var MOSIAC = 0x10;
// Toggles
var CONCEAL = 0x20;
var REVEAL = 0x2000; // @temp Turns off Conceal
var SEPARATED = 0x40;
var BLOCKS = 0x4000; // @temp Turns off Separated
var STEADY = 0x8000; // @temp (turn off flash)
var DOUBLE = 0x100;
var NORMAL = 0x1000; // @temp Turns off Double Height
var HOLD = 0x200;
var RELEASE = 0x20000; // @temp turns off Hold
var NEWBACK = 0x400;
var BLACKBACK = 0x800;
/**
* This function converts ANSI text into an array of attributes
*
* @param contents - Our ANSI content to convert
* @param width - The width before wrapping to the next line
* @param yoffset - fields offset as discovered
* @param xoffset - fields offset as discovered
* @param debug - Enable debug mode
*/
function rawtoattrs(contents,width,yoffset,xoffset,debug) {
if (debug)
writeln('DEBUG active: '+debug);
lines = (''+contents).split(/\r\n/);
var i = 0;
var bg = BG_BLACK;
var fg = LIGHTGRAY;
var attr = fg + bg + i;
// Attribute state on a new line
var new_line = attr;
var y = 0;
var frame = {
content: [],
dynamic_fields: [],
input_fields: [],
};
// @todo temp hack, rework ansi variable - perhaps have a function that converts an attribute back into an ANSI sequence
var ansi = { i: 0, f: 37, b: 40 };
while (lines.length > 0) {
var x = 0;
var line = lines.shift();
if ((debug !== undefined) && (y > debug)) {
exit(1);
}
if (debug) {
log(LOG_DEBUG,'y:'+y);
log(LOG_DEBUG,'line:'+line);
write('y:'+y+', line:'+line);
}
while (line.length > 0) {
if (x >= width) {
x = 0;
// Each new line, we reset the attrs
attr = new_line;
y++;
}
//writeln('next ch:'+line[0].charCodeAt(0));
/* parse control codes */
var m = line.match(/^([\x00-\x1f])/);
if (m !== null) {
line = line.substr(m[0].length);
attr = 0;
match = m.shift().charCodeAt(0);
/*
writeln('- match:'+match);
writeln('- match 0x0f:'+(match & 0x0f));
if (match & 0x10) {
writeln(' - got mosiac');
attr += MOSIAC;
}
*/
//if (match < 0x0f) {
//switch(match & 0x07) {
switch(match) {
case 0x00:
attr += BLACK;
break;
case 0x01:
attr += RED;
break;
case 0x02:
attr += GREEN;
break;
case 0x03:
attr += YELLOW;
break;
case 0x04:
attr += BLUE;
break;
case 0x05:
attr += MAGENTA;
break;
case 0x06:
attr += CYAN;
break;
case 0x07:
attr += LIGHTGRAY;
break;
case 0x08:
attr = BLINK;
break;
case 0x09:
attr = STEADY;
break;
/*
case 0x0a:
//attr = ENDBOX; // End Box (Unused?)
break;
case 0x0b:
//attr = STARTBOX; // Start Box (Unused?)
break;
*/
case 0x0c:
//attr &= ~DOUBLE;
attr = NORMAL;
break;
case 0x0d:
attr = DOUBLE;
break;
case 0x0e:
attr = NORMAL; // @todo Double Width (Unused)?
break;
case 0x0f:
attr = NORMAL; // @todo Double Width (Unused?)
break;
case 0x10:
attr = MOSIAC|BLACK;
break;
case 0x11:
attr += MOSIAC|RED;
break;
case 0x12:
attr += MOSIAC|GREEN;
break;
case 0x13:
attr += MOSIAC|YELLOW;
break;
case 0x14:
attr += MOSIAC|BLUE;
break;
case 0x15:
attr += MOSIAC|MAGENTA;
break;
case 0x16:
attr += MOSIAC|CYAN;
break;
case 0x17:
attr += MOSIAC|LIGHTGRAY;
break;
case 0x18:
attr = CONCEAL;
break;
case 0x19:
attr = BLOCKS;
break;
case 0x1a:
attr = SEPARATED;
break;
/*
case 0x1b:
//attr = NORMAL; // CSI
break;
*/
case 0x1c:
attr = BLACKBACK; // Black Background
break;
case 0x1d:
attr = NEWBACK; // New Background
break;
case 0x1e:
attr = HOLD; // Mosiac Hold
break;
case 0x1f:
attr = RELEASE; // Mosiac Release
break;
// Catch all for other codes
default:
attr = 0xff00;
}
if (debug)
writeln(' - got control code:'+attr+'['+y+','+x+'] - length:'+attr.length);
store(x++,y,null,attr);
attr = undefined;
continue;
}
/* parse an input field */
// Input field 'FIELD;valueTYPE;input char'
// @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03
var m = line.match(/^\x1b_(([A-Z]+;[0-9a-z]+)([;]?.+)?)\x1b\\/);
if (m !== null) {
log(LOG_DEBUG,'Got input field: '+JSON.stringify(m));
log(LOG_DEBUG,'ansi:'+JSON.stringify(ansi));
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var sos = m.shift().split(';');
//writeln('sos:'+JSON.stringify(sos));
for (var num in sos) {
switch (num) {
// First value is the field name
case '0':
field = sos[num];
break;
// Second value is the length/type of the field, nnX nn=size in chars, T=type (lower case)
case '1':
var c = sos[num].match(/([0-9]+)([a-z])/);
if (! c) {
log(LOG_ERROR,'SOS FAILED PARSING FIELD LENGTH/TYPE. ['+r+'x'+c+'] '+sos[num]);
break;
}
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] NUM CHARS: '+x[1]+', TYPE: '+x[2]);
fieldlen = c[1];
fieldtype = c[2];
break;
// Third field is the char to to use
case '2':
fieldchar = sos[num];
break;
default:
log(LOG_ERROR,'IGNORING ADDITIONAL SOS FIELDS. ['+r+'x'+c+'] '+sos[num]);
}
}
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
if (fieldlen)
line = fieldchar.repeat(fieldlen)+line;
frame.input_fields.push({
type: fieldtype,
length: Number(fieldlen),
char: fieldchar,
name: field,
attribute: JSON.parse(JSON.stringify(ansi)),
x: Number(x+(xoffset !== undefined ? xoffset : 0)),
y: Number(y+(yoffset !== undefined ? yoffset : 0)),
value: '',
});
log(LOG_DEBUG,'input_field:'+JSON.stringify(frame.input_fields.last));
}
/* parse dynamic value field */
// @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute)
var m = line.match(/^\x1bX(([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?)\x1b\\/);
if (m !== null) {
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var df = m.shift().split(';');
log(LOG_DEBUG,'- DF found at ['+x+'x'+y+'], Field: '+df[0]+', Length: '+df[1]+', Pad:'+df[2]);
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
line = (df[2] ? df[2] : '_').repeat(Math.abs(df[1]))+line;
frame.dynamic_fields.push({
name: df[0],
length: df[1],
pad: df[2],
x: x+(xoffset !== undefined ? xoffset : 0),
y: y+(yoffset !== undefined ? yoffset : 0),
value: undefined,
});
}
/* set character and attribute */
var ch = line[0];
line = line.substr(1);
if (debug && (debug === y)) {
writeln('y:'+y+', x:'+x+', ch:'+ch);
}
/* validate position */
if (y < 0)
y = 0;
if (x < 0)
x = 0;
store(x,y,ch,undefined);
x++;
}
// Each new line, we reset the attrs
attr = undefined;
y++;
}
return frame;
function store(x,y,ch,attr) {
/* set character and attribute */
if (! frame.content[y+1])
frame.content[y+1]=[];
frame.content[y+1][x+1] = new Char(ch,attr,SESSION_EXT);
}
}
load('ansitex/load/session.js');
// Our frame object
function SessionProtocol() {
Session.apply(this,arguments);
this.settings.MSG_SENDORNOT = ascii(27)+'BKEY 1 TO SEND, 2 NOT TO SEND';
this.settings.MSG_LOGON = ascii(27)+'BKEY 1 TO LOGON, 2 TO RETURN';
this.settings.MSG_SENT = ascii(27)+'BMESSAGE SENT - KEY _ TO CONTINUE';
this.settings.MSG_NOTSENT = ascii(27)+'BMESSAGE NOT SENT - KEY _ TO CONTINUE';
this.settings.ERR_NO_PARENT = ascii(27)+'APARENT FRAME DOESNT EXIST';
this.settings.ERR_NOT_IMPLEMENTED = ascii(27)+'ANOT IMPLEMENTED YET?';
this.settings.ERR_ROUTE = ascii(27)+'GMISTAKE?'+ascii(27)+'BTRY AGAIN OR TELL US ON *08';
this.settings.ERR_METHOD_NOT_EXIST = ascii(27)+'GMISTAKE?'+ascii(27)+'BTRY AGAIN OR TELL US ON *08';
this.settings.ACCESS_DENIED = ascii(27)+'AACCESS DENIED.';
this.settings.ALREADY_MEMBER = ascii(27)+'AALREADY MEMBER OF CUG'
this.settings.INACTIVITY = ascii(27)+'AINACTIVITY ALERT, DISCONNECT PENDING...';
this.settings.INACTIVE = ascii(27)+'AINACTIVITY DISCONNECT';
this.settings.NOACTION = ascii(27)+'ANO ACTION PERFORMED';
this.settings.BASESTAR = ascii(27)+'B*';
this.settings.INVALID_CODE = ascii(27)+'AINVAID CODE, PLEASE TRY AGAIN **';
this.settings.TOKEN_EMAIL = ascii(27)+'ATOKEN EMAILED TO YOU...';
this.settings.TOKEN_SENT = ascii(27)+'ATOKEN SENT, PLEASE ENTER TOKEN';
this.settings.INVALID_EMAIL = ascii(27)+'AINVAID EMAIL, PLEASE TRY AGAIN *00';
this.settings.INVALID_UID = ascii(27)+'AINVAID USER ID, PLEASE TRY AGAIN *00';
this.settings.CANNOT_SEND_TOKEN = ascii(27)+'ACANNOT SEND VALIDATION CODE, PLEASE TRY AGAIN *00';
this.settings.USER_EXISTS = ascii(27)+'AERROR USER EXISTS, PLEASE TRY AGAIN *00';
this.settings.USER_CREATE_ERROR = ascii(27)+'AERROR CREATING USER, PLEASE TRY AGAIN *00';
this.settings.LOGIN_ERROR = ascii(27)+'AERROR LOGGING IN, PLEASE TRY AGAIN *00';
this.settings.CANCEL_MSG = ascii(27)+'BPRESS 2 TO CANCEL';
this.settings.SYS_ERROR = ascii(27)+'ASYS ERR, TRY AGAIN OR TELL US ON *08';
this.settings.LOADING = ascii(27)+'Cloading...';
this.settings.PROCESSING = ESC+VIEWDATA_YELLOW+'processing...';
var blp = 0; // Length of data on the bottom line
/**
* Set the attribute at the current position
*/
this.attr = function(field) {
//NOOP - the terminal takes care of this
}
this.baselineClear = function(reposition) {
msg = '';
log(LOG_DEBUG,'- Clear Bottom Line ['+blp+'] - reposition ['+reposition+']');
write_raw(VIEWDATA_HOME+VIEWDATA_UP+msg+
((blp > msg.length)
? (' '.repeat(2+blp-msg.length)+(reposition ? VIEWDATA_HOME+VIEWDATA_UP+VIEWDATA_RIGHT.repeat(msg.length) : ''))
: '')
);
blp = msg.length;
}
/**
* Send a message to the baseline.
*
* @param text
* @param reposition
*/
this.baselineSend = function(text,reposition) {
var msg = this.getMessage(text);
var x = this.strlen(msg);
log(LOG_DEBUG,'- Bottom Line ['+msg+'] ('+x+') - reposition ['+reposition+'] BLP:'+blp);
write_raw(VIEWDATA_HOME+VIEWDATA_UP+msg+
((blp > x)
? (' '.repeat(blp-x)+(reposition ? VIEWDATA_HOME+VIEWDATA_UP+VIEWDATA_RIGHT.repeat(x) : ''))
: '')
);
blp = x;
}
/**
* Turn off the cursor
*/
this.cursorOff = function() {
write_raw(VIEWDATA_COFF);
}
/**
* Turn on cursor
* @param x
* @param y
*/
this.cursorOn = function(x,y) {
write_raw(VIEWDATA_CON);
if (x && y)
this.gotoxy(x,y);
}
// Field backspace, that leaves the field filler char
this.fieldbs = function(char) {
log(LOG_DEBUG,'- Field backspace with char:'+char);
write_raw(VIEWDATA_LEFT+char+VIEWDATA_LEFT);
}
this.gotoxy = function(x,y) {
log(LOG_DEBUG,'- Moving cursor to y:'+y+', x:'+x);
// @todo This could be optimised to go the shortest route
write_raw(VIEWDATA_HOME);
if (x > 0)
write_raw(VIEWDATA_RIGHT.repeat(x-1));
if (y > 0)
write_raw(VIEWDATA_DOWN.repeat(y-1));
}
this.strlen = function(str) {
return str.replace(/\x1b/g,'').length;
};
this.qrcode = function(qr) {
// Render the body
var qrcode = VIEWDATA_HOME+VIEWDATA_DOWN.repeat(5);
var offset = this.settings.FRAME_WIDTH-Math.ceil(qr.size/2)-1;
for (var x = -1; x < qr.size; x=x+3) {
var line = VIEWDATA_RIGHT.repeat(offset ? offset-1 : 0)+ESC+VIEWDATA_MOSIAC_WHITE;
for (var y = -1; y < qr.size; y=y+2) {
var char = 0;
//TL
char |= ((x===-1) || (y===-1) || ! qr.getModule(x,y)) ? (1<<0) : (0<<0);
//TR
char |= ((x===-1) || (y === qr.size-1) || ! qr.getModule(x,y+1)) ? (1<<1) : (0<<1);
//ML
char |= ((y===-1) || ! qr.getModule(x+1,y)) ? (1<<2) : (0<<2);
//MR
char |= ((y === qr.size-1) || ! qr.getModule(x+1,y+1)) ? (1<<3) : (0<<3);
//BL
char |= ((x===qr.size-2) || (y===-1) || ! qr.getModule(x+2,y)) ? (1<<4) : (0<<4);
//BR
char |= ((x===qr.size-2) || (y === qr.size-1) || ! qr.getModule(x+2,y+1)) ? (1<<5) : (0<<5);
char += 0x20;
if (char > 0x3f)
char += 0x20;
line += ascii(char);
}
// Render the right column
if (y%2)
line += '\x35';
repeat_count = this.settings.FRAME_WIDTH-Math.ceil(qr.size/2)-offset-(offset ? 1 : 2)-(y%2 === 1 ? 0 : 1);
qrcode += line+' '.repeat(repeat_count > 0 ? repeat_count : 0);
// To fix some terminals where moving right from col 40 doesnt advance to col 1 on the next line
qrcode +=VIEWDATA_LEFT+VIEWDATA_CR+VIEWDATA_DOWN;
}
log(LOG_DEBUG,'WIDTH:'+this.settings.FRAME_WIDTH);
log(LOG_DEBUG,'QR :'+(Math.ceil(qr.size/2)+1));
log(LOG_DEBUG,'OFF :'+offset);
log(LOG_DEBUG,'Y :'+(y%2 ? 0 : 1));
log(LOG_DEBUG,'X :'+(x%3 ? 0 : 1));
// Render the bottom
if (x%3) {
line = VIEWDATA_RIGHT.repeat(offset ? offset-1 : 0)+ESC+VIEWDATA_MOSIAC_WHITE;
for (var y = 0; y < qr.size; y=y+2) {
line += '\x23';
}
// Render the right column
if (y%2 === 0) {
line += '\x21';
}
qrcode += line+' '.repeat(repeat_count > 0 ? repeat_count : 0);
}
write_raw(qrcode);
};
/*
this.save=function() {
file = system.mods_dir+'ansitex/text/'+this.page+'.tex';
w = new File(file);
if (! w.open('w')) {
log(LOG_ERROR,'! ERROR: Unable to create TEX file for '+this.page);
exit(1);
}
w.write(JSON.stringify(this));
w.close();
log(LOG_DEBUG,'Saved file: '+this.page+'.tex');
}
*/
}
function videotex(data) {
var output = '';
//$output .= ($byte < 32) ? ESC.chr($byte+64) : chr($byte);
for (var i = 0; i < data.length; i++) {
output += (data.charCodeAt(i) < 32) ? "\x1b"+String.fromCharCode(data.charCodeAt(i)+64) : String.fromCharCode(data.charCodeAt(i));
}
return output;
}
SessionProtocol.prototype = Session.prototype;
SessionProtocol.prototype.constructor = SessionProtocol;

1070
load/windows.js Normal file

File diff suppressed because it is too large Load Diff

20
logon.js Normal file
View File

@ -0,0 +1,20 @@
require("sbbsdefs.js", 'SS_RLOGIN');
//require("nodedefs.js", 'NODE_QUIET');
// To force all users to the ansitex shell
// @todo Make a ini config setting for this
user.command_shell = 'ansitex';
// Disable System Info and ?
system.settings |= (SYS_NOSYSINFO | SYS_NONODELIST);
// @note: Unable to suppress "Logging on to <BBS> as <USER>..."
// Need to suppress Search for new messages & files
user.settings &= ~(USER_ASK_SSCAN | USER_ASK_NSCAN | USER_ANFSCAN | USER_PAUSE | USER_NO_EXASCII | USER_COLDKEYS);
// Enable ANSI and some other settings
user.settings |= (USER_AUTOTERM | USER_ANSI | USER_COLOR);
// Disable Chatting
user.chat_settings |= (CHAT_NOPAGE | CHAT_NOACT);

1290
main.js Normal file

File diff suppressed because it is too large Load Diff

1
text/1a Normal file
View File

@ -0,0 +1 @@
{"version":2,"cost":0,"key":[null,11,2,null,null,null,null,null,5162,516],"dynamic_fields":[{"name":"DATE:%Y-%b-%d","length":11,"pad":" ","x":30,"y":6},{"name":"nodeid","length":8,"pad":" ","x":33,"y":7}]}

1
text/980a Normal file
View File

@ -0,0 +1 @@
{"version":2,"key":[0,0,null,null,null,null,null,null,null,null]}

1
text/981a Normal file
View File

@ -0,0 +1 @@
{"version":2,"attrs":"1","key":[980,"register",null,null,null,null,null,null,null,null],"dynamic_fields":[{"name":"nodeid","length":8,"pad":" ","x":33,"y":2}],"input_fields":[{"type":"t","length":25,"char":".","name":"EMAIL","x":13,"y":11,"value":""},{"type":"t","length":15,"char":".","name":"USER","x":13,"y":12,"value":""},{"type":"p","length":15,"char":".","name":"PASS","x":13,"y":13,"value":""},{"type":"t","length":25,"char":".","name":"FULLNAME","x":13,"y":14,"value":""},{"type":"t","length":6,"char":".","name":"TOKEN","x":13,"y":15,"value":""}]}

1
text/981b Normal file
View File

@ -0,0 +1 @@
{"version":2,"key":[0,null,null,null,null,null,null,null,null,null]}

1
text/983a Normal file
View File

@ -0,0 +1 @@
{"version":2,"type":"i","key":[0,null,null,null,null,null,null,null,null,null],"dynamic_fields":[{"name":"nodeid","length":8,"pad":" ","x":33,"y":2}]}

1
text/98a Normal file
View File

@ -0,0 +1 @@
{"version":2,"attrs":12,"key":[null,"login",null,null,null,null,null,null,null,null],"dynamic_fields":[{"name":"nodeid","length":8,"pad":" ","x":33,"y":2}],"input_fields":[{"type":"t","length":15,"char":".","name":"USER","y":11,"x":16,"attribute":{},"value":""},{"type":"p","length":15,"char":".","name":"PASS","y":12,"x":16,"attribute":{},"value":""}]}

1
text/98b Normal file
View File

@ -0,0 +1 @@
{"version":2,"key":[1,null,null,null,null,null,null,null,null,null],"dynamic_fields":[{"name":"nodeid","length":8,"pad":" ","x":33,"y":2}]}

1
text/998a Normal file
View File

@ -0,0 +1 @@
{"version":2,"key":[0,null,null,null,null,null,null,null,null,null]}

1
text/999a Normal file
View File

@ -0,0 +1 @@
{"version":2,"key":[0,null,null,null,null,null,null,null,null,null]}

1
text/999b Normal file
View File

@ -0,0 +1 @@
{"version":2,"key":[0,null,null,null,null,null,null,null,null,null]}

1
text/99a Normal file
View File

@ -0,0 +1 @@
{"version":2,"key":[0,null,null,null,null,null,null,null,null,null]}

1
text/99b Normal file
View File

@ -0,0 +1 @@
{"version":2,"attrs":"15","key":[null,null,null,null,null,null,null,null,null,null]}

1
text/tex/198a Normal file
View File

@ -0,0 +1 @@
{"version":1,"frame":198,"index":"a","owner":1,"cost":0,"content":"G1swbRtbNzZDG1sxbS4bWzBtDQogG1szM20bWG1zZ19ncnBfbmFtZTs0MBtcG1szMEMbWzM3bRtbMW3awr/Cv7+zG1swbQ0KIBtbMzZtG1htc2dfYXJlYV9hcmVhdGFnOzQwG1wbWzMwQxtbMzdtsyCzwrSzsw0KIBtbMTszNm0bWG1zZ19hcmVhX2Rlc2M7NDAbXBtbMzBDG1sxOzM3bVN1bW1hcnkbWzBtDQobWzE7MzBtxMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSzxMTExMTExMTExMTExMTExMTExMTExMTExMTExBtbMG0NCiAbWERBVEVUSU1FOzI0G1wbWzI1QxtbMTszMG2zG1swbSAbWzFtTmV3IE1lc3NhZ2VzIHRvIFlvdRtbMG06IBtbMTszMW0bWG1zZ19hcmVhX25ld3RvbWU7LTUbXBtbMG0NChtbNTBDG1sxOzMwbbMbWzBtICAgICAbWzFtVW5yZWFkIE1lc3NhZ2VzG1swbTogG1sxOzMxbRtYbXNnX2FyZWFfbmV3Oy01G1wbWzBtDQogG1sxOzMybTEbWzM3bSBGaXJzdCB0byB5b3UgICAbWzM0bRtYbXNnX2FyZWFfbXNnb3RvbWVfZGF0ZTszMRtcG1swbSAbWzE7MzBtsxtbMG0gICAgICAbWzFtVG90YWwgTWVzc2FnZXMbWzBtOiAbWzE7MzFtG1htc2dfYXJlYV90b3RhbDstNRtcG1swbQ0KIBtbMTszMm0yG1swbSAbWzFtRmlyc3QgdW5yZWFkICAgG1sxOzM0bRtYbXNnX2FyZWFfbXNndW5yZWFkX2RhdGU7MzEbXBtbMG0gG1sxOzMwbbMbWzBtDQobWzUwQxtbMTszMG2zG1swbSAgICBQZW5kaW5nIE1lc3NhZ2VzOiAbWzMxbRtYbXNnX2FyZWFfcGVuZGluZzstNRtcG1szN20NCiAbWzE7MzJtMyAbWzM3bU9sZGVzdCBNZXNzYWdlIBtbMzRtG1htc2dfYXJlYV9tc2dvbGRlc3RfZGF0ZTszMRtcG1szN20gG1sxOzMwbbMbWzBtDQogG1sxOzMybTQgG1szN21MYXRlc3QgTWVzc2FnZSAbWzM0bRtYbXNnX2FyZWFfbXNnbmV3ZXN0X2RhdGU7MzEbXBtbMzdtIBtbMTszMG2zG1swbSAgICAgICAgICAbWzMybTkbWzBtIE5ldyBTY2FuOiAgIBtbMTszMm0bWG1zZ19hcmVhX25ld3NjYW47LTMbXBtbMG0NChtbNTBDG1sxOzMwbcEbWzBtDQogG1szMm01G1sxbSAbWzBtV3JpdGUgTmV3IE1lc3NhZ2UgICAgICAgICAgICAgICAgICAgICAgICAgG1sxOzQ0bdXNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NzbgbWzBtDQogG1szMm02G1sxbSAbWzBtU2VhcmNoIGZvciBNZXNzYWdlICAgICAgICAgICAgICAgICAgICAgICAgG1sxOzQ0bbMbWzMzbSCvIE1lc3NhZ2UgUmVhZGluZyBOYXZpZ2F0aW9uIK4gG1sxOzM3bbMbWzBtDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAbWzE7NDRtsyAbWzMybRgbWzM2bSBTY3JvbGwgVVAgG1swOzQ0bSAgG1sxbbMbWzA7NDRtIBtbMTszMm0ZG1szNm0gU2Nyb2xsIERPV04bWzA7NDRtIBtbMTszM20gG1szN22zG1swbQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgG1sxOzQ0bbMgG1szMm0xG1szNm0gUHJldiBUbyBNZSAgG1sxOzMzbbMbWzA7NDRtIBtbMTszMm0yG1szNm0gTmV4dCBUbyBNZSAgG1szM20gG1szN22zG1swbQ0KIBtbMzJtOBtbMzdtIEFyZWEgU3VtbWFyeSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBtbMTs0NG2zG1szM20gG1szMm00IBtbMzZtUHJldmlvdXMgICAgG1sxOzMzbbMbWzA7NDRtIBtbMzJtMxtbMTszNm0gG1swOzM2OzQ0bVByZXYgVGhyZWFkG1szN20gG1sxOzMzbSAbWzM3bbMbWzBtDQogG1sxOzMybTAbWzA7MzJtIBtbMTszN21BcmVhIERlc2NyaXB0aW9uICAgICAgICAgICAgICAgICAgICAgICAgICAbWzE7NDRtsxtbMzNtIBtbMzJtNhtbMzZtIE5leHQbWzFtIBtbMDs0NG0gICAgICAgG1sxbbMbWzA7NDRtIBtbMzJtNxtbMzdtIBtbMzZtTmV4dCBUaHJlYWQbWzM3bSAbWzE7MzNtIBtbMzdtsxtbMG0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBtbMTs0NG2zG1swOzQ0bSAbWzMybTUbWzM3bSAbWzM2bVdyaXRlIE5ldxtbMzdtICAgG1sxbbMbWzA7NDRtIBtbMzJtOBtbMzdtIBtbMzZtUmVwbHkbWzM3bSAgICAgICAbWzE7MzNtIBtbMzdtsxtbMG0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBtbMTs0NG2zG1swOzQ0bSAbWzMybSMbWzM3bSAbWzM2bU1zZyBBdHRycyAgG1szN20gG1sxbbMbWzA7NDRtIBtbMTszMm0wG1szN20gG1sxOzM2bVJldHVybiBoZXJlG1szN20gG1sxOzMzbSAbWzM3bbMbWzBtDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAbWzE7NDRt1M3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NvhtbMG0NCg==","isPublic":0,"isAccessible":1,"type":"m","key":[null,null,null,null,null,null,null,null,null,null],"date":"2022-04-29T00:00:00.000Z"}

1
text/tex/199a Normal file
View File

@ -0,0 +1 @@
{"version":1,"frame":199,"index":"a","owner":1,"cost":0,"content":"DQogG1sxOzM2bURhdGU6IBtbMzdtQERBVEU6NjBADQogICAbWzE7MzZtVG86IBtbMzdtQFRPOjYwQA0KIBtbMTszNm1Gcm9tOiAbWzM3bUBGUk9NOjYwQA0KIBtbMTszNm1TdWJqOiAbWzM3bUBTVUJKRUNUOjYwQA0KIBtbMTszMG3ExMTExMQbWzBtxMTExMTEG1sxOzMwbcTExMTExBtbMG3ExMTExMQbWzE7MzBtxMTExMTEG1swbcTExMTExBtbMTszMG3ExMTExMQbWzBtxMTExMTEG1sxOzMwbcTExMTExBtbMG3ExMTExMQbWzE7MzBtxMTExMTEG1swbQ0K","isPublic":0,"isAccessible":1,"type":"m","key":[null,null,null,null,null,null,null,null,null,null],"date":"2022-04-29T00:00:00.000Z"}

20
text/tex/1a.ans Normal file
View File

@ -0,0 +1,20 @@
 . ÚÄÜÚÄÜÚÄÜ ß ÚÂÜÚÄÜÚ Ü 
Ú¿¿¿Ú¿ Ú¿¿ڿ¿Ú ³ßÛ³ Û.\ÜÞÛ  ³ Ã_ÜÚßÜ 
³ ³Â´³³³ ³ ³ÃÙ³³³³ ÄÄÄÄÄÄÄÄÄÄÄÄÜÜÜÜÜÜÜÜÜÜ
À ÙÁÁÙÙÙ À ÙÁÙÙÙÁÙ node: Xnodeid;-11\
date: XDATE:%Y-%b-%d;-11\
time: XTIME;-11\
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄijÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ 1 Fido Message Networks ³ Useful Pages
2 BBS Door Games [Coming Soon] ³
³ *10012# Dovenet Message Network
³ *10021# FSXnet Message Network
³ *11337# TQWnet Message Network
³
³ *516# ANSItex
³ *5162# Navigating ANSItex
9 About ANSItex ³ *95# Help
³ *99# Log Off
 ³
³
*0# to get back here anytime

17
text/tex/980a.ans Normal file
View File

@ -0,0 +1,17 @@
 ÚÄÜÚÄÜÚÄÜ ß ÚÂÜÚÄÜÚ Ü 
Ú ¿Â¿³Ú¿Ú¿Ú¿¿ ³ßÛ³ Û.\ÜÞÛ  ³ Ã_ÜÚßÜ 
³³³ÃÙ³³ ³³³ ³ÃÙ ÄÄÄÄÄÄÄÄÄÄÄÄÜÜÜÜÜÜÜÜÜÜ
ÀÁÙÁÙÀÀÙÀÙÀ ÙÁÙ node: Xnodeid;-11\
 date: XDATE:%Y-%b-%d;-11\
time: XTIME;-11\
Welcome, you have connected to ANSItex a BBS that is based on the
1980's Videotex service, but using ANSI.
Depending on which country you live in the Videotex service was called
Viatel (AU), Prestel (UK), Minitex (FR), Telidon (CA), Ibertex (SP), etc

 If you got here by mistake, you should disconnect now, otherwise, you can
 press 0 to get to the login screen.



7
text/tex/983a.ans Normal file
View File

@ -0,0 +1,7 @@
レトワレトワレトワ ゚ レツワレトワレ ワ 
ウ゚ロウ ロ.\ヷロ  ウ テ_ワレ゚ワ 
トトトトトトトトトトトトワワワワワワワワワワ
Login FAILED.
To try again, please press 0

21
text/tex/98a.ans Normal file
View File

@ -0,0 +1,21 @@
 ÚÄÜÚÄÜÚÄÜ ß ÚÂÜÚÄÜÚ Ü 
³ßÛ³ Û.\ÜÞÛ  ³ Ã_ÜÚßÜ 
ÄÄÄÄÄÄÄÄÄÄÄÄÜÜÜÜÜÜÜÜÜÜ
. . ³ ³
Ú¿¿Â¿Ú¿ ¿Ú¿ ³ USER: _USER;25t;ù\ ³
À¿³³³³³ ³³³ ³ ³
ÀÙÙÁ´ÙÙ ÙÙÙ ³ PASS: _PASS;40p;ù\ ³
ÄÙ ³ ³
To register an account enter NEW for the user name
Use ** to clear your input *00 to start again

20
text/tex/98b.ans Normal file
View File

@ -0,0 +1,20 @@
 ÚÄÜÚÄÜÚÄÜ ß ÚÂÜÚÄÜÚ Ü 
Ú ¿Â¿³Ú¿Ú¿Ú¿¿ ³ßÛ³ Û.\ÜÞÛ  ³ Ã_ÜÚßÜ 
³³³ÃÙ³³ ³³³ ³ÃÙ ÄÄÄÄÄÄÄÄÄÄÄÄÜÜÜÜÜÜÜÜÜÜ
ÀÁÙÁÙÀÀÙÀÙÀ ÙÁÙ node: Xnodeid;-11\
date: XDATE:%Y-%b-%d;-11\
time: XTIME;-11\
Hi XREALNAME;20\

Welcome to ANSItex, a BBS inspired by Videotex/Viewdata of the 1980's and
1990's.
This BBS interface is build on top of Synchronet BBS, and might be something
different to what you are used to with other Synchronet BBS'.

 Navigation is via pages, which can be accessed via *pagenum#. Information is
available via page *516#, which will help you get started if you need it.
To get to the home page at any time, use *0#.
Press 0 Main Menu

21
text/tex/999a.ans Normal file
View File

@ -0,0 +1,21 @@
 Ă-ÂżÚżĂ- ڿ¿¿¿ ABCDEFGHIJKLMNOPQRSTUVWXYZ
ł ĂŮŔżł łłÂ´łłĂŮ abcdefghijklmnopqrstuvwxyz
ŔŮÁŮŔŮŔŮ ĂŮÁÁÁ´ÁŮ !@#$%^&*()_+{}|\:;"'<>?,./~
ŰßßßßßßßßßŔßßßßŮßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßŰ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł0 ÚżŔŮijôÁÂŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł1 ɻȼͺ̹ĘËŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł2 ոԾͳƵĎŃŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł3 ַӽĺǶĐŇŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł4 ĹÎŘ×ččś™ďŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł5 °±˛ŰßÜÝŢţúŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł6 đ Ţ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł7 Ţ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł8 ®Żňó©Şýö«¬Ţ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIł9 ăńôőęťäřűüŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIłA ŕáâĺćçëěíîŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIłB €‡Ą¤<30>ź÷ů­¨Ţ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIłC <30>„… ¦†ŽŹŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIłD <30>‰Š<30>ŚŤˇžŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIłE “”•˘§<30>—ŁšŢ
Ű BLA BLU GRE CYA RED MAG YEL WHI BLA BLU GRE CYA RED MAG YEL WHIłF /\(){}[]`'Ţ
ŰÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜŰ

21
text/tex/999b.ans Normal file
View File

@ -0,0 +1,21 @@
 Ă-ÂżÚżĂ- ڿ¿¿¿ ABCDEFGHIJKLMNOPQRSTUVWXYZ
ł ĂŮŔżł łłÂ´łłĂŮ abcdefghijklmnopqrstuvwxyz
ŔŮÁŮŔŮŔŮ ĂŮÁÁÁ´ÁŮ !@#$%^&*()_+{}|\:;"'<>?,./~
ŰßßßßßßßßßŔßßßßŮßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßŰ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł0 ÚżŔŮijôÁÂŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł1 ɻȼͺ̹ĘËŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł2 ոԾͳƵĎŃŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł3 ַӽĺǶĐŇŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł4 ĹÎŘ×ččś™ďŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł5 °±˛ŰßÜÝŢţúŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł6 đ Ţ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł7 Ţ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł8 ®Żňó©Şýö«¬Ţ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIł9 ăńôőęťäřűüŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIłA ŕáâĺćçëěíîŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIłB €‡Ą¤<30>ź÷ů­¨Ţ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIłC <30>„… ¦†ŽŹŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIłD <30>‰Š<30>ŚŤˇžŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIłE “”•˘§<30>—ŁšŢ
Ű BLA BLA RED RED GRE GRE YEL YEL BLU BLU MAG MAG CYA CYA WHI WHIłF /\(){}[]`'Ţ
ŰÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜŰ

21
text/tex/99a.ans Normal file
View File

@ -0,0 +1,21 @@
 Ú¿ ÚÄÜÚÄÜÚÄÜ ß ÚÂÜÚÄÜÚ Ü 
¿ڿڿڴÿ¿Ú¿ ³ßÛ³ Û.\ÜÞÛ  ³ Ã_ÜÚßÜ 
³³³³³³³³³³³³ÃÙ ÄÄÄÄÄÄÄÄÄÄÄÄÜÜÜÜÜÜÜÜÜÜ
Á´ÀÙÀÙÀÙÀÙÀ´ÁÙ node: Xnodeid;-11\
ÄÙ ÄÙ date: XDATE:%Y-%b-%d;-11\
time: XTIME;-11\
You are about to disconnect from ANSItex XREALNAME;20\
If you wanted to stay online, you can press *0# to get back to the main menu
Otherwise press # to disconnect now.
*0# to get back to the Main Menu

8
text/tex/99b.ans Normal file
View File

@ -0,0 +1,8 @@
 Ú¿ ÚÄÜÚÄÜÚÄÜ ß ÚÂÜÚÄÜÚ Ü 
¿ڿڿڴÿ¿Ú¿ ³ßÛ³ Û.\ÜÞÛ  ³ Ã_ÜÚßÜ 
³³³³³³³³³³³³ÃÙ ÄÄÄÄÄÄÄÄÄÄÄÄÜÜÜÜÜÜÜÜÜÜ
Á´ÀÙÀÙÀÙÀÙÀ´ÁÙ node: Xnodeid;-11\ ÄÙ ÄÙ date: XDATE:%Y-%b-%d;-11\ time: XTIME;-11\
Thanks for visiting ANSItex XREALNAME;20\
Hope to see you again...

1
text/vtx/1a.bin Normal file
View File

@ -0,0 +1 @@
7ccks3w{#35 ,l|l|,,7k#37k#55jjj 45j 55 kj/|5ho <=$,5uzzzpquzpu5 /./*,//upzpquzp5 Videotex *0_To get back here DATE........... NODE ........$0$0$0$0$0$0$0$0$0$0$0$0$0$0$0$0$0$0$0$ 1Fido Message Networks 8Navigating AnsiTEX 9About AnsiTEX *95_ Help *99_ Log off 

1
text/vtx/980a.bin Normal file
View File

@ -0,0 +1 @@
,l|l|,,7k#37k#5 kj/|5ho <=$,5 /./*,//upzpquzp5 Videotex Welcome Welcome, you have connected to ANSItex a BBS that is based on the 1980's Videotex service. Depending on which country you live in the Videotex service was called Viatel (AU), Prestel (UK), Minitel (FR), Telidon (CA), Ibertex (SP), etc If you got here by mistake, you should disconnect now, otherwise you can press0to get to the login screen. To login using SQRL, press1

1
text/vtx/981a.bin Normal file
View File

@ -0,0 +1 @@
,l|l|,,7k#37k#5 ........kj/|5ho <=$,5 /./*,//upzpquzp5 Videotex Register ** to clear input *00to start again Use_to Enter !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Email :eeeeeeeeeeeeeeeeeeeeeeeee User ID :nnnnnnnnnnnnnnn Password :ppppppppppppppp Full Name:fffffffffffffffffffffffff Token :zzzzzz 000000000000000000000000000000000000 Registering and using this system, you agree to abide by the system rules. See*988_

1
text/vtx/981b.bin Normal file
View File

@ -0,0 +1 @@
,l|l|,,7k#37k#5 ........kj/|5ho <=$,5 /./*,//upzpquzp5 Videotex Welcome Welcome toANSItex, a bulletin board system based on the 1980's Videotex service. Your usage here is governed by some rules, which you can see on page*988_ If you are not familiar with Videotex, you can find some information on page *516_ To get to the home page anytime, use *0_or use0now.

1
text/vtx/983a.bin Normal file
View File

@ -0,0 +1 @@
,l|l|,,7k#37k#5 ........kj/|5ho <=$,5 /./*,//upzpquzp5 Videotex Login FAILED Press0to try again. If this keeps happening, you may like to request that your password is reset.

1
text/vtx/98a.bin Normal file
View File

@ -0,0 +1 @@
,l|l|,,7k#37k#5 ........kj/|5ho <=$,5 /./*,//upzpquzp5 Videotex Sign In User............... Pass............... EnterNEWto register new account Press**to clear your input Press*00to start again

1
text/vtx/98b.bin Normal file
View File

@ -0,0 +1 @@
,l|l|,,7k#37k#5 ........kj/|5ho <=$,5 /./*,//upzpquzp5 Videotex Sign In Welcome toANSItex. To get to the main menu, you can press*0_any time. Press0to continue

BIN
text/vtx/999a.bin Normal file

Binary file not shown.

BIN
text/vtx/999b.bin Normal file

Binary file not shown.

1
text/vtx/99a.bin Normal file
View File

@ -0,0 +1 @@
7+o/??/o ,l|l|,,7k#37k#55jj %5sz kj/|5ho <=$,5urzsqusz /./*,//upzpquzp5 Videotex You are about todisconnectfrom ANSItex. To head back to the main menu, please use*0_ Otherwise to disconnect press_ Thanks for calling

1
text/vtx/99b.bin Normal file
View File

@ -0,0 +1 @@
7+o/??/o ,l|l|,,7k#37k#55jj %5sz kj/|5ho <=$,5urzsqusz /./*,//upzpquzp5 Videotex Thanks for calling Hope to see you again.

37
tools/clear_tags.js Normal file
View File

@ -0,0 +1,37 @@
/** CLEAR ALL THE TAGS IN A MESSAGE BASE */
load('ansitex/load/msgbases.js');
if (argv.length !== 1) {
writeln('ERROR: Need a msgbase page prefix');
exit(1);
} else {
writeln('Showing frames in:'+argv[0]);
}
var ma = new MsgAreas();
var area = ma.getArea(argv[0]);
writeln('Opening ['+argv[0]+'] - ('+area.code+')');
var msgbase = area.msgbase;
msgbase.open();
writeln('- First:'+msgbase.first_msg);
writeln('- Last:'+msgbase.last_msg);
var msgs = msgbase.get_all_msg_headers(false,false) || [];
for (var x in msgs) {
if (! msgs[x].tags)
continue;
writeln('TAG was:'+msgs[x].tags);
msgs[x].tags = '';
if (! msgbase.put_msg_header(msgs[x].number,msgs[x]))
writeln('ERROR:'+msgbase.error);
}
msgbase.close();

243
tools/export.js Normal file
View File

@ -0,0 +1,243 @@
load('smbdefs.js');
// ANSItex specific includes
load('ansitex/load/defs.js');
load('ansitex/load/funcs.js');
/**
* Work out which key should be used for a page, or validate if a key used is valid
*
* @param page
* @param signed
* @returns {*}
*/
function getKey(page,signed) {
log(LOG_DEBUG,'+ CHECKING key:'+signed+' for page: '+page);
var f = new File(file_cfgname(system.mods_dir,'ansitex/ctrl/videotex.ini'));
var match = '';
if (f.open("r")) {
var BreakException = {};
// Default Key
var key = f.iniGetValue('prefix','key');
try {
f.iniGetSections("prefix:").forEach(function (prefix) {
var p = prefix.substr(7);
var pk = f.iniGetValue(prefix, 'Key', '');
var re = new RegExp('^' + p, 'g');
// If it was signed, is the key a value one
if (signed && signed.length) {
if (pk === signed) {
//print('SIGNED key is valid:' + signed);
key = signed;
throw BreakException;
}
// Which key should sign this page
} else if (page.toString().match(re) && (p.length > match.length)) {
match = p;
key = pk;
}
});
} catch (e) {
if (e !== BreakException) throw e;
}
}
f.close();
log(LOG_DEBUG,'- key:'+key);
return key;
}
/**
* Export message from message base
*
* @param msgbase
*/
function msgBaseExport(msgbase) {
var ini = new File(msgbase.file+'.ini');
var i=0;
if (ini.open("r")) {
export_ptr=ini.iniGetValue('videotex','export_ptr',0);
ini.close();
} else {
log(LOG_ERROR,'! ERROR: Unable to open INI for ['+msgbase.file+']');
}
// If pointer doesnt exist, reset it from the message base.
if (export_ptr === undefined) {
var f = new File(file_getcase(msgbase.file+'.sbl'));
if (f.open('rb')) {
export_ptr = f.readBin(4);
f.close();
}
}
highest = export_ptr;
var total_msgs = msgbase.total_msgs;
log(LOG_DEBUG,'| msgBaseExport: export_ptr='+export_ptr+'|last_msg='+msgbase.last_msg+'|total_msgs='+total_msgs);
if (msgbase.last_msg >= export_ptr)
i = total_msgs-(msgbase.last_msg-export_ptr);
for (; i<total_msgs; i++) {
if (js.terminated)
break;
var idx = msgbase.get_msg_index(true,i);
if (! idx) {
log(LOG_ERROR,'! ERROR: Reading index of msg offset ['+i+'] : ['+msgbase.error+']');
continue;
}
// Skip message until we get to our last one read.
if (idx.number <= export_ptr)
continue;
if (idx.number > highest)
highest = idx.number;
// Only check messages from Videotex
if (idx.from !== crc16_calc('videotex')) {
log(LOG_DEBUG,'! IGNORING: Message offset ['+i+'] in ['+msgbase.file+']');
continue;
}
// Get the message header
var hdr = msgbase.get_msg_header(true,i);
// Ignore deleted messages
if (hdr.attr&MSG_DELETE) {
log(LOG_DEBUG,'! IGNORING: Deleted message offset ['+i+'] in ['+msgbase.file+']');
continue;
}
// Ignore locally posted messages
if (! hdr.from_net_type) {
log(LOG_DEBUG,'! IGNORING: Local message offset ['+i+'] in ['+msgbase.file+']');
continue;
}
log(LOG_DEBUG,'+ PROCESSING: Message offset ['+i+'] in ['+msgbase.file+'] from ['+hdr.from_net_addr+'] ('+hdr.subject+')');
var body = msgbase.get_msg_body(
/* by_offset: */true,
i,
/* strip Ctrl-A */true,
/* rfc822-encoded: */false,
/* include tails: */false);
t = new File(system.temp_dir+'videotex.gpg');
if (! t.open('w+')) {
log(LOG_ERROR,'! ERROR: Unable to open temp file ['+system.temp_dir+'videotex.gpg'+']');
exit(1);
}
try {
t.write(base64_decode(body));
t.close();
} catch (error) {
log(LOG_ERROR,error);
exit(1);
}
// Check if the signature is good
result = system.exec('gpgv --keyring '+system.mods_dir+'ansitex/keys/pubring.kbx '+system.temp_dir+'videotex.gpg');
if (result !== 0 ) {
log(LOG_ERROR,'! ERROR: Invalid Signature for message offset ['+i+'] in ['+msgbase.file+']');
continue;
}
if (file_exists(system.temp_dir+'videotex.tex'))
file_remove(system.temp_dir+'videotex.tex')
// Check that the signature is allowed to author the frames
result = system.exec('gpg --homedir '+system.mods_dir+'ansitex/keys --batch --status-fd 2 -o '+system.temp_dir+'videotex.tex '+system.temp_dir+'videotex.gpg 2>'+system.temp_dir+'videotex.log');
if (result !== 0 ) {
log(LOG_ERROR,'! ERROR: Failed to extract message offset ['+i+'] in ['+msgbase.file+']');
continue;
}
// Put frame in place.
f = new File(system.temp_dir+'videotex.tex');
if (! f.exists || ! f.open('r')) {
log(LOG_ERROR,'! ERROR: Failed to open frame for message offset ['+i+'] in ['+msgbase.file+']');
exit(1);
}
try {
frame = JSON.parse(f.read());
x = new Frame(0);
frame.render = x.render;
// @todo Figure out how to delete this duplicate code
Object.defineProperty(frame,'page', {
get: function() {return this.frame+this.index}
});
x = null;
} catch (error) {
log(LOG_ERROR,error);
continue;
}
// Get the key that signed the message
f = new File(system.temp_dir+'videotex.log');
if (! f.exists || ! f.open('r')) {
log(LOG_ERROR,'! ERROR: Failed to open gpg log for message offset ['+i+'] in ['+msgbase.file+']');
continue;
}
var signed = '';
while (string = f.readln()) {
var matches = string.match(/\s+GOODSIG\s+.*\<(.*)\>/);
if (matches) {
signed = matches[1];
}
}
f.close();
if (signed !== getKey(frame.frame,signed)) {
log(LOG_ERROR,'! ERROR: Key ['+signed+' is not authorised for message offset ['+i+'] in ['+msgbase.file+']');
continue;
}
// Save the frame.
log(LOG_INFO,'Updating page ['+frame.page+'] from ['+hdr.from_net_addr+'] signed with ['+signed+']');
saveFrame(frame);
}
if (ini.open(file_exists(ini.name) ? 'r+':'w+')) {
ini.iniSetValue('videotex','export_ptr',highest);
ini.close();
} else {
log(LOG_ERROR,'! ERROR: Unable to save to INI for ['+msgbase.file+']');
}
}
var msgbase = new MsgBase(findMsgBase(null).code);
log(LOG_DEBUG,'+ ANSITEX_EXPORT: Open message base in ['+msgbase.file+']');
if (! msgbase.open()) {
log(LOG_ERROR,'! ERROR: Unable to open ['+msgbase.file+']');
exit(-1);
}
msgBaseExport(msgbase);
msgbase.close();

49
tools/frame_get.js Normal file
View File

@ -0,0 +1,49 @@
load('ansitex/load/page.js');
load('ansitex/load/funcs.js');
load('lz-string.js');
if ((argv.length > 2) || (argv.length < 1)) {
writeln('ERROR: Need a frame ID and type');
exit(1);
} else {
writeln('Showing frame:'+argv[0]);
}
var SESSION_EXT;
switch (argv[1]) {
case 'TEX':
case 'tex':
SESSION_EXT = 'tex';
load('ansitex/load/session/ansitex.js');
break;
case 'VTX':
case 'vtx':
default:
load('ansitex/load/session/viewdata.js');
SESSION_EXT = 'vtx';
break;
}
var p = new Page();
var po = new PageObject(argv[0]);
if (p.get(po)) {
writeln('Page : '+p.name);
writeln('- Cost : '+p.cost);
writeln('- Owner : '+p.owner);
writeln('- Key : '+p.key);
writeln('- Type : '+p.type);
writeln('- CUG : '+p.cug);
writeln('- isPublic: '+p.isPublic);
writeln('- isAccess: '+p.accessible);
} else {
writeln('Failed to load:'+argv[0]);
}
writeln('----');
for (var x in p.raw)
writeln(x+':'+JSON.stringify(p.raw[x])+"\r\n");

35
tools/frame_import.js Normal file
View File

@ -0,0 +1,35 @@
/**
* Load a frame, and store it in the message base
*/
load('ansitex/load/page.js');
load('ansitex/load/funcs.js');
load('lz-string.js');
/* parse command arguments */
if (argv.length !== 1) {
writeln('ERROR: Need a frame ID');
exit(1);
} else {
writeln('Importing frame:'+argv[0]);
}
var po = new PageObject(argv[0]);
// Load ANSItex frame
var SESSION_EXT = 'tex';
require('ansitex/load/session/ansitex.js','SESSION_ANSITEX');
var p = new Page();
p.get(po);
// Load Viewdata frame
var SESSION_EXT = 'vtx';
require('ansitex/load/session/viewdata.js','SESSION_VIEWDATA');
var vtx = p.file_content(FRAMES_HOME+SESSION_EXT+'/'+po.toString()+'.'+CONTENT_EXT);
if (vtx !== undefined)
p.raw = vtx;
p.save();

26
tools/frames_list.js Normal file
View File

@ -0,0 +1,26 @@
load('ansitex/load/funcs.js');
load('ansitex/load/msgbases.js');
if (argv.length !== 1) {
writeln('ERROR: Need a msgbase page prefix');
exit(1);
} else {
writeln('Showing frames in:'+argv[0]);
}
var ma = new MsgAreas();
var area = ma.getArea(argv[0]);
writeln('Opening ['+argv[0]+'] - ('+area.code+')');
var msgbase = area.msgbase;
msgbase.open();
writeln('- First:'+msgbase.first_msg);
writeln('- Last:'+msgbase.last_msg);
writeln('- Next:'+area.page_next);
msgbase.close();
var frames = area.frames;
for (var x in frames)
writeln(padright(frames[x].number,4,' ')+':'+frames[x].tags);

26
tools/frames_tag.js Normal file
View File

@ -0,0 +1,26 @@
load('ansitex/load/msgbases.js');
/**
* Go through our messages and tag a frame id for messages without one.
* @note: May need to run jsexec with -m 32MB to overcome memory issues
*/
const ma = new MsgAreas()
var areas = ma.managed;
for (var i=0;i<areas.length;i++) {
if (argv[0] && ((argv[0] !== areas[i].code) && (argv[0] !== areas[i].page_prefix)))
continue;
var total = areas[i].total_msgs;
var frames = areas[i].frames.length;
writeln('Area : '+areas[i].full_name);
writeln('- Total Messages : '+total);
writeln('- Page Prefix : '+areas[i].page_prefix);
writeln('- Tagged Messages : '+frames);
writeln('- Untagged Messages: '+(total-frames));
writeln('- Next Tag : '+areas[i].page_next);
areas[i].tag_msgs();
}

5
tools/msgbases_list.js Normal file
View File

@ -0,0 +1,5 @@
load('ansitex/load/msgbases.js');
/* Show a list of our message bases */
var ma = new MsgAreas();
ma.list;

119
tools/save.js Normal file
View File

@ -0,0 +1,119 @@
// ANSItex specific includes
load('ansitex/load/defs.js');
load('ansitex/load/funcs.js');
var options = loadOptions();
// Modes of operation:
// -s -p x -i y = To send a stored page via the msgbase
// {-s} -p x -i y -f file = To update (and optionally send) a page with new content
// -all details = to create a new page
// Import
send = (argv.indexOf('-s') >= 0);
// Page
p = getArg('-p','No page specified with -p',true);
// Index
i = getArg('-i','No index specified with -i',true);
// File to convert
file = getArg('-f','No file specified with -f',false);
frame = new TexFrame();
frame.load(pageStr({frame: p,index: i}));
if (! send || ! frame || file) {
if (frame.page === null) {
frame = new TexFrame();
frame.frame = p;
frame.index = i;
}
// Key
key = getArg('-k','No index specified with -k',false);
// Cost
cost = getArg('-c','No index specified with -c',false);
// Owner
owner = getArg('-o','No owner specified with -o',false);
// Owner
type = getArg('-t','No type specified with -t',false);
if (file) {
f = new File(file);
if (! f.exists || ! f.open('r')) {
log(LOG_ERROR,'! ERROR: Unable to open ['+file+']');
exit(1);
}
frame.content = base64_encode(f.read());
f.close();
}
if (owner) {
frame.owner = base64_encode(owner.replace(/\\1/g,"\1"));
}
if (key) {
frame.key = key.split(',').map(function(t){return parseInt(t)});
if (frame.key.length !== 10) {
log(LOG_ERROR,'! ERROR: Must specify 10 keys with -k');
exit(1);
}
}
// Public
if (argv.indexOf('-P') >= 0)
frame.isPublic = true;
if (argv.indexOf('-A') >= 0)
frame.isAccessible = true;
if (cost)
frame.cost = cost;
if (type)
frame.type = type; // @todo validate this is a valid type.
// Date
frame.date = new Date().toISOString();
// Final validation
if (! frame.owner) {
log(LOG_ERROR,'! ERROR: No owner specified ['+file+']');
exit(1);
}
// Store the frame in file
frame.save();
}
// @NOTE: We need to use a binary signature then base64 encode it, as mailers may strip 0x0a while messages are in transit.
if (send === 1 && options.gpg_key) {
if (! file) {
file = system.mods_dir+'ansitex/text/'+frame.page+'.tex';
}
if (! file_exists(file)) {
log(LOG_ERROR,'! ERROR: File '+file+' doesnt exist?');
exit(1);
}
if (file_exists(file+'.asc'))
file_remove(file+'.asc')
result = system.exec('gpg --homedir '+system.mods_dir+'ansitex/keys --clearsign --batch --local-user '+options.gpg_key+' -s '+file);
w = new File(file+'.asc');
if (w.open('r')) {
msg = base64_encode(w.read());
} else {
log(LOG_ERROR,'! ERROR: Unable to send with GPG for '+frame.page + ' Error: '+w.error);
exit(1);
}
w.close();
msgBaseImport(null,frame.page,msg);
printf('GPG Result: %s',result);
}