base: Fix upload support for null
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded

base: Tweak serve to be slightly more extendable
discord_embed: Added new site, discord embed
This commit is contained in:
Jonatan Nilsson 2022-10-17 07:52:55 +00:00
parent 9b227ead62
commit 2b1e2d695a
17 changed files with 577 additions and 21 deletions

View file

@ -15,20 +15,27 @@ export function uploadMedia(file) {
}
}
console.log(media)
let body = {}
if (media.preview) {
body.preview = media.preview
}
if (media.small?.avif) {
body.small = media.small.avif
}
if (media.medium?.avif) {
body.medium = media.medium.avif
}
if (media.large?.avif) {
body.large = media.large.avif
}
return client.upload(media.path + '?token=' + token, { file: {
file: file.path,
filename: file.name,
} }, 'POST', {
preview: media.preview,
small: media.small?.avif,
medium: media.medium?.avif,
large: media.large?.avif,
/*
small: media.small.jpeg,
medium: media.medium.avif,
large_jpeg: mediakl.large.jpeg,*/
}).then(res => {
} }, 'POST', body).then(res => {
out.filename = res.filename
out.path = res.path
out.preview = res.preview
@ -38,11 +45,18 @@ export function uploadMedia(file) {
out.size = file.size
out.type = file.type
return client.post(media.path + '/' + out.filename + '?token=' + token, {
small: media.small?.jpeg,
medium: media.medium?.jpeg,
large: media.large?.jpeg,
})
let body = {}
if (media.small?.jpeg) {
body.small = media.small.jpeg
}
if (media.medium?.jpeg) {
body.medium = media.medium.jpeg
}
if (media.large?.jpeg) {
body.large = media.large.jpeg
}
return client.post(media.path + '/' + out.filename + '?token=' + token, body)
.then(res => {
if (out.sizes.small) { out.sizes.small.jpeg = res.small }
if (out.sizes.medium) { out.sizes.medium.jpeg = res.medium }

View file

@ -22,8 +22,11 @@ export default class ServeHandler {
}
let indexFile = fsSync.readFileSync(path.join(this.root, 'index.html'))
this.loadTemplate(indexFile)
}
loadTemplate(indexFile) {
this.template = dot.template(indexFile.toString(), { argName: ['headerDescription', 'headerImage', 'headerTitle', 'headerUrl', 'payloadData', 'payloadTree', 'version', 'nonce', 'type', 'banner', 'media', 'in_debug'] })
// console.log(indexFile.toString())
}
register(server) {

View file

@ -53,6 +53,10 @@ export default class Server {
this.flaska.before(function(ctx) {
ctx.state.started = new Date().getTime()
ctx.req.ip = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress
ctx.log = ctx.log.child({
id: Math.random().toString(36).substring(2, 14),
})
ctx.db = pool
})
this.flaska.before(QueryHandler())
@ -92,6 +96,7 @@ export default class Server {
ctx.log[level]({
duration: requestTime,
status: ctx.status,
ip: ctx.req.ip,
}, (ctx.aborted ? '[ABORT]' : '<--') + ` ${status}${ctx.method} ${ctx.url}`)
})
}

85
discord_embed/api/id.mjs Normal file
View file

@ -0,0 +1,85 @@
/**
* Javascript AlphabeticID class
* (based on a script by Kevin van Zonneveld <kevin@vanzonneveld.net>)
*
* Author: Even Simon <even.simon@gmail.com>
*
* Description: Translates a numeric identifier into a short string and backwords.
*
* Usage:
* var str = AlphabeticID.encode(9007199254740989); // str = 'fE2XnNGpF'
* var id = AlphabeticID.decode('fE2XnNGpF'); // id = 9007199254740989;
**/
const AlphabeticID = {
index:'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
/**
* [@function](https://twitter.com/function) AlphabeticID.encode
* [@description](https://twitter.com/description) Encode a number into short string
* [@param](https://twitter.com/param) integer
* [@return](https://twitter.com/return) string
**/
encode:function(_number){
if('undefined' == typeof _number){
return null;
}
else if('number' != typeof(_number)){
throw new Error('Wrong parameter type');
}
var ret = '';
for(var i=Math.floor(Math.log(parseInt(_number))/Math.log(AlphabeticID.index.length));i>=0;i--){
ret = ret + AlphabeticID.index.substr((Math.floor(parseInt(_number) / AlphabeticID.bcpow(AlphabeticID.index.length, i)) % AlphabeticID.index.length),1);
}
return reverse(ret);
},
/**
* [@function](https://twitter.com/function) AlphabeticID.decode
* [@description](https://twitter.com/description) Decode a short string and return number
* [@param](https://twitter.com/param) string
* [@return](https://twitter.com/return) integer
**/
decode:function(_string){
if('undefined' == typeof _string){
return null;
}
else if('string' != typeof _string){
throw new Error('Wrong parameter type');
}
var str = reverse(_string);
var ret = 0;
for(var i=0;i<=(str.length - 1);i++){
ret = ret + AlphabeticID.index.indexOf(str.substr(i,1)) * (AlphabeticID.bcpow(AlphabeticID.index.length, (str.length - 1) - i));
}
return ret;
},
/**
* [@function](https://twitter.com/function) AlphabeticID.bcpow
* [@description](https://twitter.com/description) Raise _a to the power _b
* [@param](https://twitter.com/param) float _a
* [@param](https://twitter.com/param) integer _b
* [@return](https://twitter.com/return) string
**/
bcpow:function(_a, _b){
return Math.floor(Math.pow(parseFloat(_a), parseInt(_b)));
}
};
/**
* [@function](https://twitter.com/function) String.reverse
* [@description](https://twitter.com/description) Reverse a string
* [@return](https://twitter.com/return) string
**/
function reverse(str){
return str.split('').reverse().join('');
};
export default AlphabeticID

View file

@ -0,0 +1,96 @@
import { uploadMedia } from '../base/media/upload.mjs'
import config from '../base/config.mjs'
export default class IndexPost {
constructor(opts = {}) {
Object.assign(this, {
frontend: opts.frontend,
uploadMedia: uploadMedia,
})
}
register(server) {
this.serve = server.routes.serve
server.flaska.post('/', [
server.formidable({ maxFileSize: 8 * 1024 * 1024, }),
], this.createNewLink.bind(this))
}
hasErrors(ctx, hasMedia) {
if (!ctx.req.body.video) {
return 'Missing video link'
}
if (!ctx.req.body.video.startsWith('http')
|| !(ctx.req.body.video.includes('mp4')
|| ctx.req.body.video.includes('webm'))) {
return 'Video link has to be a valid full url and contain mp4 or webm in it'
}
if (!ctx.req.body.image && !hasMedia) {
return 'Missing image link or file'
}
if (ctx.req.body.image) {
if (!ctx.req.body.image.startsWith('http')
|| !(ctx.req.body.image.includes('jpg')
|| ctx.req.body.image.includes('jpeg')
|| ctx.req.body.image.includes('webp')
|| ctx.req.body.image.includes('png')
)
) {
return 'Image link has to be a valid full url and contain jpg, jpeg, webp or png'
}
}
}
/** PUT: /api/auth/articles/:id */
async createNewLink(ctx) {
let hasMedia = ctx.req.files.media && ctx.req.files.media.size
ctx.state.video = ctx.req.body.video
ctx.state.image = ctx.req.body.image
let redirect = ''
let error = this.hasErrors(ctx, hasMedia)
if (!error && hasMedia) {
try {
let temp = await this.uploadMedia(ctx.req.files.media)
ctx.state.image = ctx.req.body.image = 'https://cdn.nfp.is' + temp.sizes.small.jpeg.path
}
catch (err) {
ctx.log.error(err)
error = 'Unable to upload file: ' + err.message
}
}
if (!error) {
redirect = `${this.frontend}/?v=${ctx.state.video}&i=${ctx.state.image}`
}
if (!error) {
try {
let params = [
]
let res = await ctx.db.safeCallProc('discord_embed.link_add', params)
console.log(res)
}
catch (err) {
ctx.log.error(err)
error = 'Error while generating shortened link.'
}
}
if (redirect && !error) {
ctx.status = 302
ctx.headers['Location'] = redirect
ctx.type = 'text/html; charset=utf-8'
ctx.body = `
Redirecting
<a href="${redirect}">Click here if it doesn't redirect</a>
`
}
ctx.state.error = error
return this.serve.serveIndex(ctx)
}
}
// https://litter.catbox.moe/cnl6hy.mp4

View file

@ -0,0 +1,46 @@
import path from 'path'
import Parent from '../base/serve.mjs'
import fs from 'fs/promises'
import fsSync from 'fs'
import dot from 'dot'
import config from '../base/config.mjs'
export default class ServeHandler extends Parent {
loadTemplate(indexFile) {
this.template = dot.template(indexFile.toString(), { argName: [
'imageLink',
'videoLink',
'error',
'siteUrl',
'siteUrlBase',
'version',
'nonce',
'in_debug',
'inputVideo',
'inputImage'
] })
}
async serveIndex(ctx) {
if (config.get('NODE_ENV') === 'development') {
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
this.loadTemplate(indexFile)
}
let payload = {
imageLink: ctx.query.get('i') || '',
videoLink: ctx.query.get('v') || '',
error: ctx.state.error || '',
inputVideo: ctx.state.video || ctx.query.get('v') || '',
inputImage: ctx.state.image || ctx.query.get('i') || '',
siteUrl: this.frontend + ctx.url,
siteUrlBase: this.frontend + '/',
version: this.version,
nonce: ctx.state.nonce,
in_debug: config.get('NODE_ENV') === 'development' && false,
}
ctx.body = this.template(payload)
ctx.type = 'text/html; charset=utf-8'
}
}

View file

@ -0,0 +1,22 @@
import config from '../base/config.mjs'
import Parent from '../base/server.mjs'
import IndexPost from './post.mjs'
import ServeHandler from './serve.mjs'
export default class Server extends Parent {
init() {
super.init()
let localUtil = new this.core.sc.Util(import.meta.url)
this.routes = {
post: new IndexPost({
frontend: config.get('frontend:url'),
})
}
this.routes.serve = new ServeHandler({
root: localUtil.getPathFromRoot('../public'),
version: this.core.app.running,
frontend: config.get('frontend:url'),
})
}
}

1
discord_embed/base Symbolic link
View file

@ -0,0 +1 @@
../base

View file

@ -0,0 +1,8 @@
{
"scripts": {
"build": "echo done;"
},
"dependencies": {
"service-core": "^3.0.0-beta.17"
}
}

24
discord_embed/dev.mjs Normal file
View file

@ -0,0 +1,24 @@
import fs from 'fs'
import { ServiceCore } from 'service-core'
import * as index from './index.mjs'
const port = 4120
var core = new ServiceCore('nfp_moe', import.meta.url, port, '')
let config = {
frontend: {
url: 'http://localhost:' + port
}
}
try {
config = JSON.parse(fs.readFileSync('./config.json'))
} catch {}
config.port = port
core.setConfig(config)
core.init(index).then(function() {
return core.run()
})

11
discord_embed/index.mjs Normal file
View file

@ -0,0 +1,11 @@
import config from './base/config.mjs'
export function start(http, port, ctx) {
config.sources[1].store = ctx.config
return import('./api/server.mjs')
.then(function(module) {
let server = new module.default(http, port, ctx)
return server.run()
})
}

View file

@ -0,0 +1,47 @@
{
"name": "av1_embed",
"version": "1.0.0",
"port": 4120,
"description": "AV1 discord server embed helper",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"start": "node --experimental-modules index.mjs",
"dev:server": "node dev.mjs | bunyan",
"dev": "npm-watch dev:server"
},
"watch": {
"dev:server": {
"patterns": [
"api/*",
"base/*",
"../base/*"
],
"extensions": "js,mjs",
"quiet": true,
"inherit": true
}
},
"repository": {
"type": "git",
"url": "https://git.nfp.is/nfp/nfp_sites.git"
},
"author": "Jonatan Nilsson",
"license": "WTFPL",
"bugs": {
"url": "https://git.nfp.is/nfp/nfp_sites/issues"
},
"homepage": "https://git.nfp.is/nfp/nfp_sites",
"dependencies": {
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.0",
"formidable": "^1.2.6",
"ioredis": "^5.2.3",
"nconf-lite": "^2.0.0"
},
"devDependencies": {
"service-core": "^3.0.0-beta.17"
}
}

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

View file

@ -0,0 +1,192 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Discord Embedder from AV1 server</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ if (imageLink) { }}
<meta property="og:image" content="{{=imageLink}}">
<meta property="og:type" content="video.other">
<meta property="og:video:url" content="{{=videoLink}}">
<meta property="og:video:width" content="1280">
<meta property="og:video:height" content="720">
{{ } else { }}
<meta property="og:type" content="website" />
<meta property="og:url" content="{{=siteUrl}}" />
<meta property="og:image" content="/heart.png" />
<meta property="og:description" content="Simple site to help with embedding of AV1 videos and large/external videos into Discord." />
<meta property="og:title" content="Discord Embedder Helper Website" />
{{ } }}
<link rel="icon" type="image/png" href="/favicon.png">
<style>
:root {
--content-max-width: 1280px;
--bg: black;
--bg-content-alt: #333;
--color: #d7dadc;
--link: #bb4d00;
--button-border: 1px solid #f57c00;
--button-bg: #ffad42;
--button-fg: #000;
--error: red;
}
/* Box sizing rules */
*, *::before, *::after { box-sizing: border-box;
}
/* Remove default margin */
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd {
margin: 0;
}
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
font-size: 16px;
font-family: sans-serif;
background: var(--bg);
color: var(--color);
}
input, button, textarea, select {
font: inherit;
}
h1 {
font-size: 2.488rem;
}
h2 {
font-size: 2.074rem;
}
h3 {
font-size: 1.728rem;
}
h4 {
font-size: 1.44rem;
}
h5 {
font-size: 1.0rem;
}
a, a:visited, button {
text-decoration: none;
border: none;
padding: 0;
margin: 0;
font-weight: bold;
cursor: pointer;
}
input[type=text] {
border: 1px solid var(--color);
background: var(--bg);
color: var(--color);
border-radius: 0;
padding: 0.25rem;
line-height: 1rem;
outline: none;
width: 100%;
}
label {
font-size: 0.75rem;
font-weight: 500;
margin-top: 1rem;
margin-bottom: 0.25rem;
display: block;
}
input[type=text]:hover,
input[type=text]:active,
input[type=text]:focus {
border-color: var(--link);
}
input[type=text]:focus {
outline: 1px solid var(--link);
}
button,
input[type=submit] {
border: var(--button-border);
background: var(--button-bg);
color: var(--button-fg);
padding: 0.25rem 1rem;
margin: 1rem 0 2rem;
}
pre {
background: var(--bg-content-alt);
padding: 0.5rem;
}
.row {
display: flex;
flex-wrap: wrap;
}
.row-item {
flex: 2 0 200px;
align-self: flex-end;
}
.row-item input {
width: 100%;
}
.row-inbetween {
align-self: flex-end;
padding: 0.25rem 1rem;
}
.error {
color: var(--error);
font-size: 0.8rem;
padding: 1rem 1rem 0;
}
.inside {
width: 100%;
max-width: var(--content-max-width);
display: flex;
flex-direction: column;
align-items: stretch;
margin: 0 auto;
}
</style>
</head>
<body>
<form action="/" method="post" enctype="multipart/form-data" class="inside">
<h1>Create/generate embed url</h1>
{{ if (error) { }}<p class="error">{{=error}}</p>{{ } }}
<label>Video link</label>
<input type="text" name="video" value="{{=inputVideo}}">
<div class="row">
<div class="row-item">
<label>Image link (required for proper discord embed)</label>
<input type="text" name="image" value="{{=inputImage}}">
</div>
<span class="row-inbetween">or</span>
<div class="row-item">
<label>Upload image file (max 8MiB)</label>
<input type="file" name="media">
</div>
</div>
<input type="submit" value="Generate embed url">
<p>
Alternatively, you can generate a link yourself using the following syntax:
</p>
<pre>
{{=siteUrlBase}}?v=&lt;video link&gt;&amp;i=&lt;image link&gt;
</pre>
</form>
</body>
</html>

View file

@ -2,7 +2,6 @@ import path from 'path'
import striptags from 'striptags'
import Parent from '../base/serve.mjs'
import fs from 'fs/promises'
import dot from 'dot'
import config from '../base/config.mjs'
export default class ServeHandler extends Parent {
@ -49,11 +48,11 @@ export default class ServeHandler extends Parent {
async serveIndex(ctx) {
if (config.get('NODE_ENV') === 'development') {
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
this.template = dot.template(indexFile.toString(), { argName: ['headerDescription', 'headerImage', 'headerTitle', 'headerUrl', 'payloadData', 'payloadTree', 'version', 'nonce', 'type', 'banner', 'media', 'in_debug'] })
this.loadTemplate(indexFile)
}
let payload = {
headerDescription: 'Small fansubbing and scanlation group translating and encoding our favourite shows from Japan.',
headerDescription: 'A 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,
@ -87,7 +86,8 @@ export default class ServeHandler extends Parent {
if (data.page) {
payload.headerTitle = data.page.name + ' - NFP Moe'
if (data.page.content.blocks.length) {
payload.headerDescription = 'Page ' + data.page.name + ' over at NFP Moe. ' + payload.headerDescription
if (data.page.content.blocks?.length) {
payload.headerDescription = this.getDescriptionFromBlocks(data.page.content.blocks) || payload.headerDescription
}
if (data.page.media_alt_prefix) {
@ -114,6 +114,8 @@ export default class ServeHandler extends Parent {
payload.media = data.article?.media_avif_preview || false
payload.payloadData = JSON.stringify(data)
payload.type = 'article'
} else if (ctx.url !== '/login' && !ctx.url.startsWith('/admin/')) {
ctx.status = 404
}
} catch (e) {
ctx.log.error(e)