diff --git a/xtrn/wttr.in/locator.js b/xtrn/wttr.in/locator.js new file mode 100644 index 0000000000000000000000000000000000000000..7dc236e7ab4ac6aec9dc2aacecbf33011df61d0f --- /dev/null +++ b/xtrn/wttr.in/locator.js @@ -0,0 +1,99 @@ +/* + * Just IP address lookup for now + * wttr.in does the geoip for us (albeit inaccurately) + * To do: do something with user.location / zipcode as an alternative? + * + * IP address lookup stuff adapted from syncWXremix + * (Contributed by echicken) + * https://github.com/KenDB3/syncWXremix + * Copyright (c) 2015, Kenny DiBattista <kendb3@bbs.kd3.us> + * + * ISC License + * + * Copyright (c) 2022 Zaidhaan Hussain + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +// To do: I think this is only good for IPv4 +function wstsGetIPAddress() { + const ip = []; + const data = []; + + try { + console.lock_input(true); + console.telnet_cmd(253, 28); // DO TTYLOC + + const stime = system.timer; + while (system.timer - stime < 1) { + if (!client.socket.data_waiting) continue; + data.push(client.socket.recvBin(1)); + if (data.length >= 14 && data[data.length - 3] !== 255 && data[data.length - 2] === 255 && data[data.length - 1] === 240) break; + } + + // Check for a valid reply + if (data.length < 14 || // Minimum response length + // Should start like this + data[0] !== 255 || // IAC + data[1] !== 250 || // SB + data[2] !== 28 || // TTYLOC + data[3] !== 0 || // FORMAT + // Should end like this + data[data.length - 2] !== 255 || // IAC + data[data.length - 1] !== 240 // SE + ) { + throw 'Invalid reply to TTYLOC command.'; + } + + for (var d = 4; d < data.length - 2; d++) { + ip.push(data[d]); + if (data[d] === 255) d++; + } + if (ip.length !== 8) throw 'Invalid reply to TTYLOC command.'; + } catch (err) { + log(LOG_DEBUG, err); + } finally { + console.lock_input(false); + if (ip.length !== 8) return; + return ip.slice(0, 4).join('.'); + } +} + +// for webv4 ... I think +function wsrsGetIPAddress() { + var fn = format('%suser/%04d.web', system.data_dir, user.number); + if (!file_exists(fn)) return; + var f = new File(fn); + if (!f.open('r')) return; + var session = f.iniGetObject(); + f.close(); + if (typeof session.ip_address === 'undefined') return; + return session.ip_address; +} + +function getAddress() { + const addrRe = /^(127\.)|(192\.168\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(169\.254\.)|(::1$)|([fF][cCdD])/; + if (user.ip_address.search(addrRe) > -1) { + var addr; + if (client.protocol === 'Telnet') { + addr = wstsGetIPAddress(); + } else if (bbs.sys_status&SS_RLOGIN) { + addr = wsrsGetIPAddress(); + } + if (addr === undefined || addr.search(addrRe) > -1) return; + return addr; + } + return user.ip_address; +} + +this; \ No newline at end of file diff --git a/xtrn/wttr.in/readme.txt b/xtrn/wttr.in/readme.txt new file mode 100644 index 0000000000000000000000000000000000000000..d57c59beaf53909050995bc5d7a76810f070a65d --- /dev/null +++ b/xtrn/wttr.in/readme.txt @@ -0,0 +1,46 @@ +wttr.in viewer for Synchronet BBS +by echicken -at- bbs.electronicchicken.com + +Contents + + 1) About + 2) Installation + 3) Customization + +1) About + + This script pulls a weather report from https://wttr.in and displays it in + the terminal. wttr.in is a console-oriented weather forecast service + created and hosted by Igor Chubin (https://github.com/chubin/wttr.in). + +2) Installation + + In SCFG, go to 'External Programs', then 'Online Programs (Doors)', and + choose the section to which you'd like to add this mod. Fill out the new + entry with the following details: + + Name wttr.in Weather Forecast + Internal Code WTTR + Start-up Directory /sbbs/xtrn/wttr.in + Command Line ?wttr.js + Multiple Concurrent Users Yes + + If you want this mod to run during your logon process, set the following: + + Execute on Event logon + + All other options can be left at their default settings. + +3) Customization + + Please see https://wttr.in/:help for a list of options. You may pass any + of the 'Units' and 'View options' values to this script on the command + line to customize the output, for example: + + Command Line ?wttr.js m0AFn + + The default is 'AFn' for ANSI, no 'Follow' line, and narrow output. + + Note that your command line argument will completely replace the default + parameters. You will probably want to specify 'A' and 'n' in addition to + your chosen values. diff --git a/xtrn/wttr.in/wttr.js b/xtrn/wttr.in/wttr.js new file mode 100644 index 0000000000000000000000000000000000000000..aca8871abd4adf132d08ca4d626fd7217b6138ad --- /dev/null +++ b/xtrn/wttr.in/wttr.js @@ -0,0 +1,35 @@ +require('sbbsdefs.js', 'P_UTF8'); +require('http.js', 'HTTPRequest'); +const xterm = load({}, js.exec_dir + 'xterm-colors.js'); +const locator = load({}, js.exec_dir + 'locator.js'); + +function uReplace(str) { + return str.replace(/\xE2\x9A\xA1/g, '/ '); // U+26A1 Lightning bolt +} + +function fetchWeather(addr) { + const qs = argc > 0 ? argv.join('') : 'AFn'; + const http = new HTTPRequest(); + if (addr !== undefined) http.extra_headers = { 'X-Forwarded-For': addr }; + const body = http.Get('https://wttr.in/?' + qs); + if (http.response_code !== 200) throw new Error('wttr.in response had status ' + http.response_code); + return body; +} + +function main() { + const addr = locator.getAddress(); + const weather = fetchWeather(addr); + const text = uReplace(weather); + const ansi = xterm.convertColors(text); + const attr = console.attributes; + console.clear(BG_BLACK|LIGHTGRAY); + console.putmsg(ansi, P_UTF8); + console.pause(); + console.attributes = attr; +} + +try { + main(); +} catch (err) { + log(LOG_ERROR, err); +} \ No newline at end of file diff --git a/xtrn/wttr.in/xterm-colors.js b/xtrn/wttr.in/xterm-colors.js new file mode 100644 index 0000000000000000000000000000000000000000..ac0b7ba9e0c944e5ab10363808044abd9192a720 --- /dev/null +++ b/xtrn/wttr.in/xterm-colors.js @@ -0,0 +1,116 @@ +/* + * Adapted from + * https://github.com/zaidhaan/xterm2ansi + * Copyright (c) 2022 Zaidhaan Hussain + * + * ISC License + * + * Copyright (c) 2022 Zaidhaan Hussain + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +const colors = [ + { r: 0x00, g: 0x00, b: 0x00, code: 30, bold: 0 }, // black + { r: 0xCD, g: 0x00, b: 0x00, code: 31, bold: 0 }, // red + { r: 0x00, g: 0xCD, b: 0x00, code: 32, bold: 0 }, // green + { r: 0xCD, g: 0xCD, b: 0x00, code: 33, bold: 0 }, // yellow + { r: 0x00, g: 0x00, b: 0xEE, code: 34, bold: 0 }, // blue + { r: 0xCD, g: 0x00, b: 0xCD, code: 35, bold: 0 }, // magenta + { r: 0x00, g: 0xCD, b: 0xCD, code: 36, bold: 0 }, // cyan + { r: 0xE5, g: 0xE5, b: 0xE5, code: 37, bold: 0 }, // white + { r: 0x7F, g: 0x7F, b: 0x7F, code: 30, bold: 1 }, // bright black + { r: 0xFF, g: 0x00, b: 0x00, code: 31, bold: 1 }, // bright red + { r: 0x00, g: 0xFF, b: 0x00, code: 32, bold: 1 }, // bright green + { r: 0xFF, g: 0xFF, b: 0x00, code: 33, bold: 1 }, // bright yellow + { r: 0x5C, g: 0x5C, b: 0xFF, code: 34, bold: 1 }, // bright blue + { r: 0xFF, g: 0x00, b: 0xFF, code: 35, bold: 1 }, // bright magenta + { r: 0x00, g: 0xFF, b: 0xFF, code: 36, bold: 1 }, // bright cyan + { r: 0xFF, g: 0xFF, b: 0xFF, code: 37, bold: 1 }, // bright white +]; + +function colorDistance(r1, g1, b1, r2, g2, b2) { + return Math.sqrt(Math.pow(r2 - r1, 2) + Math.pow(b2 - b1, 2) + Math.pow(g2 - g1, 2)); +} + +function rgbToANSI(rgb) { + var nearest = 0; + var minDistance = 256; + for (var i = 0; i < 16; i++) { + var currentColor = colors[i]; + var distance = colorDistance(rgb.r, rgb.g, rgb.b, currentColor.r, currentColor.g, currentColor.b); + if (distance < minDistance) { + minDistance = distance; + nearest = currentColor; + } + } + return nearest; +} + +function xterm256ToRGB(color) { + var r = 0; + var g = 0; + var b = 0; + if (color < 16) { + r = ansi_colors[color].r; + g = ansi_colors[color].g; + b = ansi_colors[color].b; + } else if (color < 232) { + color -= 16; + const _r = (color / 36); + const _g = (color % 36) / 6; + const _b = (color % 6); + r = _r ? _r * 40 + 55 : 0; + g = _g ? _g * 40 + 55 : 0; + b = _b ? _b * 40 + 55 : 0; + } else { + color -= 232; + r = g = b = (color * 10) + 8; + } + return { r: r, g: g, b: b }; +} + +function xterm256ToANSI(color) { + const rgb = xterm256ToRGB(color); + return rgbToANSI(rgb); +} + +function replace256(match, p1, p2, p3) { + const color = parseInt(p2, 10); + if (isNaN(color)) return ''; + const ansiColor = xterm256ToANSI(color); + const code = (p1 === '48') ? (ansiColor.code + 10) : ansiColor.code; + return '\x1b[' + ansiColor.bold + ';' + code + p3 + 'm'; +} + +function replaceRGB(match, p1, p2, p3, p4, p5) { + const r = parseInt(p2, 10); + if (isNaN(r)) return ''; + const g = parseInt(p3, 10); + if (isNaN(g)) return ''; + const b = parseInt(p4, 10); + if (isNaN(b)) return ''; + const ansiColor = rgbToANSI({ r: r, g: g, b:b }); + const code = (p1 === '48') ? (ansiColor.code + 10): ansiColor.code; + return '\x1b[' + ansiColor.bold + ';' + code + p5 + 'm'; +} + +function convertColors(str) { + return str.replace( + /\x1b\[([34]8);5;(\d+)(;\d+)?m/g, replace256 + ).replace( + /\x1b\[([34]8);2;(\d+);(\d+);(\d+)(;\d+)?m/g, replaceRGB + ); +} + +this; \ No newline at end of file