diff --git a/exec/sbbslist.js b/exec/sbbslist.js new file mode 100644 index 0000000000000000000000000000000000000000..59cdf0ef7255d7cae15115cd9666a78365081430 --- /dev/null +++ b/exec/sbbslist.js @@ -0,0 +1,590 @@ +// $Id$ + +// Synchronet BBS List + +// This one script replaces (or *will* replace) the functionality of: +// sbl[.exe] - External online program (door) +// smb2sbl[.exe] - Imports BBS entries from Synchronet Message Base (e.g. from SYNCDATA echo) +// sbl2smb[.exe] - Exports BBS entries to Synchronet Message Base (e.g. to SYNCDATA echo) +// sbbslist[.exe] - Exports BBS entries to HTML and various plain-text formats (e.g. sbbs.lst, sbbsimsg.lst, syncterm.lst) + +var REVISION = "$Revision$".split(' ')[1]; +var sbl_dir = "../xtrn/sbl/"; + +var options={sub:"syncdata"}; +opts=load(new Object, "modopts.js", "sbbslist"); +if(this.opts && opts.sub) + options.sub=opts.sub; + +var lib = {}; +load(lib, "sbbslist_lib.js"); + +var sort_property = 'name'; + +function compare(a, b) +{ + if(a[sort_property].toLowerCase()>b[sort_property].toLowerCase()) return 1; + if(a[sort_property].toLowerCase()<b[sort_property].toLowerCase()) return -1; + return 0; +} + +// This date format is required for backwards compatibility with SMB2SBL +function date_to_str(date) +{ + return format("%02u/%02u/%02u", date.getUTCMonth()+1, date.getUTCDate(), date.getUTCFullYear()%100); +} + +function date_from_str(str) +{ + return new Date(parseInt(str.substr(6,2),10), parseInt(str.substr(0,2),10), parseInt(str.substr(3,2),10)); +} + +function export_entry(bbs, msgbase) +{ + var i; + var hdr = { to:'sbl', from:bbs.entry.created.by, subject:bbs.name }; + + var body = ""; // This section for SMB2SBL compatibility + body += "Name: " + bbs.name + "\r\n"; + body += "Birth: " + date_to_str(new Date(bbs.first_online)) + "\r\n"; + if(bbs.software.bbs) + body += "Software: " + bbs.software.bbs + "\r\n"; + for(i in bbs.sysop) { + body += "Sysop: " + bbs.sysop[i].name + "\r\n"; + if(i==0 && bbs.sysop[i].email) + body += "e-mail: " + bbs.sysop[i].email + "\r\n"; + } + if(bbs.web_site) + body += "Web-site: " + bbs.web_site + "\r\n"; + if(bbs.location) + body += "Location: " + bbs.location + "\r\n"; + for(i in bbs.service) { + switch(bbs.service[i].protocol) { + case 'modem': + body += "Number: " + bbs.service[i].address + "\r\n"; + body += "MinRate: " + bbs.service[i].min_rate + "\r\n"; + body += "MaxRate: " + bbs.service[i].max_rate + "\r\n"; + if(bbs.service[i].description) + body += "Modem: " + bbs.service[i].description + "\r\n"; + break; + case 'telnet': + body += "Telnet: " + bbs.service[i].address + "\r\n"; + body += "Port: " + bbs.service[i].port + "\r\n"; + break; + } + } + for(i in bbs.network) { + body += "Network: " + bbs.network[i].name + "\r\n"; + body += "Address: " + bbs.network[i].address + "\r\n"; + } + for(i in bbs.terminal.support) + body += "Terminal: " + bbs.terminal.support[i].type + "\r\n"; + if(bbs.terminal.nodes) + body += "Nodes: " + bbs.terminal.nodes + "\r\n"; + if(bbs.total.storage) + body += "Megs: " + bbs.total.storage / (1024*1024) + "\r\n"; + if(bbs.total.msgs) + body += "Msgs: " + bbs.total.msgs + "\r\n"; + if(bbs.total.files) + body += "Files: " + bbs.total.files + "\r\n"; + if(bbs.total.users) + body += "Users: " + bbs.total.users + "\r\n"; + if(bbs.total.subs) + body += "Subs: " + bbs.total.subs + "\r\n"; + if(bbs.total.dirs) + body += "Dirs: " + bbs.total.dirs + "\r\n"; + if(bbs.total.xtrns) + body += "Xtrns: " + bbs.total.xtrns + "\r\n"; + for(i in bbs.description) + body += "Desc: " + bbs.description[i] + "\r\n"; + + body += "\r\n<json>\r\n"; + body += JSON.stringify(bbs, null, 1) + "\r\n"; + body += "</json>\r\n"; + body += "--- " + js.exec_file + " " + REVISION + "\r\n"; +// print(body); + + return msgbase.save_msg(hdr, body); +} + +function export_to_msgbase(list, msgbase) +{ + var i; + var count=0; + var last_export; + + var ini = new File(msgbase.file + ".ini"); + print("Opening " + ini.name); + if(ini.open("r")) { + last_export=ini.iniGetValue("sbbslist","last_export", new Date(0)); + print("last export = " + last_export); + ini.close(); + } else + print("Error " + ini.error + " opening " + ini.name); + /* Fallback to using old SBL export pointer (time_t) storage file/format */ + if(!last_export) { + var f = new File(sbl_dir + "sbl2smb.dab"); + print("Opening " + f.name); + if(f.open("rb")) { + last_export = new Date(f.readBin(4)*1000); + f.close(); + } else + print("Error " + f.error + " opening " + f.name); + } + print("Exporting entries created/modified/verified since " + last_export.toString()); + for(i in list) { + if(js.terminated) + break; + if(list[i].imported) + continue; + if(list[i].entry.created.on < last_export + && list[i].entry.updated.on < last_export + && list[i].entry.verified.on < last_export) + continue; + if(!export_entry(list[i], msgbase)) + break; + count++; + } + print("Exported " + count + " entries"); + print("Opening " + ini.name); + if(ini.open(file_exists(ini.name) ? 'r+':'w+')) { + ini.iniSetValue("sbbslist","last_export",new Date()); + ini.close(); + } else + print("Error " + ini.error + " opening " + ini.name); +} + +function import_entry(bbs, text) +{ + var i; + var json_begin; + var json_end; + + text=text.split("\r\n"); + for(i=0; i<text.length; i++) { + if(text[i]=="---" || text[i].substring(0,4)=="--- ") + break; + } + text.length=i; + + for(i=0; i<text.length; i++) { + if(text[i].toLowerCase()=="<json>") + json_begin=i+1; + else if(text[i].toLowerCase()=="</json>") + json_end=i; + } + if(json_begin && json_end > json_begin) { + text=text.splice(json_begin, json_end-json_begin); + + try { + if((bbs = JSON.parse(text.join(' '))) != undefined) + return bbs; + } catch(e) { + alert("Error " + e + " parsing JSON"); + } + return bbs; + } + + /* Parse the old SBL2SMB syntax: */ + + var sysop=0; + var network=0; + var terminal=0; + var desc=0; + var number=0; + + bbs.sysop = []; + bbs.network = []; + bbs.terminal = { nodes:0, support:[] }; + bbs.description = []; + bbs.service = []; + bbs.total = {}; + + for(i in text) { + //print(text[i]); + if(!text[i].length) + continue; + var match=text[i].match(/\s*([A-Z\-]+)\:\s*(.*)/i); + if(!match || match.length < 3) { + print("No match: " + text[i]); + continue; + } + //print(match[1] + " = " + match[2]); + switch(match[1].toLowerCase()) { + case 'birth': + bbs.first_online = date_from_str(match[2]); + break; + case 'software': + bbs.software = {bbs: match[2]}; + break; + case 'sysop': + if(bbs.sysop.length) + sysop++; + bbs.sysop[sysop] = { name: match[2] }; + break; + case 'e-mail': + bbs.sysop[0].email = match[2]; + break; + case 'web-site': + bbs.web_site = match[2]; + break; + case 'number': + if(bbs.service.length) + number++; + bbs.service[number] = {address: match[2], protocol: 'modem'}; + break; + case 'telnet': /* SBL2SMB never actually generated this line though SMB2SBL supported it */ + if(bbs.service.length) + number++; + bbs.service[number] = {address: match[2], protocol: 'telnet', port: 23}; + break; + case 'minrate': + var minrate = parseInt(match[2], 10); + if(minrate == 0xffff) { + bbs.service[number].protocol = 'telnet'; + bbs.service[number].port = 23; + } else + bbs.service[number].minrate = minrate; + break; + case 'maxrate': + if(bbs.service[number].protocol == 'telnet') + bbs.service[number].port = parseInt(match[2], 10); + else + bbs.service[number].maxrate = parseInt(match[2], 10); + break; + case 'location': + bbs.location = match[2]; + break; + case 'port': /* SBL2SMB never actually generated this line though SMB2SBL supported it */ + bbs.service[number].port = parseInt(match[2], 10); + break; + case 'network': + if(bbs.network.length) + network++; + bbs.network[network] = {name: match[2]}; + break; + case 'address': + bbs.network[network].address = match[2]; + break; + case 'terminal': + if(bbs.terminal.support.length) + terminal++; + bbs.terminal.support[terminal] = {type: match[2]}; + break; + case 'desc': + if(bbs.description.length) + desc++; + bbs.description[desc] = match[2]; + break; + case 'megs': + bbs.total.storage = parseInt(match[2], 10)*1024*1024; + break; + case 'msgs': + case 'files': + case 'users': + case 'subs': + case 'dirs': + case 'xtrns': + bbs.total[match[1].toLowerCase()] = parseInt(match[2], 10); + break; + case 'nodes': + bbs.terminal.nodes = parseInt(match[2], 10); + break; + } + } + return bbs; +} + +function import_from_msgbase(list, msgbase) +{ + var i; + var count=0; + var import_ptr; + var highest=0; + var sbl_crc=crc16_calc("sbl"); + + var ini = new File(msgbase.file + ".ini"); + print("Opening " + ini.name); + if(ini.open("r")) { + import_ptr=ini.iniGetValue("sbbslist","import_ptr", 0); + ini.close(); + } else + print("Error " + ini.error + " opening " + ini.name); + if(import_ptr==undefined) { + var f = new File(file_getcase(msgbase.file + ".sbl")); + if(f.open("rb")) { + import_ptr = f.readBin(4); + f.close(); + } + } + highest=import_ptr; + print("import_ptr = " + import_ptr); + for(i=0; i<msgbase.total_msgs; i++) { + if(js.terminated) + break; + //print(i); + var idx = msgbase.get_msg_index(/* by_offset: */true, i); + if(!idx) { + print("Error " + msgbase.error + " reading index of msg offset " + i); + continue; + } + if(idx.number <= import_ptr) + continue; + if(idx.to != sbl_crc) { + //print(idx.to + " != " + sbl_crc); + continue; + } + if(idx.number > highest) + highest = idx.number; + var hdr = msgbase.get_msg_header(/* by_offset: */true, i); + var l; + var msg_from = truncsp(hdr.from); + var bbs_name = truncsp(hdr.subject); +// print("Searching " + list.length + " entries for BBS: " + bbs_name); + for(l=0; l<list.length; l++) { + //print("Comparing " + list[l].name); + if(list[l].name.toLowerCase() == bbs_name.toLowerCase()) + break; + } +// print("l = " + l); + if(l==undefined) + l=0; + if(list.length && list[l]) { + if(!list[l].entry) + continue; + if(list[l].entry.created.by.toLowerCase() != msg_from.toLowerCase()) { + print(msg_from + " did not create entry: " + bbs_name + " (" + list[l].entry.created.by + " did)"); + continue; + } + if(list[l].imported == false) { + print(msg_from + " attempting to update/over-write local entry: " + bbs_name); + continue; + } + print("Updating existing entry: " + bbs_name + " (by " + list[l].entry.created.by + ")"); + } else { + print(msg_from + " creating new entry: " + bbs_name); + list[l] = {name: bbs_name, entry: {created: {by:msg_from, on:new Date() } } }; + } + var body = msgbase.get_msg_body(/* by_offset: */true, i + ,/* strip Ctrl-A */true, /* rfc822-encoded: */false, /* include tails: */false); + import_entry(list[l], body); + list[l].imported = true; + if(!list[l].birth) + list[l].birth=list[l].entry.created.on; + list[l].entry.updated= { on: new Date(), by:msg_from }; + count++; + } + + if(ini.open(file_exists(ini.name) ? 'r+':'w+')) { + print("new import_ptr = " + highest); + ini.iniSetValue("sbbslist","import_ptr",highest); + ini.close(); + } else + print("Error opening/creating " + ini.name); + print("Imported " + count + " entries"); + if(count) + return lib.write_list(list); +} + +// From sbldefs.h (Do not change, for backwards compatibility): +const sbl_defs = { + MAX_SYSOPS: 5, + MAX_NUMBERS: 20, + MAX_NETS: 10, + MAX_TERMS: 5, + DESC_LINES: 5 +}; + +// Reads a single BBS entry from SBL v3.x "sbl.dab" file +function read_dab_entry(f) +{ + // These sbl.dab magic numbers come from sbldefs.h (now deprecated) + var i; + var total; + var obj = { name: '', entry:{}, sysop:[], service:[], terminal:{}, network:[], description:[], total:{} }; + + obj.name = truncsp(f.read(26)); + if(f.eof) + return null; + obj.entry.created = { on:null, by:truncsp(f.read(26)) }; + obj.software = { bbs: truncsp(f.read(16)) }; + total = f.readBin(1); + for(i=0;i<sbl_defs.MAX_SYSOPS;i++) + obj.sysop[i] = { name: truncsp(f.read(26)) }; + obj.sysop.length = total; + total_numbers = f.readBin(1); + total = f.readBin(1); + for(i=0;i<sbl_defs.MAX_NETS;i++) { + obj.network[i] = {}; + obj.network[i].name = truncsp(f.read(16)); + } + for(i=0;i<sbl_defs.MAX_NETS;i++) + obj.network[i].address = truncsp(f.read(26)); + obj.network.length = total; + total = f.readBin(1); + obj.terminal.support = []; + for(i=0;i<sbl_defs.MAX_TERMS;i++) + obj.terminal.support[i] = { type: truncsp(f.read(16)) }; + obj.terminal.support.length = total; + for(i=0;i<sbl_defs.DESC_LINES;i++) + obj.description.push(truncsp(f.read(51))); + while(--i) { + if(obj.description[i].length) + break; + } + obj.description.length=i+1; + obj.terminal.nodes = f.readBin(2); + obj.total.users = f.readBin(2); + obj.total.subs = f.readBin(2); + obj.total.dirs = f.readBin(2); + obj.total.xtrns = f.readBin(2); + obj.entry.created.on = new Date(f.readBin(4)*1000); + var updated = f.readBin(4); + if(updated) + obj.entry.updated = { on: new Date(updated*1000) }; + obj.first_online = new Date(f.readBin(4)*1000); + obj.total.storage = f.readBin(4)*1024*1024; + obj.total.msgs = f.readBin(4); + obj.total.files = f.readBin(4); + obj.imported = Boolean(f.readBin(4)); + for(i=0;i<sbl_defs.MAX_NUMBERS;i++) { + obj.service[i] = { address: truncsp(f.read(29)), protocol: 'modem' }; + var location = truncsp(f.read(31)); + var min_rate = f.readBin(2) + var port = f.readBin(2); + if(min_rate==0xffff) { + obj.service[i].protocol = 'telnet'; + obj.service[i].port = port; + } else { + obj.service[i].min_rate = min_rate; + obj.service[i].max_rate = port; + } + if(obj.location==undefined || obj.location.length==0) + obj.location=location; + } + obj.service.length = total_numbers; + var updated_by = truncsp(f.read(26)); + if(obj.entry.updated) + obj.entry.updated.by = updated_by; + obj.entry.verified = { on: new Date(f.readBin(4)*1000), by: truncsp(f.read(26)) }; + obj.web_site = truncsp(f.read(61)); + var sysop_email = truncsp(f.read(61)); + if(obj.sysop.length) + obj.sysop[0].email = sysop_email; + f.readBin(4); // 'exported' not used, always zero + obj.entry.verification = { count: f.readBin(4), attempts: f.readBin(4) }; + f.read(310); // unused padding + + return obj; +} + +// Upgrades from SBL v3.x (native/binary-data) to v4.x (JavaScript/JSON-data) +function upgrade_list(sbl_dab) +{ + var dab = new File(sbl_dab); + print("Upgrading from: " + sbl_dab); + if(!dab.open("rb")) { + alert("Error " + dab.error + " opening " + dab.name); + exit(); + } + + var list=[]; + + while(!dab.eof) { + if(js.terminated) + break; + var bbs = read_dab_entry(dab); + if(bbs==null || !bbs.name.length) + continue; +// print(bbs.name); + list.push(bbs); + } + dab.close(); + + lib.write_list(list); + + return list; +} + +function main() +{ + var i; + var list; + var import_now = false; + var export_now = false; + var show_now = false; + var dump_now = false; + var sort = false; + var msgbase; + + print("Synchronet BBS List v4 Rev " + REVISION); + + for(i in argv) { + switch(argv[i]) { + case "upgrade": + upgrade_list(sbl_dir + "sbl.dab"); + exit(); + case "import": + import_now = true; + break; + case "export": + export_now = true; + break; + case "dump": + dump_now = true; + case "show": + show_now = true; + break; + case "backup": + file_backup(lib.list_fname); + break; + case "-sort": + sort = true; + if(i+1<argc) + sort_property=argv[++i]; + break; + } + } + + if(!file_exists(lib.list_fname)) + list=upgrade_list(sbl_dir + "sbl.dab"); + else + list=lib.read_list(); + + if(import_now || export_now) { + msgbase = new MsgBase(options.sub); + print("Opening msgbase " + msgbase.file); + if(!msgbase.open()) { + alert("Error " + msgbase.error + " opening msgbase: " + msgbase.file); + exit(-1); + } + if(import_now) + import_from_msgbase(list, msgbase); + if(export_now) + export_to_msgbase(list, msgbase); + msgbase.close(); + } + + if(sort) { + print("Sorting list by property: " + sort_property); + list.sort(compare); + } + + if(show_now) { + for(i in list) { + if(dump_now) { + for(j in list[i]) + print(j + " : " + list[i][j]); + continue; + } + printf("%-25s %-25s %-25s", list[i].name, list[i].entry.created.by, list[i].sysop[0] ? list[i].sysop[0].name: ''); + if(list[i].entry.updated) + print(new Date(list[i].entry.updated.on).toISOString()); + else + print(); + } + print(list.length + " BBS entries"); + } +} + +main(); \ No newline at end of file