diff --git a/core/archive_util.js b/core/archive_util.js index 02a2e283..62f81059 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -2,9 +2,10 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; -const stringFormat = require('./string_format.js'); -const Errors = require('./enig_error.js').Errors; +const Config = require('./config.js').config; +const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; +const resolveMimeType = require('./mime_util.js').resolveMimeType; // base/modules const fs = require('fs'); @@ -19,9 +20,6 @@ class Archiver { this.decompress = config.decompress; this.list = config.list; this.extract = config.extract; - - /*this.sig = new Buffer(config.sig, 'hex'); - this.offset = config.offset || 0;*/ } ok() { @@ -76,39 +74,33 @@ module.exports = class ArchiveUtil { }); } - if(_.has(Config, 'archives.formats')) { - Object.keys(Config.archives.formats).forEach(fmtKey => { + if(_.isObject(Config.fileTypes)) { + Object.keys(Config.fileTypes).forEach(mimeType => { + const fileType = Config.fileTypes[mimeType]; + if(fileType.sig) { + fileType.sig = new Buffer(fileType.sig, 'hex'); + fileType.offset = fileType.offset || 0; - Config.archives.formats[fmtKey].sig = new Buffer(Config.archives.formats[fmtKey].sig, 'hex'); - Config.archives.formats[fmtKey].offset = Config.archives.formats[fmtKey].offset || 0; - - const sigLen = Config.archives.formats[fmtKey].offset + Config.archives.formats[fmtKey].sig.length; - if(sigLen > this.longestSignature) { - this.longestSignature = sigLen; - } + // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well + const sigLen =fileType.offset + fileType.sig.length; + if(sigLen > this.longestSignature) { + this.longestSignature = sigLen; + } + } }); } } - - /* - getArchiver(archType) { - if(!archType || 0 === archType.length) { - return; - } + + getArchiver(mimeTypeOrExtension) { + mimeTypeOrExtension = resolveMimeType(mimeTypeOrExtension); - archType = archType.toLowerCase(); - return this.archivers[archType]; - }*/ - - getArchiver(archType) { - if(!archType || 0 === archType.length) { + if(!mimeTypeOrExtension) { // lookup returns false on failure return; } - if(_.has(Config, [ 'archives', 'formats', archType, 'handler' ] ) && - _.has(Config, [ 'archives', 'archivers', Config.archives.formats[archType].handler ] )) - { - return Config.archives.archivers[ Config.archives.formats[archType].handler ]; + const archiveHandler = _.get( Config, [ 'fileTypes', mimeTypeOrExtension, 'archiveHandler'] ); + if(archiveHandler) { + return _.get( Config, [ 'archives', 'archivers', archiveHandler ] ); } } @@ -121,10 +113,6 @@ module.exports = class ArchiveUtil { } detectType(path, cb) { - if(!_.has(Config, 'archives.formats')) { - return cb(Errors.DoesNotExist('No formats configured')); - } - fs.open(path, 'r', (err, fd) => { if(err) { return cb(err); @@ -136,15 +124,19 @@ module.exports = class ArchiveUtil { return cb(err); } - const archFormat = _.findKey(Config.archives.formats, archFormat => { - const lenNeeded = archFormat.offset + archFormat.sig.length; + const archFormat = _.findKey(Config.fileTypes, fileTypeInfo => { + if(!fileTypeInfo.sig) { + return false; + } + + const lenNeeded = fileTypeInfo.offset + fileTypeInfo.sig.length; if(bytesRead < lenNeeded) { return false; } - const comp = buf.slice(archFormat.offset, archFormat.offset + archFormat.sig.length); - return (archFormat.sig.equals(comp)); + const comp = buf.slice(fileTypeInfo.offset, fileTypeInfo.offset + fileTypeInfo.sig.length); + return (fileTypeInfo.sig.equals(comp)); }); return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); @@ -157,19 +149,13 @@ module.exports = class ArchiveUtil { // so we have this horrible, horrible hack: let err; proc.once('data', d => { - if(_.isString(d) && d.startsWith('execvp(3) failed.: No such file or directory')) { - err = new Error(`${action} failed: ${d.trim()}`); + if(_.isString(d) && d.startsWith('execvp(3) failed.')) { + err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); } }); proc.once('exit', exitCode => { - if(exitCode) { - return cb(new Error(`${action} failed with exit code: ${exitCode}`)); - } - if(err) { - return cb(err); - } - return cb(null); + return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err); }); } @@ -177,7 +163,7 @@ module.exports = class ArchiveUtil { const archiver = this.getArchiver(archType); if(!archiver) { - return cb(new Error(`Unknown archive type: ${archType}`)); + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } const fmtObj = { @@ -205,7 +191,7 @@ module.exports = class ArchiveUtil { const archiver = this.getArchiver(archType); if(!archiver) { - return cb(new Error(`Unknown archive type: ${archType}`)); + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } const fmtObj = { @@ -235,7 +221,7 @@ module.exports = class ArchiveUtil { const archiver = this.getArchiver(archType); if(!archiver) { - return cb(new Error(`Unknown archive type: ${archType}`)); + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } const fmtObj = { @@ -254,7 +240,7 @@ module.exports = class ArchiveUtil { proc.once('exit', exitCode => { if(exitCode) { - return cb(new Error(`List failed with exit code: ${exitCode}`)); + return cb(Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)); } const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 }; diff --git a/core/bbs.js b/core/bbs.js index 3ac0c861..f2ab58ba 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -21,7 +21,7 @@ const fs = require('fs'); const paths = require('path'); // our main entry point -exports.bbsMain = bbsMain; +exports.main = main; // object with various services we want to de-init/shutdown cleanly if possible const initServices = {}; @@ -42,7 +42,7 @@ function printHelpAndExit() { process.exit(); } -function bbsMain() { +function main() { async.waterfall( [ function processArgs(callback) { diff --git a/core/config.js b/core/config.js index 854d842f..06b95124 100644 --- a/core/config.js +++ b/core/config.js @@ -223,6 +223,14 @@ function getDefaultConfig() { privateKeyPem : paths.join(__dirname, './../misc/ssh_private_key.pem'), firstMenu : 'sshConnected', firstMenuNewUser : 'sshConnectedNewUser', + }, + webSocket : { + port : 8810, + enabled : true, // :TODO: default to false + }, + secureWebSocket : { + port : 8811, + enabled : false, } }, @@ -263,6 +271,124 @@ function getDefaultConfig() { } } }, + + infoExtractUtils : { + Exiftool2Desc : { + cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x + }, + Exiftool : { + cmd : 'exiftool', + args : [ + '-charset', 'utf8', '{filePath}', + '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', '--filemodifydate', '--fileaccessdate', '--fileinodechangedate' + ] + } + }, + + fileTypes : { + // + // File types explicitly known to the system. Here we can configure + // information extraction, archive treatment, etc. + // + // MIME types can be found in mime-db: https://github.com/jshttp/mime-db + // + // Resources for signature/magic bytes: + // * http://www.garykessler.net/library/file_sigs.html + // + // + // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads + // :TODO: textual : bool -- if text, we can view. + // :TODO: asText : { cmd, args[] } -> viewable text + + // + // Audio + // + 'audio/mpeg' : { + desc : 'MP3 Audio', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'application/pdf' : { + desc : 'Adobe PDF', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + // + // Images + // + 'image/jpeg' : { + desc : 'JPEG Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/png' : { + desc : 'Portable Network Graphic Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/gif' : { + desc : 'Graphics Interchange Format Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/webp' : { + desc : 'WebP Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + // + // Archives + // + 'application/zip' : { + desc : 'ZIP Archive', + sig : '504b0304', + offset : 0, + archiveHandler : '7Zip', + }, + 'application/x-arj' : { + desc : 'ARJ Archive', + sig : '60ea', + offset : 0, + archiveHandler : 'Arj', + }, + 'application/x-rar-compressed' : { + desc : 'RAR Archive', + sig : '526172211a0700', + offset : 0, + archiveHandler : 'Rar', + }, + 'application/gzip' : { + desc : 'Gzip Archive', + sig : '1f8b', + offset : 0, + archiveHandler : '7Zip', + }, + // :TODO: application/x-bzip + 'application/x-bzip2' : { + desc : 'BZip2 Archive', + sig : '425a68', + offset : 0, + archiveHandler : '7Zip', + }, + 'application/x-lzh-compressed' : { + desc : 'LHArc Archive', + sig : '2d6c68', + offset : 2, + archiveHandler : 'Lha', + }, + 'application/x-7z-compressed' : { + desc : '7-Zip Archive', + sig : '377abcaf271c', + offset : 0, + archiveHandler : '7Zip', + } + + // :TODO: update archives::formats to fall here + // * archive handler -> archiveHandler (consider archive if archiveHandler present) + // * sig, offset, ... + // * mime-db -> exts lookup + // * + }, archives : { archivers : { @@ -348,62 +474,6 @@ function getDefaultConfig() { } } }, - - formats : { - // - // Resources - // * http://www.garykessler.net/library/file_sigs.html - // - zip : { - sig : '504b0304', - offset : 0, - exts : [ 'zip' ], - handler : '7Zip', - desc : 'ZIP Archive', - }, - '7z' : { - sig : '377abcaf271c', - offset : 0, - exts : [ '7z' ], - handler : '7Zip', - desc : '7-Zip Archive', - }, - arj : { - sig : '60ea', - offset : 0, - exts : [ 'arj' ], - handler : 'Arj', - desc : 'ARJ Archive', - }, - rar : { - sig : '526172211a0700', - offset : 0, - exts : [ 'rar' ], - handler : 'Rar', - desc : 'RAR Archive', - }, - gzip : { - sig : '1f8b', - offset : 0, - exts : [ 'gz' ], - handler : '7Zip', - desc : 'Gzip Archive', - }, - bzip : { - sig : '425a68', - offset : 0, - exts : [ 'bz2' ], - handler : '7Zip', - desc : 'BZip2 Archive', - }, - lzh : { - sig : '2d6c68', - offset : 2, - exts : [ 'lzh', 'ice' ], - handler : 'Lha', - desc : 'LHArc Archive', - } - } }, fileTransferProtocols : { @@ -550,8 +620,9 @@ function getDefaultConfig() { "\\b('[1789][0-9])\\b", // eslint-disable-line quotes '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 + '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- do this before 19xx 20xx such that this has priority + '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. - // :TODO: "Copyright YYYY someone" ], web : { diff --git a/core/file_base_area.js b/core/file_base_area.js index 5030f725..9ad03023 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -10,6 +10,9 @@ const FileDb = require('./database.js').dbs.file; const ArchiveUtil = require('./archive_util.js'); const CRC32 = require('./crc.js').CRC32; const Log = require('./logger.js').log; +const resolveMimeType = require('./mime_util.js').resolveMimeType; +const stringFormat = require('./string_format.js'); +const wordWrapText = require('./word_wrap.js').wordWrapText; // deps const _ = require('lodash'); @@ -19,6 +22,8 @@ const crypto = require('crypto'); const paths = require('path'); const temptmp = require('temptmp').createTrackedSession('file_area'); const iconv = require('iconv-lite'); +const exec = require('child_process').exec; +const moment = require('moment'); exports.isInternalArea = isInternalArea; exports.getAvailableFileAreas = getAvailableFileAreas; @@ -210,7 +215,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); function getMatch(input) { - if(input) { + if(input) { let m; for(let i = 0; i < patterns.length; ++i) { m = patterns[i].exec(input); @@ -222,8 +227,12 @@ function attemptSetEstimatedReleaseDate(fileEntry) { } // - // We attempt deteciton in short -> long order + // We attempt detection in short -> long order // + // Throw out anything that is current_year + 2 (we give some leway) + // with the assumption that must be wrong. + // + const maxYear = moment().add(2, 'year').year(); const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); if(match && match[1]) { let year; @@ -240,7 +249,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { year = parseInt(match[1]); } - if(year) { + if(year && year <= maxYear) { fileEntry.meta.est_release_year = year; } } @@ -390,9 +399,82 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c ); } +function getInfoExtractUtilForDesc(mimeType, descType) { + let util = _.get(Config, [ 'fileTypes', mimeType, `${descType}DescUtil` ]); + if(!_.isString(util)) { + return; + } + + util = _.get(Config, [ 'infoExtractUtils', util ]); + if(!util || !_.isString(util.cmd)) { + return; + } + + return util; +} + function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb) { - // :TODO: implement me! - return cb(null); + + async.series( + [ + function processDescFilesStart(callback) { + stepInfo.step = 'desc_files_start'; + return iterator(callback); + }, + function getDescriptions(callback) { + const mimeType = resolveMimeType(filePath); + if(!mimeType) { + return callback(null); + } + + async.eachSeries( [ 'short', 'long' ], (descType, nextDesc) => { + const util = getInfoExtractUtilForDesc(mimeType, descType); + if(!util) { + return nextDesc(null); + } + + const args = (util.args || [ '{filePath} '] ).map( arg => stringFormat(arg, { filePath : filePath } ) ); + + exec(`${util.cmd} ${args.join(' ')}`, (err, stdout) => { + if(err) { + logDebug( + { error : err.message, cmd : util.cmd, args : args }, + `${_.upperFirst(descType)} description command failed` + ); + } else { + stdout = (stdout || '').trim(); + if(stdout.length > 0) { + const key = 'short' === descType ? 'desc' : 'descLong'; + if('desc' === key) { + // + // Word wrap short descriptions to FILE_ID.DIZ spec + // + // "...no more than 45 characters long" + // + // See http://www.textfiles.com/computers/fileid.txt + // + stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n'); + } + + fileEntry[key] = stdout; + } + } + + return nextDesc(null); + }); + }, () => { + return callback(null); + }); + }, + function processDescFilesFinish(callback) { + stepInfo.step = 'desc_files_finish'; + return iterator(callback); + }, + ], + err => { + return cb(err); + } + ); } function addNewFileEntry(fileEntry, filePath, cb) { diff --git a/core/file_entry.js b/core/file_entry.js index 35241d7d..49e4b8a3 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -396,13 +396,13 @@ module.exports = class FileEntry { FROM file_user_rating WHERE file_id = f.file_id) AS avg_rating - FROM file f, file_meta m`; + FROM file f`; sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; } else { sql = `SELECT DISTINCT f.file_id, f.${filter.sort} - FROM file f, file_meta m`; + FROM file f`; sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; } @@ -410,7 +410,7 @@ module.exports = class FileEntry { } else { sql = `SELECT DISTINCT f.file_id - FROM file f, file_meta m`; + FROM file f`; sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; } diff --git a/core/mime_util.js b/core/mime_util.js new file mode 100644 index 00000000..ca7ab50e --- /dev/null +++ b/core/mime_util.js @@ -0,0 +1,14 @@ +/* jslint node: true */ +'use strict'; + +const mimeTypes = require('mime-types'); + +exports.resolveMimeType = resolveMimeType; + +function resolveMimeType(query) { + if(mimeTypes.extensions[query]) { + return query; // alreaed a mime-type + } + + return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined +} \ No newline at end of file diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 25697a13..68998044 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -48,174 +48,182 @@ function userStatAsString(client, statName, defaultValue) { return (StatLog.getUserStat(client.user, statName) || defaultValue).toString(); } +const PREDEFINED_MCI_GENERATORS = { + // + // Board + // + BN : function boardName() { return Config.general.boardName; }, + + // ENiGMA + VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, + VN : function version() { return packageJson.version; }, + + // +op info + SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, + SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, + SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, + SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, + SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, + SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, + // :TODO: op age, web, ????? + + // + // Current user / session + // + UN : function userName(client) { return client.user.username; }, + UI : function userId(client) { return client.user.userId.toString(); }, + UG : function groups(client) { return _.values(client.user.groups).join(', '); }, + UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, + LO : function location(client) { return userStatAsString(client, 'location', ''); }, + UA : function age(client) { return client.user.getAge().toString(); }, + BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY + US : function sex(client) { return userStatAsString(client, 'sex', ''); }, + UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, + UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, + UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, + UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, + UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, + ND : function connectedNode(client) { return client.node.toString(); }, + IP : function clientIpAddress(client) { return client.remoteAddress; }, + ST : function serverName(client) { return client.session.serverName; }, + FN : function activeFileBaseFilterName(client) { + const activeFilter = FileBaseFilters.getActiveFilter(client); + return activeFilter ? activeFilter.name : ''; + }, + DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 + DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes + const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + UP : function userNumUploadsclient(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 + UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes + const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + NR : function userUpDownRatio(client) { // Obv/2 + return getRatio(client, 'ul_total_count', 'dl_total_count'); + }, + KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio + return getRatio(client, 'ul_total_bytes', 'dl_total_bytes'); + }, + + MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, + PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, + PC : function userPostCallRatio(client) { return getRatio(client, 'post_count', 'login_count'); }, + + MD : function currentMenuDescription(client) { + return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; + }, + + MA : function messageAreaName(client) { + const area = getMessageAreaByTag(client.user.properties.message_area_tag); + return area ? area.name : ''; + }, + MC : function messageConfName(client) { + const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); + return conf ? conf.name : ''; + }, + ML : function messageAreaDescription(client) { + const area = getMessageAreaByTag(client.user.properties.message_area_tag); + return area ? area.desc : ''; + }, + CM : function messageConfDescription(client) { + const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); + return conf ? conf.desc : ''; + }, + + SH : function termHeight(client) { return client.term.termHeight.toString(); }, + SW : function termWidth(client) { return client.term.termWidth.toString(); }, + + // + // Date/Time + // + // :TODO: change to CD for 'Current Date' + DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, + CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, + + // + // OS/System Info + // + OS : function operatingSystem() { + return { + linux : 'Linux', + darwin : 'Mac OS X', + win32 : 'Windows', + sunos : 'SunOS', + freebsd : 'FreeBSD', + }[os.platform()] || os.type(); + }, + + OA : function systemArchitecture() { return os.arch(); }, + + SC : function systemCpuModel() { + // + // Clean up CPU strings a bit for better display + // + return os.cpus()[0].model + .replace(/\(R\)|\(TM\)|processor|CPU/g, '') + .replace(/\s+(?= )/g, ''); + }, + + // :TODO: MCI for core count, e.g. os.cpus().length + + // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage + NV : function nodeVersion() { return process.version; }, + + AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, + + TC : function totalCalls() { return StatLog.getSystemStat('login_count').toString(); }, + + RR : function randomRumor() { + // start the process of picking another random one + setNextRandomRumor(); + + return StatLog.getSystemStat('random_rumor'); + }, + + // + // System File Base, Up/Download Info + // + // :TODO: DD - Today's # of downloads (iNiQUiTY) + // + // :TODO: System stat log for total ul/dl, total ul/dl bytes + + // :TODO: PT - Messages posted *today* (Obv/2) + // -> Include FTN/etc. + // :TODO: NT - New users today (Obv/2) + // :TODO: CT - Calls *today* (Obv/2) + // :TODO: TF - Total files on the system (Obv/2) + // :TODO: FT - Files uploaded/added *today* (Obv/2) + // :TODO: DD - Files downloaded *today* (iNiQUiTY) + // :TODO: TP - total message/posts on the system (Obv/2) + // -> Include FTN/etc. + // :TODO: LC - name of last caller to system (Obv/2) + // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) + + + // + // Special handling for XY + // + XY : function xyHack() { return; /* nothing */ }, +}; + function getPredefinedMCIValue(client, code) { if(!client || !code) { return; } - try { - return { - // - // Board - // - BN : function boardName() { return Config.general.boardName; }, + const generator = PREDEFINED_MCI_GENERATORS[code]; - // ENiGMA - VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, - VN : function version() { return packageJson.version; }, + if(generator) { + let value; + try { + value = generator(client); + } catch(e) { + Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' ); + } - // +op info - SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, - SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, - SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, - SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, - SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, - SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, - // :TODO: op age, web, ????? - - // - // Current user / session - // - UN : function userName() { return client.user.username; }, - UI : function userId() { return client.user.userId.toString(); }, - UG : function groups() { return _.values(client.user.groups).join(', '); }, - UR : function realName() { return userStatAsString(client, 'real_name', ''); }, - LO : function location() { return userStatAsString(client, 'location', ''); }, - UA : function age() { return client.user.getAge().toString(); }, - BD : function birthdate() { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY - US : function sex() { return userStatAsString(client, 'sex', ''); }, - UE : function emailAddres() { return userStatAsString(client, 'email_address', ''); }, - UW : function webAddress() { return userStatAsString(client, 'web_address', ''); }, - UF : function affils() { return userStatAsString(client, 'affiliation', ''); }, - UT : function themeId() { return userStatAsString(client, 'theme_id', ''); }, - UC : function loginCount() { return userStatAsString(client, 'login_count', 0); }, - ND : function connectedNode() { return client.node.toString(); }, - IP : function clientIpAddress() { return client.remoteAddress; }, - ST : function serverName() { return client.session.serverName; }, - FN : function activeFileBaseFilterName() { - const activeFilter = FileBaseFilters.getActiveFilter(client); - return activeFilter ? activeFilter.name : ''; - }, - DN : function userNumDownloads() { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 - DK : function userByteDownload() { // Obv/2 uses DK=downloaded Kbytes - const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr - }, - UP : function userNumUploads() { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 - UK : function userByteUpload() { // Obv/2 uses UK=uploaded Kbytes - const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr - }, - NR : function userUpDownRatio() { // Obv/2 - return getRatio(client, 'ul_total_count', 'dl_total_count'); - }, - KR : function userUpDownByteRatio() { // Obv/2 uses KR=upload/download Kbyte ratio - return getRatio(client, 'ul_total_bytes', 'dl_total_bytes'); - }, - - MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, - PS : function userPostCount() { return userStatAsString(client, 'post_count', 0); }, - PC : function userPostCallRatio() { return getRatio(client, 'post_count', 'login_count'); }, - - MD : function currentMenuDescription() { - return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; - }, - - MA : function messageAreaName() { - const area = getMessageAreaByTag(client.user.properties.message_area_tag); - return area ? area.name : ''; - }, - MC : function messageConfName() { - const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); - return conf ? conf.name : ''; - }, - ML : function messageAreaDescription() { - const area = getMessageAreaByTag(client.user.properties.message_area_tag); - return area ? area.desc : ''; - }, - CM : function messageConfDescription() { - const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); - return conf ? conf.desc : ''; - }, - - SH : function termHeight() { return client.term.termHeight.toString(); }, - SW : function termWidth() { return client.term.termWidth.toString(); }, - - // - // Date/Time - // - // :TODO: change to CD for 'Current Date' - DT : function date() { return moment().format(client.currentTheme.helpers.getDateFormat()); }, - CT : function time() { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, - - // - // OS/System Info - // - OS : function operatingSystem() { - return { - linux : 'Linux', - darwin : 'Mac OS X', - win32 : 'Windows', - sunos : 'SunOS', - freebsd : 'FreeBSD', - }[os.platform()] || os.type(); - }, - - OA : function systemArchitecture() { return os.arch(); }, - SC : function systemCpuModel() { - // - // Clean up CPU strings a bit for better display - // - return os.cpus()[0].model.replace(/\(R\)|\(TM\)|processor|CPU/g, '') - .replace(/\s+(?= )/g, ''); - }, - - // :TODO: MCI for core count, e.g. os.cpus().length - - // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage - NV : function nodeVersion() { return process.version; }, - - AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, - - TC : function totalCalls() { return StatLog.getSystemStat('login_count').toString(); }, - - RR : function randomRumor() { - // start the process of picking another random one - setNextRandomRumor(); - - return StatLog.getSystemStat('random_rumor'); - }, - - // - // System File Base, Up/Download Info - // - // :TODO: DD - Today's # of downloads (iNiQUiTY) - // - // :TODO: System stat log for total ul/dl, total ul/dl bytes - - // :TODO: PT - Messages posted *today* (Obv/2) - // -> Include FTN/etc. - // :TODO: NT - New users today (Obv/2) - // :TODO: CT - Calls *today* (Obv/2) - // :TODO: TF - Total files on the system (Obv/2) - // :TODO: FT - Files uploaded/added *today* (Obv/2) - // :TODO: DD - Files downloaded *today* (iNiQUiTY) - // :TODO: TP - total message/posts on the system (Obv/2) - // -> Include FTN/etc. - // :TODO: LC - name of last caller to system (Obv/2) - // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) - - - // - // Special handling for XY - // - XY : function xyHack() { return; /* nothing */ }, - - }[code](); // :TODO: Just call toString() here and remove above - DRY - - } catch(e) { - // Don't use client.log here as we may not have a client logger established yet!! - Log.warn( { code : code, exception : e.message }, 'Exception caught attempting to construct predefined MCI value'); + return value; } } diff --git a/core/theme.js b/core/theme.js index d9a51ca0..2e02cbca 100644 --- a/core/theme.js +++ b/core/theme.js @@ -378,10 +378,11 @@ function getThemeArt(options, cb) { options.random = _.isBoolean(options.random) ? options.random : true; // FILENAME.EXT support // - // We look for themed art in the following manor: - // * Supplied theme via |themeId| - // * Fallback 1: Default theme (if different than |themeId|) - // * General art directory + // We look for themed art in the following order: + // 1) Direct/relative path + // 2) Via theme supplied by |themeId| + // 3) Via default theme + // 4) General art directory // async.waterfall( [ @@ -389,7 +390,7 @@ function getThemeArt(options, cb) { // // We allow relative (to enigma-bbs) or full paths // - if('/' === options.name[0]) { + if('/' === options.name.charAt(0)) { // just take the path as-is options.basePath = paths.dirname(options.name); } else if(options.name.indexOf('/') > -1) { @@ -409,41 +410,35 @@ function getThemeArt(options, cb) { } options.basePath = paths.join(Config.paths.themes, options.themeId); - - art.getArt(options.name, options, function artLoaded(err, artInfo) { - callback(null, artInfo); + art.getArt(options.name, options, (err, artInfo) => { + return callback(null, artInfo); }); }, function fromDefaultTheme(artInfo, callback) { if(artInfo || Config.defaults.theme === options.themeId) { - callback(null, artInfo); - } else { - options.basePath = paths.join(Config.paths.themes, Config.defaults.theme); - - art.getArt(options.name, options, function artLoaded(err, artInfo) { - callback(null, artInfo); - }); + return callback(null, artInfo); } + + options.basePath = paths.join(Config.paths.themes, Config.defaults.theme); + art.getArt(options.name, options, (err, artInfo) => { + return callback(null, artInfo); + }); }, function fromGeneralArtDir(artInfo, callback) { if(artInfo) { - callback(null, artInfo); - } else { - options.basePath = Config.paths.art; - - art.getArt(options.name, options, function artLoaded(err, artInfo) { - callback(err, artInfo); - }); + return callback(null, artInfo); } + + options.basePath = Config.paths.art; + art.getArt(options.name, options, (err, artInfo) => { + return callback(err, artInfo); + }); } ], function complete(err, artInfo) { if(err) { - if(options.client) { - options.client.log.debug( { error : err.message }, 'Cannot find theme art' ); - } else { - Log.debug( { error : err.message }, 'Cannot find theme art' ); - } + const logger = _.get(options, 'client.log') || Log; + logger.debug( { reason : err.message }, 'Cannot find theme art'); } return cb(err, artInfo); } diff --git a/main.js b/main.js index ef624294..0bc7bee9 100755 --- a/main.js +++ b/main.js @@ -9,4 +9,4 @@ If this file does not run directly, ensure it's executable: > chmod u+x main.js */ -require('./core/bbs.js').bbsMain(); \ No newline at end of file +require('./core/bbs.js').main(); \ No newline at end of file diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 0a0e2134..9dacc23d 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -17,6 +17,7 @@ const Config = require('../core/config.js').config; const DownloadQueue = require('../core/download_queue.js'); const FileAreaWeb = require('../core/file_area_web.js'); const FileBaseFilters = require('../core/file_base_filter.js'); +const resolveMimeType = require('../core/mime_util.js').resolveMimeType; const cleanControlCodes = require('../core/string_util.js').cleanControlCodes; @@ -236,9 +237,8 @@ exports.getModule = class FileAreaList extends MenuModule { }); if(entryInfo.archiveType) { - entryInfo.archiveTypeDesc = _.has(Config, [ 'archives', 'formats', entryInfo.archiveType, 'desc' ]) ? - Config.archives.formats[entryInfo.archiveType].desc : - entryInfo.archiveType; + const mimeType = resolveMimeType(entryInfo.archiveType); + entryInfo.archiveTypeDesc = mimeType ? _.get(Config, [ 'fileTypes', mimeType, 'desc' ] ) || mimeType : entryInfo.archiveType; } else { entryInfo.archiveTypeDesc = 'N/A'; } diff --git a/mods/menu.hjson b/mods/menu.hjson index ef4c4455..5852ad28 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -2372,6 +2372,17 @@ ] focusItemIndex: 1 } + + // :TODO: these can be removed once the hack is not required: + TL10: {} + TL11: {} + TL12: {} + TL13: {} + TL14: {} + TL15: {} + TL16: {} + TL17: {} + TL18: {} } submit: { @@ -2453,6 +2464,17 @@ "general", "nfo/readme", "file listing" ] } + + // :TODO: these can be removed once the hack is not required: + TL10: {} + TL11: {} + TL12: {} + TL13: {} + TL14: {} + TL15: {} + TL16: {} + TL17: {} + TL18: {} } actionKeys: [ diff --git a/package.json b/package.json index 4e8046c8..62e69dc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.5-alpha", + "version": "0.0.6-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", @@ -15,36 +15,37 @@ "keywords": [ "bbs", "telnet", + "ssh", "retro" ], "dependencies": { - "async": "^2.1.4", + "async": "^2.4.0", "binary": "0.3.x", "buffers": "NuSkooler/node-buffers", - "bunyan": "^1.7.1", + "bunyan": "^1.8.10", "farmhash": "^1.2.1", - "fs-extra": "^2.0.0", + "fs-extra": "^3.0.1", "gaze": "^1.1.2", "hashids": "^1.1.1", - "hjson": "^2.4.1", - "iconv-lite": "^0.4.13", - "inquirer": "^3.0.1", + "hjson": "^2.4.2", + "iconv-lite": "^0.4.17", + "inquirer": "^3.0.6", "later": "1.2.0", "lodash": "^4.17.4", - "mime-types": "^2.1.12", + "mime-types": "^2.1.15", "minimist": "1.2.x", - "moment": "^2.11.0", - "uuid": "^3.0.1", - "uuid-parse" : "^1.0.0", + "moment": "^2.18.1", + "nodemailer": "^4.0.1", "ptyw.js": "NuSkooler/ptyw.js", + "sanitize-filename": "^1.6.1", "sqlite3": "^3.1.1", "ssh2": "^0.5.1", - "temptmp" : "^1.0.0", - "sanitize-filename" : "^1.6.1", - "nodemailer" : "^3.1.3" - }, - "devDependencies": { + "temptmp": "^1.0.0", + "uuid": "^3.0.1", + "uuid-parse": "^1.0.0", + "ws" : "^2.3.1" }, + "devDependencies": {}, "engines": { "node": ">=6.9.2" } diff --git a/util/exiftool2desc.js b/util/exiftool2desc.js new file mode 100755 index 00000000..311daa2b --- /dev/null +++ b/util/exiftool2desc.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node + +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +// :TODO: Make this it's own sep tool/repo + +const exiftool = require('exiftool'); +const fs = require('fs'); +const moment = require('moment'); + +const TOOL_VERSION = '1.0.0.0'; + +// map fileTypes -> handlers +const FILETYPE_HANDLERS = {}; +[ 'AIFF', 'APE', 'FLAC', 'OGG', 'MP3' ].forEach(ext => FILETYPE_HANDLERS[ext] = audioFile); +[ 'PDF', 'DOC', 'DOCX', 'DOCM', 'ODB', 'ODC', 'ODF', 'ODG', 'ODI', 'ODP', 'ODS', 'ODT' ].forEach(ext => FILETYPE_HANDLERS[ext] = documentFile); +[ 'PNG', 'JPEG', 'GIF', 'WEBP', 'XCF' ].forEach(ext => FILETYPE_HANDLERS[ext] = imageFile); + +function audioFile(metadata) { + let desc = `${metadata.artist||'Unknown Artist'} - ${metadata.title||'Unknown'} (`; + if(metadata.year) { + desc += `${metadata.year}, `; + } + desc += `${metadata.audioBitrate})`; + return desc; +} + +function documentFile(metadata) { + let desc = `${metadata.author||'Unknown Author'} - ${metadata.title||'Unknown'}`; + const created = moment(metadata.createdate); + if(created.isValid()) { + desc += ` (${created.format('YYYY')})`; + } + return desc; +} + +function imageFile(metadata) { + let desc = `${metadata.fileType} image (`; + if(metadata.animationIterations) { + desc += 'Animated, '; + } + desc += `${metadata.imageSize}px`; + const created = moment(metadata.createdate); + if(created.isValid()) { + desc += `, ${created.format('YYYY')})`; + } else { + desc += ')'; + } + return desc; +} + +function main() { + const argv = exports.argv = require('minimist')(process.argv.slice(2), { + alias : { + h : 'help', + v : 'version', + } + }); + + if(argv.version) { + console.info(TOOL_VERSION); + return 0; + } + + if(0 === argv._.length || argv.help) { + console.info('usage: exiftool2desc.js [--version] [--help] PATH'); + return 0; + } + + const path = argv._[0]; + + fs.readFile(path, (err, data) => { + if(err) { + return -1; + } + + exiftool.metadata(data, (err, metadata) => { + if(err) { + return -1; + } + + const handler = FILETYPE_HANDLERS[metadata.fileType]; + if(!handler) { + return -1; + } + + console.info(handler(metadata)); + return 0; + }); + }); +} + +return main(); \ No newline at end of file