discord_embed: Add discord link refresher support. Add width and height support
Some checks are pending
/ deploy (push) Waiting to run
Some checks are pending
/ deploy (push) Waiting to run
This commit is contained in:
parent
28dd7e77fb
commit
38381f3bad
4 changed files with 140 additions and 41 deletions
|
@ -2,6 +2,8 @@ import { uploadMedia, deleteFile } from '../base/media/upload.mjs'
|
||||||
import config from '../base/config.mjs'
|
import config from '../base/config.mjs'
|
||||||
import AlphabeticID from './id.mjs'
|
import AlphabeticID from './id.mjs'
|
||||||
|
|
||||||
|
const ImageRegex = /\.(jpg|png|gif)(\?|$)/
|
||||||
|
|
||||||
export default class IndexPost {
|
export default class IndexPost {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
|
@ -16,40 +18,6 @@ export default class IndexPost {
|
||||||
server.flaska.post('/', [
|
server.flaska.post('/', [
|
||||||
server.formidable({ maxFileSize: 8 * 1024 * 1024, }),
|
server.formidable({ maxFileSize: 8 * 1024 * 1024, }),
|
||||||
], this.createNewLink.bind(this))
|
], this.createNewLink.bind(this))
|
||||||
server.flaska.get('/video/:id', this.videoRedirect.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
async videoRedirect(ctx) {
|
|
||||||
try {
|
|
||||||
let id = AlphabeticID.decode(ctx.params.id.slice(0,-5))
|
|
||||||
let videoLink = null
|
|
||||||
if (id) {
|
|
||||||
let res = await ctx.db.safeCallProc('discord_embed.link_get', [id - 3843])
|
|
||||||
if (res.first.length) {
|
|
||||||
videoLink = res.first[0].video_link
|
|
||||||
} else {
|
|
||||||
ctx.status = 404
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoLink) {
|
|
||||||
ctx.status = 302
|
|
||||||
ctx.headers['Location'] = videoLink
|
|
||||||
ctx.type = 'text/html; charset=utf-8'
|
|
||||||
ctx.body = `
|
|
||||||
Redirecting
|
|
||||||
<a href="${videoLink}">Click here if it doesn't redirect</a>
|
|
||||||
`
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
ctx.status = 404
|
|
||||||
ctx.state.error = 'Video not found.'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
ctx.log.error(err, 'Unable to fetch resource ' + ctx.url.slice(1))
|
|
||||||
ctx.state.error = 'Unknown error while fetching link.'
|
|
||||||
}
|
|
||||||
return this.serve.serveIndex(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasErrors(ctx, hasMedia) {
|
hasErrors(ctx, hasMedia) {
|
||||||
|
@ -61,6 +29,24 @@ Redirecting
|
||||||
return 'Video link has to be a valid full url'
|
return 'Video link has to be a valid full url'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImageRegex.exec(ctx.req.body.video)) {
|
||||||
|
return 'Image links are not gonna work, that is not how embedding video works. What are you even doing mate?'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.req.body.width) {
|
||||||
|
let n = Number(ctx.req.body.width)
|
||||||
|
if (isNaN(n) || n < 10 || n > 5000) {
|
||||||
|
return 'The video width does not look right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.req.body.height) {
|
||||||
|
let n = Number(ctx.req.body.height)
|
||||||
|
if (isNaN(n) || n < 10 || n > 3000) {
|
||||||
|
return 'The video height does not look right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.req.body.image) {
|
if (ctx.req.body.image) {
|
||||||
if (!ctx.req.body.image.startsWith('http')) {
|
if (!ctx.req.body.image.startsWith('http')) {
|
||||||
return 'Image link has to be a valid full url'
|
return 'Image link has to be a valid full url'
|
||||||
|
@ -77,6 +63,8 @@ Redirecting
|
||||||
async createNewLink(ctx) {
|
async createNewLink(ctx) {
|
||||||
ctx.state.video = ctx.req.body.video
|
ctx.state.video = ctx.req.body.video
|
||||||
ctx.state.image = ctx.req.body.image || 'https://cdn.nfp.is/av1/empty.png'
|
ctx.state.image = ctx.req.body.image || 'https://cdn.nfp.is/av1/empty.png'
|
||||||
|
ctx.state.width = ctx.req.body.width || null
|
||||||
|
ctx.state.height = ctx.req.body.height || null
|
||||||
|
|
||||||
let rateLimited = false
|
let rateLimited = false
|
||||||
let redisKey = 'ratelimit_' + ctx.req.ip.replace(/:/g, '-')
|
let redisKey = 'ratelimit_' + ctx.req.ip.replace(/:/g, '-')
|
||||||
|
@ -129,6 +117,8 @@ Redirecting
|
||||||
ctx.state.video,
|
ctx.state.video,
|
||||||
ctx.state.image,
|
ctx.state.image,
|
||||||
ctx.req.ip,
|
ctx.req.ip,
|
||||||
|
ctx.state.width,
|
||||||
|
ctx.state.height,
|
||||||
]
|
]
|
||||||
let res = await ctx.db.safeCallProc('discord_embed.link_add', params)
|
let res = await ctx.db.safeCallProc('discord_embed.link_add', params)
|
||||||
let id = AlphabeticID.encode(res.first[0].id + 3843)
|
let id = AlphabeticID.encode(res.first[0].id + 3843)
|
||||||
|
|
|
@ -7,11 +7,15 @@ import dot from 'dot'
|
||||||
import config from '../base/config.mjs'
|
import config from '../base/config.mjs'
|
||||||
import AlphabeticID from './id.mjs'
|
import AlphabeticID from './id.mjs'
|
||||||
|
|
||||||
|
const ExpirationRegex = /ex=([0-9a-fA-F]{8})/
|
||||||
|
|
||||||
export default class ServeHandler extends Parent {
|
export default class ServeHandler extends Parent {
|
||||||
loadTemplate(indexFile) {
|
loadTemplate(indexFile) {
|
||||||
this.template = dot.template(indexFile.toString(), { argName: [
|
this.template = dot.template(indexFile.toString(), { argName: [
|
||||||
'imageLink',
|
'imageLink',
|
||||||
'videoLink',
|
'videoLink',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
'error',
|
'error',
|
||||||
'siteUrl',
|
'siteUrl',
|
||||||
'siteUrlBase',
|
'siteUrlBase',
|
||||||
|
@ -19,12 +23,28 @@ export default class ServeHandler extends Parent {
|
||||||
'nonce',
|
'nonce',
|
||||||
'in_debug',
|
'in_debug',
|
||||||
'inputVideo',
|
'inputVideo',
|
||||||
'inputImage'
|
'inputImage',
|
||||||
|
'inputWidth',
|
||||||
|
'inputHeight'
|
||||||
], strip: false })
|
], strip: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshUrl(link) {
|
||||||
|
var res = await fetch('https://discord.com/api/v10/attachments/refresh-urls', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bot ${config.get('discord_token')}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ attachment_urls: [link] })
|
||||||
|
})
|
||||||
|
let output = await res.json()
|
||||||
|
return output?.refreshed_urls?.[0].refreshed
|
||||||
|
}
|
||||||
|
|
||||||
register(server) {
|
register(server) {
|
||||||
super.register(server)
|
super.register(server)
|
||||||
|
server.flaska.get('/video/:id', this.videoRedirect.bind(this))
|
||||||
server.flaska.onerror(this.serveError.bind(this))
|
server.flaska.onerror(this.serveError.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,13 +61,19 @@ export default class ServeHandler extends Parent {
|
||||||
|
|
||||||
let videoLink = ctx.query.get('v') || ''
|
let videoLink = ctx.query.get('v') || ''
|
||||||
let imageLink = ctx.query.get('i') || ''
|
let imageLink = ctx.query.get('i') || ''
|
||||||
|
let width = ctx.query.get('w') || 1280
|
||||||
|
let height = ctx.query.get('h') || 720
|
||||||
|
|
||||||
ctx.body = this.template({
|
ctx.body = this.template({
|
||||||
videoLink: videoLink,
|
videoLink: videoLink,
|
||||||
imageLink: imageLink,
|
imageLink: imageLink,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
error: ctx.state.error || '',
|
error: ctx.state.error || '',
|
||||||
inputVideo: ctx.state.video || videoLink || '',
|
inputVideo: ctx.state.video || videoLink || '',
|
||||||
inputImage: ctx.state.image || imageLink || '',
|
inputImage: ctx.state.image || imageLink || '',
|
||||||
|
inputWidth: width,
|
||||||
|
inputHeight: height,
|
||||||
siteUrl: this.frontend + ctx.url,
|
siteUrl: this.frontend + ctx.url,
|
||||||
siteUrlBase: this.frontend + '/',
|
siteUrlBase: this.frontend + '/',
|
||||||
version: this.version,
|
version: this.version,
|
||||||
|
@ -57,6 +83,39 @@ export default class ServeHandler extends Parent {
|
||||||
ctx.type = 'text/html; charset=utf-8'
|
ctx.type = 'text/html; charset=utf-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async videoRedirect(ctx) {
|
||||||
|
try {
|
||||||
|
let id = AlphabeticID.decode(ctx.params.id.slice(0,-5))
|
||||||
|
let videoLink = null
|
||||||
|
if (id) {
|
||||||
|
let res = await ctx.db.safeCallProc('discord_embed.link_get', [id - 3843])
|
||||||
|
if (res.first.length) {
|
||||||
|
videoLink = res.first[0].video_link
|
||||||
|
} else {
|
||||||
|
ctx.status = 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoLink) {
|
||||||
|
ctx.status = 302
|
||||||
|
ctx.headers['Location'] = videoLink
|
||||||
|
ctx.type = 'text/html; charset=utf-8'
|
||||||
|
ctx.body = `
|
||||||
|
Redirecting
|
||||||
|
<a href="${videoLink}">Click here if it doesn't redirect</a>
|
||||||
|
`
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
ctx.status = 404
|
||||||
|
ctx.state.error = 'Video not found.'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ctx.log.error(err, 'Unable to fetch resource ' + ctx.url.slice(1))
|
||||||
|
ctx.state.error = 'Unknown error while fetching link.'
|
||||||
|
}
|
||||||
|
return this.serveIndex(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
async serveIndex(ctx) {
|
async serveIndex(ctx) {
|
||||||
if (config.get('NODE_ENV') === 'development') {
|
if (config.get('NODE_ENV') === 'development') {
|
||||||
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
|
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
|
||||||
|
@ -65,11 +124,14 @@ export default class ServeHandler extends Parent {
|
||||||
|
|
||||||
let videoLink = ctx.query.get('v') || ''
|
let videoLink = ctx.query.get('v') || ''
|
||||||
let imageLink = ctx.query.get('i') || (videoLink ? 'https://cdn.nfp.is/av1/empty.png' : '')
|
let imageLink = ctx.query.get('i') || (videoLink ? 'https://cdn.nfp.is/av1/empty.png' : '')
|
||||||
|
let width = ctx.query.get('w') || 1280
|
||||||
|
let height = ctx.query.get('h') || 720
|
||||||
|
let id = null
|
||||||
|
|
||||||
if (!ctx.state.error) {
|
if (!ctx.state.error) {
|
||||||
if (ctx.url.match(/^\/[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]+$/) && ctx.url.length < 7) {
|
if (ctx.url.match(/^\/[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]+$/) && ctx.url.length < 7) {
|
||||||
try {
|
try {
|
||||||
let id = AlphabeticID.decode(ctx.url.slice(1))
|
id = AlphabeticID.decode(ctx.url.slice(1))
|
||||||
if (id) {
|
if (id) {
|
||||||
let res = await ctx.db.safeCallProc('discord_embed.link_get', [id - 3843])
|
let res = await ctx.db.safeCallProc('discord_embed.link_get', [id - 3843])
|
||||||
if (res.first.length) {
|
if (res.first.length) {
|
||||||
|
@ -80,6 +142,8 @@ export default class ServeHandler extends Parent {
|
||||||
videoLink = this.frontend + '/video/' + ctx.url.slice(1) + '.webm'
|
videoLink = this.frontend + '/video/' + ctx.url.slice(1) + '.webm'
|
||||||
}
|
}
|
||||||
imageLink = res.first[0].image_link
|
imageLink = res.first[0].image_link
|
||||||
|
width = res.first[0].width || width
|
||||||
|
height = res.first[0].height || height
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404
|
ctx.status = 404
|
||||||
}
|
}
|
||||||
|
@ -94,15 +158,40 @@ export default class ServeHandler extends Parent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoLink.startsWith('https://cdn.discordapp.com')) {
|
if (videoLink.startsWith('https://cdn.discordapp.com')) {
|
||||||
|
if (id) {
|
||||||
|
let match = ExpirationRegex.exec(videoLink)
|
||||||
|
if (match && match[1]) {
|
||||||
|
try {
|
||||||
|
let expiration = Number('0x' + match[1]) * 1000
|
||||||
|
if (new Date() > new Date(expiration)) {
|
||||||
|
let newLink = await this.refreshUrl(videoLink)
|
||||||
|
if (newLink) {
|
||||||
|
ctx.log.info({
|
||||||
|
old: videoLink,
|
||||||
|
new: newLink,
|
||||||
|
}, 'Updating link')
|
||||||
|
videoLink = newLink
|
||||||
|
await ctx.db.safeCallProc('discord_embed.link_update', [id - 3843, videoLink])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ctx.log.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
videoLink = videoLink.replace('https://cdn.discordapp.com', 'https://discordproxy.nfp.is')
|
videoLink = videoLink.replace('https://cdn.discordapp.com', 'https://discordproxy.nfp.is')
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload = {
|
let payload = {
|
||||||
videoLink: videoLink,
|
videoLink: videoLink,
|
||||||
imageLink: imageLink,
|
imageLink: imageLink,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
error: ctx.state.error || '',
|
error: ctx.state.error || '',
|
||||||
inputVideo: ctx.state.video || videoLink || '',
|
inputVideo: ctx.state.video || videoLink || '',
|
||||||
inputImage: ctx.state.image || imageLink || '',
|
inputImage: ctx.state.image || imageLink || '',
|
||||||
|
inputWidth: width,
|
||||||
|
inputHeight: height,
|
||||||
siteUrl: this.frontend + ctx.url,
|
siteUrl: this.frontend + ctx.url,
|
||||||
siteUrlBase: this.frontend + '/',
|
siteUrlBase: this.frontend + '/',
|
||||||
version: this.version,
|
version: this.version,
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default class Server extends Parent {
|
||||||
static: new StaticRoutes(),
|
static: new StaticRoutes(),
|
||||||
post: new IndexPost({
|
post: new IndexPost({
|
||||||
frontend: config.get('frontend:url'),
|
frontend: config.get('frontend:url'),
|
||||||
})
|
}),
|
||||||
}
|
}
|
||||||
this.routes.serve = new ServeHandler({
|
this.routes.serve = new ServeHandler({
|
||||||
root: localUtil.getPathFromRoot('../public'),
|
root: localUtil.getPathFromRoot('../public'),
|
||||||
|
@ -32,7 +32,6 @@ export default class Server extends Parent {
|
||||||
runCreateServer() {
|
runCreateServer() {
|
||||||
super.runCreateServer()
|
super.runCreateServer()
|
||||||
|
|
||||||
|
|
||||||
this.flaska.before((ctx) => {
|
this.flaska.before((ctx) => {
|
||||||
ctx.redis = this.redis
|
ctx.redis = this.redis
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
<meta property="og:image" content="{{=imageLink}}">
|
<meta property="og:image" content="{{=imageLink}}">
|
||||||
<meta property="og:type" content="video.other">
|
<meta property="og:type" content="video.other">
|
||||||
<meta property="og:video:url" content="{{=videoLink}}">
|
<meta property="og:video:url" content="{{=videoLink}}">
|
||||||
<meta property="og:video:width" content="1280">
|
<meta property="og:video:width" content="{{=width}}">
|
||||||
<meta property="og:video:height" content="720">
|
<meta property="og:video:height" content="{{=height}}">
|
||||||
{{ } else { }}
|
{{ } else { }}
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="{{=siteUrl}}" />
|
<meta property="og:url" content="{{=siteUrl}}" />
|
||||||
|
@ -264,6 +264,18 @@ pre {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row optional">
|
||||||
|
<div class="row-item">
|
||||||
|
<label>Optional: Overwrite aspect ratio from default 16:9 (1280x720)</label>
|
||||||
|
<input id="inputWidth" type="text" name="width" value="{{=inputWidth}}">
|
||||||
|
</div>
|
||||||
|
<span class="row-inbetween">x</span>
|
||||||
|
<div class="row-item">
|
||||||
|
<label> </label>
|
||||||
|
<input id="inputHeight" type="text" name="height" value="{{=inputHeight}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="testing" class="hidden">
|
<div id="testing" class="hidden">
|
||||||
<p>Test area (does the video load and play? If not, it might not work on discord)</p>
|
<p>Test area (does the video load and play? If not, it might not work on discord)</p>
|
||||||
|
|
||||||
|
@ -287,6 +299,8 @@ pre {
|
||||||
var generateurl = document.getElementById('generateurl');
|
var generateurl = document.getElementById('generateurl');
|
||||||
var inputVideo = document.getElementById('inputVideo');
|
var inputVideo = document.getElementById('inputVideo');
|
||||||
var inputImage = document.getElementById('inputImage');
|
var inputImage = document.getElementById('inputImage');
|
||||||
|
var inputWidth = document.getElementById('inputWidth');
|
||||||
|
var inputHeight = document.getElementById('inputHeight');
|
||||||
var previewVideo = document.getElementById('previewVideo');
|
var previewVideo = document.getElementById('previewVideo');
|
||||||
var isExample = true;
|
var isExample = true;
|
||||||
previewVideo.poster = defaultPoster;
|
previewVideo.poster = defaultPoster;
|
||||||
|
@ -302,7 +316,10 @@ pre {
|
||||||
generateurl.innerText = baseSite + '?v=<video link>';
|
generateurl.innerText = baseSite + '?v=<video link>';
|
||||||
previewVideo.classList.add('hidden')
|
previewVideo.classList.add('hidden')
|
||||||
} else {
|
} else {
|
||||||
generateurl.innerText = baseSite + '?v=' + encodeURIComponent(inputVideo.value) + (inputImage.value ? '&i=' + encodeURIComponent(inputImage.value) : '');
|
generateurl.innerText = baseSite + '?v=' + encodeURIComponent(inputVideo.value)
|
||||||
|
+ (inputImage.value ? '&i=' + encodeURIComponent(inputImage.value) : '')
|
||||||
|
+ (inputWidth.value !== '1280' ? '&w=' + encodeURIComponent(inputWidth.value) : '')
|
||||||
|
+ (inputHeight.value !== '720' ? '&h=' + encodeURIComponent(inputHeight.value) : '');
|
||||||
|
|
||||||
previewVideo.pause();
|
previewVideo.pause();
|
||||||
if (inputVideo.value !== existingVideoUrl) {
|
if (inputVideo.value !== existingVideoUrl) {
|
||||||
|
@ -322,8 +339,12 @@ pre {
|
||||||
|
|
||||||
inputVideo.addEventListener('change', checkChange);
|
inputVideo.addEventListener('change', checkChange);
|
||||||
inputImage.addEventListener('change', checkChange);
|
inputImage.addEventListener('change', checkChange);
|
||||||
|
inputWidth.addEventListener('change', checkChange);
|
||||||
|
inputHeight.addEventListener('change', checkChange);
|
||||||
inputVideo.addEventListener('keyup', checkChange);
|
inputVideo.addEventListener('keyup', checkChange);
|
||||||
inputImage.addEventListener('keyup', checkChange);
|
inputImage.addEventListener('keyup', checkChange);
|
||||||
|
inputWidth.addEventListener('keyup', checkChange);
|
||||||
|
inputHeight.addEventListener('keyup', checkChange);
|
||||||
checkChange()
|
checkChange()
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in a new issue