diff --git a/api/config.mjs b/api/config.mjs index ea732e7..5fdf6e4 100644 --- a/api/config.mjs +++ b/api/config.mjs @@ -55,6 +55,7 @@ nconf.defaults({ "url": "http://beta01.nfp.moe" }, "mssql": { + "conn_timeout": 5, "floor": 1, "ceiling": 2, "heartbeatSecs": 20, diff --git a/api/db.mjs b/api/db.mjs new file mode 100644 index 0000000..e4448b8 --- /dev/null +++ b/api/db.mjs @@ -0,0 +1,54 @@ +import MSSQL from 'msnodesqlv8' +import { HttpError } from 'flaska' + +export function initPool(core, config) { + let pool = new MSSQL.Pool(config) + + core.log.info(config, 'MSSQL database setttings') + + pool.on('open', function() { + core.log.info('MSSQL connection open') + }) + + let waiting = false + + /*pool.on('error', function(error) { + if (error.length) { + let msg = 'Error in MSSQL pool\n => ' + error[0].message.trim() + for (let i = 1; i < error.length; i++) { + msg += '\n => ' + error[i].message.trim() + } + core.log.error(msg) + } else { + core.log.error('Error in MSSQL pool') + core.log.error(error) + } + + if (waiting) { return } + core.log.warn('Attempting to connect again in 5 seconds') + waiting = true + setTimeout(function() { + waiting = false + console.log('opening') + pool.open() + console.log('done') + }, 5000) + })*/ + + core.log.info('Attempting to connect to MSSQL server') + pool.open() + + return { + safeCallProc: function(name, params, options) { + return pool.promises.callProc(name, params, options) + .catch(function(err) { + let message = err.message + if (err.lineNumber && err.procName) { + message = `Error at ${err.procName}:${err.lineNumber} => ${message}` + } + throw new HttpError(500, message) + }) + }, + promises: pool.promises, + } +} \ No newline at end of file diff --git a/api/file/torrent.mjs b/api/file/torrent.mjs new file mode 100644 index 0000000..be47192 --- /dev/null +++ b/api/file/torrent.mjs @@ -0,0 +1,105 @@ +import bencode from 'bencode' + +/* +Taken from parse-torrent +*/ + +/** + * Parse a torrent. Throws an exception if the torrent is missing required fields. + * @param {Buffer|Object} torrent + * @return {Object} parsed torrent + */ +export function decodeTorrentFile (torrent) { + if (Buffer.isBuffer(torrent)) { + torrent = bencode.decode(torrent) + } + + // sanity check + ensure(torrent.info, 'info') + ensure(torrent.info['name.utf-8'] || torrent.info.name, 'info.name') + ensure(torrent.info['piece length'], 'info[\'piece length\']') + ensure(torrent.info.pieces, 'info.pieces') + + if (torrent.info.files) { + torrent.info.files.forEach(file => { + ensure(typeof file.length === 'number', 'info.files[0].length') + ensure(file['path.utf-8'] || file.path, 'info.files[0].path') + }) + } else { + ensure(typeof torrent.info.length === 'number', 'info.length') + } + + const result = { + info: torrent.info, + infoBuffer: bencode.encode(torrent.info), + name: (torrent.info['name.utf-8'] || torrent.info.name).toString(), + announce: [] + } + + result.infoHash = sha1.sync(result.infoBuffer) + result.infoHashBuffer = Buffer.from(result.infoHash, 'hex') + + if (torrent.info.private !== undefined) result.private = !!torrent.info.private + + if (torrent['creation date']) result.created = new Date(torrent['creation date'] * 1000) + if (torrent['created by']) result.createdBy = torrent['created by'].toString() + + if (Buffer.isBuffer(torrent.comment)) result.comment = torrent.comment.toString() + + // announce and announce-list will be missing if metadata fetched via ut_metadata + if (Array.isArray(torrent['announce-list']) && torrent['announce-list'].length > 0) { + torrent['announce-list'].forEach(urls => { + urls.forEach(url => { + result.announce.push(url.toString()) + }) + }) + } else if (torrent.announce) { + result.announce.push(torrent.announce.toString()) + } + + // handle url-list (BEP19 / web seeding) + if (Buffer.isBuffer(torrent['url-list'])) { + // some clients set url-list to empty string + torrent['url-list'] = torrent['url-list'].length > 0 + ? [torrent['url-list']] + : [] + } + result.urlList = (torrent['url-list'] || []).map(url => url.toString()) + + // remove duplicates by converting to Set and back + result.announce = Array.from(new Set(result.announce)) + result.urlList = Array.from(new Set(result.urlList)) + + const files = torrent.info.files || [torrent.info] + result.files = files.map((file, i) => { + const parts = [].concat(result.name, file['path.utf-8'] || file.path || []).map(p => p.toString()) + return { + path: path.join.apply(null, [path.sep].concat(parts)).slice(1), + name: parts[parts.length - 1], + length: file.length, + offset: files.slice(0, i).reduce(sumLength, 0) + } + }) + + result.length = files.reduce(sumLength, 0) + + const lastFile = result.files[result.files.length - 1] + + result.pieceLength = torrent.info['piece length'] + result.lastPieceLength = ((lastFile.offset + lastFile.length) % result.pieceLength) || result.pieceLength + result.pieces = splitPieces(torrent.info.pieces) + + return result +} + +function splitPieces (buf) { + const pieces = [] + for (let i = 0; i < buf.length; i += 20) { + pieces.push(buf.slice(i, i + 20).toString('hex')) + } + return pieces +} + +function ensure (bool, fieldName) { + if (!bool) throw new Error(`Torrent is missing required field: ${fieldName}`) +} diff --git a/api/page/model.mjs b/api/page/model.mjs index c437b95..612fe14 100644 --- a/api/page/model.mjs +++ b/api/page/model.mjs @@ -20,7 +20,26 @@ Page model: */ export async function getTree(ctx) { - let res = await ctx.db.promises.callProc('pages_gettree', []) - console.log(res) - return {} + let res = await ctx.db.safeCallProc('pages_gettree', []) + let out = [] + let children = [] + let map = new Map() + for (let page of res.first) { + if (!page.parent_id) { + out.push(page) + } else { + children.push(page) + } + map.set(page.id, page) + } + for (let page of children) { + let parent = map.get(page.parent_id) + if (!parent.children) { + parent.children = [] + } + parent.children.push(page) + } + return { + tree: out + } } diff --git a/api/serve.mjs b/api/serve.mjs index 4ab7123..a4abaac 100644 --- a/api/serve.mjs +++ b/api/serve.mjs @@ -1,13 +1,25 @@ import path from 'path' +import dot from 'dot' import { FileResponse, HttpError } from 'flaska' import fs from 'fs/promises' +import fsSync from 'fs' + +import { getTree } from './page/model.mjs' export default class ServeHandler { constructor(opts = {}) { Object.assign(this, { fs: opts.fs || fs, + fsSync: opts.fsSync || fsSync, root: opts.root, + template: null, + frontend: opts.frontend || 'http://localhost:4000', + version: opts.version || 'version', }) + + let indexFile = fsSync.readFileSync(path.join(this.root, 'index.html')) + this.template = dot.template(indexFile.toString(), { argName: ['headerDescription', 'headerImage', 'headerTitle', 'headerUrl', 'payloadData', 'payloadLinks', 'payloadTree', 'version', 'nonce'] }) + // console.log(indexFile.toString()) } /** GET: /::file */ @@ -18,21 +30,52 @@ export default class ServeHandler { let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html')) + if (!ctx.params.file || ctx.params.file === 'index.html') { + return this.serveIndex(ctx) + } + if (!file.startsWith(this.root)) { ctx.status = 404 ctx.body = 'HTTP 404 Error' return } - return this.fs.stat(file).catch((err) => { - if (err.code === 'ENOENT') { - file = path.resolve(path.join(this.root, 'index.html')) - return this.fs.stat(file) - } - return Promise.reject(err) - }) + return this.fs.stat(file) .then(function(stat) { ctx.body = new FileResponse(file, stat) }) + .catch((err) => { + if (err.code === 'ENOENT') { + return this.serveIndex(ctx) + } + return Promise.reject(err) + }) + } + + async serveIndex(ctx) { + let payload = { + headerDescription: 'Small fansubbing and scanlation group translating and encoding our favourite shows from Japan.', + headerImage: this.frontend + '/assets/img/heart.png', + headerTitle: 'NFP Moe - Anime/Manga translation group', + headerUrl: this.frontend + ctx.url, + payloadData: null, + payloadLinks: null, + payloadTree: null, + version: this.version, + nonce: ctx.state.nonce, + } + + try { + payload.payloadTree = JSON.stringify(await getTree(ctx)) + } catch (e) { + ctx.log.error(e) + } + + ctx.body = this.template(payload) + ctx.type = 'text/html; charset=utf-8' + } + + serveErrorPage(ctx) { + } } \ No newline at end of file diff --git a/api/server.mjs b/api/server.mjs index bbabc4e..a2660b7 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -1,6 +1,6 @@ import { Flaska, QueryHandler } from 'flaska' -import MSSQL from 'msnodesqlv8' +import { initPool } from './db.mjs' import config from './config.mjs' import PageRoutes from './page/routes.mjs' import ServeHandler from './serve.mjs' @@ -15,30 +15,10 @@ export function run(http, port, core) { log: core.log, nonce: ['script-src'], nonceCacheLength: 50, - defaultHeaders: { - 'Server': 'Flaska', - 'X-Content-Type-Options': 'nosniff', - 'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'self'`, - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Resource-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - }, }, http) - + // Create our database pool - const pool = new MSSQL.Pool(config.get('mssql')) - - core.log.info(config.get('mssql'), 'MSSQL database setttings') - - pool.on('open', function() { - core.log.info('MSSQL connection open') - }) - - pool.on('error', function(error) { - core.log.error(error, 'Error in MSSQL pool') - }) - - pool.open() + let pool = initPool(core, config.get('mssql')) // configure our server if (config.get('NODE_ENV') === 'development') { @@ -87,6 +67,8 @@ export function run(http, port, core) { const serve = new ServeHandler({ root: localUtil.getPathFromRoot('../public'), + version: core.app.running, + frontend: config.get('frontend:url'), }) flaska.get('/::file', serve.serve.bind(serve)) diff --git a/app/api/page.js b/app/api/page.js index 035ec6d..73637b6 100644 --- a/app/api/page.js +++ b/app/api/page.js @@ -1,6 +1,6 @@ const common = require('./common') -const Tree = window.__nfptree || [] +const Tree = window.__nfptree?.tree || [] exports.Tree = Tree diff --git a/package.json b/package.json index c59d971..565415f 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,11 @@ "scripts": { "start": "node --experimental-modules index.mjs", "test": "echo \"Error: no test specified\" && exit 1", - "bla": "sass --help", "build:prod": "sass -s compressed app/app.scss public/assets/app.css && sass -s compressed app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js", "build": "sass app/app.scss public/assets/app.css && sass app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js", "build:watch": "npm-watch build", - "dev:server": "node dev.mjs | bunyan", - "dev:server:watch": "npm-watch dev:server", + "dev:server": "npm run build && node dev.mjs | bunyan", + "dev": "npm-watch dev:server", "watch:sass:public": "sass --watch app/app.scss public/assets/app.css", "watch:sass:admin": "sass --watch app/admin.scss public/assets/admin.css", "prod": "npm run build && npm start" @@ -22,14 +21,7 @@ "watch": { "dev:server": { "patterns": [ - "api/*" - ], - "extensions": "js,mjs", - "quiet": true, - "inherit": true - }, - "build": { - "patterns": [ + "api/*", "app/*" ], "extensions": "js,mjs", @@ -48,23 +40,18 @@ }, "homepage": "https://github.com/nfp-projects/nfp_moe", "dependencies": { - "bookshelf": "^0.15.1", - "dot": "^1.1.2", - "flaska": "^1.2.2", + "bencode": "^2.0.3", + "dot": "^2.0.0-beta.1", + "flaska": "^1.2.5", "format-link-header": "^2.1.0", - "http-errors": "^1.7.2", - "json-mask": "^0.3.8", - "knex-core": "^0.19.5", "msnodesqlv8": "^2.4.7", "nconf-lite": "^1.0.1", - "parse-torrent": "^7.0.1", - "pg": "^8.7.3", "striptags": "^3.1.1" }, "devDependencies": { "asbundle": "^2.6.1", - "mithril": "^2.0.4", - "sass": "^1.17.0", - "service-core": "^3.0.0-beta.13" + "mithril": "^2.2.2", + "sass": "^1.52.3", + "service-core": "^3.0.0-beta.17" } } diff --git a/public/assets/Inter.var.woff2 b/public/assets/Inter.var.woff2 new file mode 100644 index 0000000..365eedc Binary files /dev/null and b/public/assets/Inter.var.woff2 differ diff --git a/public/assets/inter.css b/public/assets/inter.css new file mode 100644 index 0000000..f450010 --- /dev/null +++ b/public/assets/inter.css @@ -0,0 +1,200 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url("Inter-Thin.woff2?v=3.19") format("woff2"), + url("Inter-Thin.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url("Inter-ThinItalic.woff2?v=3.19") format("woff2"), + url("Inter-ThinItalic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url("Inter-ExtraLight.woff2?v=3.19") format("woff2"), + url("Inter-ExtraLight.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 200; + font-display: swap; + src: url("Inter-ExtraLightItalic.woff2?v=3.19") format("woff2"), + url("Inter-ExtraLightItalic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url("Inter-Light.woff2?v=3.19") format("woff2"), + url("Inter-Light.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 300; + font-display: swap; + src: url("Inter-LightItalic.woff2?v=3.19") format("woff2"), + url("Inter-LightItalic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("Inter-Regular.woff2?v=3.19") format("woff2"), + url("Inter-Regular.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url("Inter-Italic.woff2?v=3.19") format("woff2"), + url("Inter-Italic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("Inter-Medium.woff2?v=3.19") format("woff2"), + url("Inter-Medium.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url("Inter-MediumItalic.woff2?v=3.19") format("woff2"), + url("Inter-MediumItalic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("Inter-SemiBold.woff2?v=3.19") format("woff2"), + url("Inter-SemiBold.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url("Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"), + url("Inter-SemiBoldItalic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("Inter-Bold.woff2?v=3.19") format("woff2"), + url("Inter-Bold.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url("Inter-BoldItalic.woff2?v=3.19") format("woff2"), + url("Inter-BoldItalic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url("Inter-ExtraBold.woff2?v=3.19") format("woff2"), + url("Inter-ExtraBold.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 800; + font-display: swap; + src: url("Inter-ExtraBoldItalic.woff2?v=3.19") format("woff2"), + url("Inter-ExtraBoldItalic.woff?v=3.19") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("Inter-Black.woff2?v=3.19") format("woff2"), + url("Inter-Black.woff?v=3.19") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url("Inter-BlackItalic.woff2?v=3.19") format("woff2"), + url("Inter-BlackItalic.woff?v=3.19") format("woff"); +} + +/* ------------------------------------------------------- +Variable font. +Usage: + + html { font-family: 'Inter', sans-serif; } + @supports (font-variation-settings: normal) { + html { font-family: 'Inter var', sans-serif; } + } +*/ +@font-face { + font-family: 'Inter var'; + font-weight: 100 900; + font-display: swap; + font-style: normal; + font-named-instance: 'Regular'; + src: url("Inter-roman.var.woff2?v=3.19") format("woff2"); +} +@font-face { + font-family: 'Inter var'; + font-weight: 100 900; + font-display: swap; + font-style: italic; + font-named-instance: 'Italic'; + src: url("Inter-italic.var.woff2?v=3.19") format("woff2"); +} + + +/* -------------------------------------------------------------------------- +[EXPERIMENTAL] Multi-axis, single variable font. + +Slant axis is not yet widely supported (as of February 2019) and thus this +multi-axis single variable font is opt-in rather than the default. + +When using this, you will probably need to set font-variation-settings +explicitly, e.g. + + * { font-variation-settings: "slnt" 0deg } + .italic { font-variation-settings: "slnt" 10deg } + +*/ +@font-face { + font-family: 'Inter var experimental'; + font-weight: 100 900; + font-display: swap; + font-style: oblique 0deg 10deg; + src: url("Inter.var.woff2?v=3.19") format("woff2"); +} diff --git a/public/index.html b/public/index.html index dbd11e4..07a5860 100644 --- a/public/index.html +++ b/public/index.html @@ -2,41 +2,37 @@ - {{=it.title}} + {{=headerTitle}} - + - - - - - {{? it.image === '/assets/img/heart.jpg' }} + + + + + {{? headerImage === '/assets/img/heart.jpg' }} - {{?? true }} {{? }} - - - + + -
- + diff --git a/temp.sql b/temp.sql new file mode 100644 index 0000000..2afd9c6 --- /dev/null +++ b/temp.sql @@ -0,0 +1,24 @@ +use nfp_sites; +go + +-- Object_definition(object_id) + +Declare @sql varchar(max) ; +SELECT @sql=Object_definition(object_id) +FROM sys.procedures +WHERE name = 'spe_get_my_data' and SCHEMA_NAME(schema_id) = 'dbo'; +SET @sql=REPLACE(@sql, 'CREATE PROCEDURE [dbo].', 'ALTER PROCEDURE [nfp_moe].'); +print @sql +exec (@sql) +print '-----------------------------'; + + +SELECT * +FROM sys.procedures +WHERE name like 'spe_%' and SCHEMA_NAME(schema_id) = 'dbo'; + + +SELECT * +FROM sys.procedures where SCHEMA_NAME(schema_id) = 'dbo'; + +EXEC sp_helptext 'dbo.spe_get_schema'; \ No newline at end of file