From f61d7509fafdb6dbc1ae1524b8b322dec140e7c8 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 14:23:45 +0000 Subject: [PATCH 01/11] Add file attachment support. Only supports a single file. Doesn't generate a blurhash, so Mastodon seems to show the image as "Not available" for a few seconds. --- design/partials/composer.handlebars | 3 ++- index.js | 5 +++-- lib/account.js | 31 ++++++++++++++++++++++++++--- lib/storage.js | 22 ++++++++++++++++++-- public/app.js | 26 +++++++++++++++++++++++- routes/admin.js | 21 +++++++++++++++++-- routes/public.js | 17 ++++++++++++++-- 7 files changed, 112 insertions(+), 13 deletions(-) diff --git a/design/partials/composer.handlebars b/design/partials/composer.handlebars index 5028afa..ed18b47 100644 --- a/design/partials/composer.handlebars +++ b/design/partials/composer.handlebars @@ -13,6 +13,7 @@ + @@ -27,4 +28,4 @@ input.focus(); input.selectionStart = input.selectionEnd = input.value.length; - \ No newline at end of file + diff --git a/index.js b/index.js index c24f051..2981f46 100644 --- a/index.js +++ b/index.js @@ -73,7 +73,8 @@ app.use(bodyParser.json({ type: 'application/activity+json' })); // support json encoded bodies app.use(bodyParser.json({ - type: 'application/json' + type: 'application/json', + limit: '4mb' // allow large bodies as attachments are base64 in JSON })); // support json encoded bodies app.use(cookieParser()) @@ -155,4 +156,4 @@ ensureAccount(USERNAME, DOMAIN).then((myaccount) => { http.createServer(app).listen(app.get('port'), function () { console.log('Express server listening on port ' + app.get('port')); }); -}); \ No newline at end of file +}); diff --git a/lib/account.js b/lib/account.js index c3fff64..b4e8674 100644 --- a/lib/account.js +++ b/lib/account.js @@ -25,7 +25,9 @@ import { createFileName, getFileName, boostsFile, - pathToDMs + pathToDMs, + writeMediaFile, + readMediaFile } from './storage.js'; import { getActivity, @@ -118,6 +120,16 @@ export const acceptDM = (dm, inboxUser) => { } +export const writeMedia = (filename, attachment) => { + logger('write media', filename, attachment.type); + writeMediaFile(filename, JSON.stringify(attachment)); // store the JSON, so we have the type and data in a single file +} + +export const readMedia = (filename) => { + logger('read media', filename); + return JSON.parse(readMediaFile(filename)); +} + export const isMyPost = (activity) => { return (activity.id.startsWith(`https://${DOMAIN}/m/`)); } @@ -335,7 +347,7 @@ export const sendToFollowers = async (object) => { } -export const createNote = async (body, cw, inReplyTo, toUser) => { +export const createNote = async (body, cw, inReplyTo, toUser, relativeAttachment) => { const publicAddress = "https://www.w3.org/ns/activitystreams#Public"; let d = new Date(); @@ -350,6 +362,19 @@ export const createNote = async (body, cw, inReplyTo, toUser) => { ActivityPub.actor.followers ]; + let attachment; + if (relativeAttachment) { + attachment = [ + { + type: 'Document', + mediaType: relativeAttachment.type.split('/')[0], + url: `https://${ DOMAIN }${relativeAttachment.relativeUrl}`, + name: '', + focalPoint: "0.0,0.0", + blurhash: null // not providing a blurhash seems to make Mastodon generate one itself, so it shows "Not available for a few seconds" + } + ]; + } // Contains mentions const tags = []; @@ -453,7 +478,7 @@ export const createNote = async (body, cw, inReplyTo, toUser) => { "atomUri": activityId, "inReplyToAtomUri": null, "content": content, - "attachment": [], + "attachment": attachment || [], "tag": tags, "replies": { "id": `${activityId}/replies`, diff --git a/lib/storage.js b/lib/storage.js index 48a5415..c0a8d5d 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -12,6 +12,7 @@ export const pathToFiles = path.resolve(dataDir, 'activitystream/'); export const pathToPosts = path.resolve(dataDir, 'posts/'); export const pathToUsers = path.resolve(dataDir, 'users/'); export const pathToDMs = path.resolve(dataDir, 'dms/'); +export const pathToMedia = path.resolve(dataDir, 'media/'); export const followersFile = path.resolve(dataDir, 'followers.json'); export const followingFile = path.resolve(dataDir, 'following.json'); @@ -199,7 +200,12 @@ const ensureDataFolder = () => { recursive: true }); } - + if (!fs.existsSync(path.resolve(pathToMedia))) { + logger('mkdir', pathToMedia); + fs.mkdirSync(path.resolve(pathToMedia), { + recursive: true + }); + } } @@ -225,6 +231,18 @@ export const readJSONDictionary = (path, defaultVal = []) => { } } +// data is JSON stringified string containing {type: mimetype, data: base64} +export const writeMediaFile = (filename, data) => { + logger('write media', filename); + fs.writeFileSync(path.join(pathToMedia, filename), data); +} + +// returns JSON stringified string containing {type: mimetype, data: base64} +export const readMediaFile = (filename) => { + logger('read media', filename); + return fs.readFileSync(path.join(pathToMedia, filename)); +} + export const writeJSONDictionary = (path, data) => { const now = new Date().getTime(); logger('write cache', path); @@ -240,4 +258,4 @@ logger('BUILDING INDEX'); ensureDataFolder(); buildIndex().then(() => { logger('INDEX BUILT!'); -}); \ No newline at end of file +}); diff --git a/public/app.js b/public/app.js index 57635ad..499bbc2 100644 --- a/public/app.js +++ b/public/app.js @@ -162,11 +162,34 @@ const app = { } return false; }, - post: () => { + readAttachment: async () => { + // read the file into base64, return mimtype and data + const files = document.getElementById('attachment').files; + if (files) { + return new Promise((resolve, reject) => { + let f = files[0]; // only read the first file + let reader = new FileReader(); + reader.onload = (function(theFile) { + return function(e) { + let base64 = btoa( + new Uint8Array(e.target.result) + .reduce((data, byte) => data + String.fromCharCode(byte), '') + ); + resolve({type: f.type, data: base64}); + }; + })(f); + reader.readAsArrayBuffer(f); + }); + } else { + resolve(null); + } + }, + post: async () => { const post = document.getElementById('post'); const cw = document.getElementById('cw'); const inReplyTo = document.getElementById('inReplyTo'); const to = document.getElementById('to'); + const attachment = await app.readAttachment(); const Http = new XMLHttpRequest(); const proxyUrl ='/private/post'; @@ -177,6 +200,7 @@ const app = { cw: cw.value, inReplyTo: inReplyTo.value, to: to.value, + attachment: attachment })); Http.onreadystatechange = () => { diff --git a/routes/admin.js b/routes/admin.js index 167bb0e..d15eaef 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -6,6 +6,7 @@ import { import express from 'express'; export const router = express.Router(); import debug from 'debug'; +import { createHash } from 'crypto'; import { getFollowers, getFollowing, @@ -20,7 +21,8 @@ import { isFollowing, getInboxIndex, getInbox, - writeInboxIndex + writeInboxIndex, + writeMedia, } from '../lib/account.js'; import { fetchUser @@ -341,7 +343,22 @@ router.get('/post', async(req, res) => { router.post('/post', async (req, res) => { // TODO: this is probably supposed to be a post to /api/outbox - const post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, req.body.to); + + let attachment; + + if (req.body.attachment) { + // get data from base64 to generate a hash + let data = Buffer.from(req.body.attachment.data, 'base64'); + let hash = createHash('md5').update(data).digest("hex"); + // use hash as filename, save the JSON (to keep mime type record as told by browser) + writeMedia(hash, req.body.attachment); + attachment = { + type: req.body.attachment.type, + relativeUrl: `/media/${hash}` + }; + } + + const post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, req.body.to, attachment); if (post.directMessage === true) { // return html partial of the new post for insertion in the feed res.status(200).render('partials/dm', { diff --git a/routes/public.js b/routes/public.js index 842aff3..7d57b5a 100644 --- a/routes/public.js +++ b/routes/public.js @@ -9,7 +9,8 @@ import { getNote, isMyPost, getAccount, - getOutboxPosts + getOutboxPosts, + readMedia } from '../lib/account.js'; import { getActivity, @@ -175,4 +176,16 @@ router.get('/notes/:guid', async (req, res) => { }); } } -}); \ No newline at end of file +}); + +router.get('/media/:id', async (req, res) => { + let attachment = readMedia(req.params.id); + if (attachment) { + res.setHeader('Content-Type', attachment.type); + let data = Buffer.from(attachment.data, 'base64'); + res.status(200).send(data); + } else { + res.status(404).send(); + } +}); + From 836a5cb58e3b7179f340d70109891c5e76af0fda Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 14:43:07 +0000 Subject: [PATCH 02/11] Fix posting with no attachment --- public/app.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app.js b/public/app.js index 499bbc2..e23d486 100644 --- a/public/app.js +++ b/public/app.js @@ -165,8 +165,8 @@ const app = { readAttachment: async () => { // read the file into base64, return mimtype and data const files = document.getElementById('attachment').files; - if (files) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { + if (files && files[0]) { let f = files[0]; // only read the first file let reader = new FileReader(); reader.onload = (function(theFile) { @@ -179,10 +179,10 @@ const app = { }; })(f); reader.readAsArrayBuffer(f); - }); - } else { - resolve(null); - } + } else { + resolve(null); + } + }); }, post: async () => { const post = document.getElementById('post'); From fdfcb20013ed9615fdaa377b29dd9447bfff196c Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 17:20:46 +0000 Subject: [PATCH 03/11] app.post() cannot be async, as onsubmit calls it synchronously. Instead, return immediately and handle the rest in .then() This was broken on Firefox, seemed ok in Chrome. --- public/app.js | 63 ++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/public/app.js b/public/app.js index e23d486..03b4347 100644 --- a/public/app.js +++ b/public/app.js @@ -184,47 +184,48 @@ const app = { } }); }, - post: async () => { + post: () => { const post = document.getElementById('post'); const cw = document.getElementById('cw'); const inReplyTo = document.getElementById('inReplyTo'); const to = document.getElementById('to'); - const attachment = await app.readAttachment(); - const Http = new XMLHttpRequest(); - const proxyUrl ='/private/post'; - Http.open("POST", proxyUrl); - Http.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - Http.send(JSON.stringify({ - post: post.value, - cw: cw.value, - inReplyTo: inReplyTo.value, - to: to.value, - attachment: attachment - })); + app.readAttachment().then((attachment) => { + const Http = new XMLHttpRequest(); + const proxyUrl ='/private/post'; + Http.open("POST", proxyUrl); + Http.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + Http.send(JSON.stringify({ + post: post.value, + cw: cw.value, + inReplyTo: inReplyTo.value, + to: to.value, + attachment: attachment + })); - Http.onreadystatechange = () => { - if (Http.readyState == 4 && Http.status == 200) { - console.log('posted!'); + Http.onreadystatechange = () => { + if (Http.readyState == 4 && Http.status == 200) { + console.log('posted!'); - // prepend the new post - const newHtml = Http.responseText; - const el = document.getElementById('home_stream') || document.getElementById('inbox_stream'); + // prepend the new post + const newHtml = Http.responseText; + const el = document.getElementById('home_stream') || document.getElementById('inbox_stream'); - if (!el) { - window.location = '/private/'; - } + if (!el) { + window.location = '/private/'; + } - // todo: ideally this would come back with all the html it needs - el.innerHTML = newHtml + el.innerHTML; + // todo: ideally this would come back with all the html it needs + el.innerHTML = newHtml + el.innerHTML; - // reset the inputs to blank - post.value = ''; - cw.value = ''; - } else { - console.error('HTTP PROXY CHANGE', Http); + // reset the inputs to blank + post.value = ''; + cw.value = ''; + } else { + console.error('HTTP PROXY CHANGE', Http); + } } - } + }); return false; }, replyTo: (activityId, mention) => { @@ -289,4 +290,4 @@ const app = { } return false; } -} \ No newline at end of file +} From 154234de49303805c65566cb34f7d20dab6bb815 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 17:24:23 +0000 Subject: [PATCH 04/11] Fix typo --- public/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app.js b/public/app.js index 03b4347..bf1fd0c 100644 --- a/public/app.js +++ b/public/app.js @@ -163,7 +163,7 @@ const app = { return false; }, readAttachment: async () => { - // read the file into base64, return mimtype and data + // read the file into base64, return mimetype and data const files = document.getElementById('attachment').files; return new Promise((resolve, reject) => { if (files && files[0]) { From 2b590ee091c157a01f62b94b207cfd7ddbeec7f9 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 19:13:09 +0000 Subject: [PATCH 05/11] Increase express max payload --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 2981f46..82a27d4 100644 --- a/index.js +++ b/index.js @@ -74,7 +74,7 @@ app.use(bodyParser.json({ })); // support json encoded bodies app.use(bodyParser.json({ type: 'application/json', - limit: '4mb' // allow large bodies as attachments are base64 in JSON + limit: '32mb' // allow large bodies as attachments are base64 in JSON })); // support json encoded bodies app.use(cookieParser()) From 0214d63ba06ffa849a925691e8ea0dc40d3d4e07 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 19:20:24 +0000 Subject: [PATCH 06/11] Move file structure logic out of storage.js --- lib/account.js | 4 ++-- lib/storage.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/account.js b/lib/account.js index b4e8674..cb98d90 100644 --- a/lib/account.js +++ b/lib/account.js @@ -122,12 +122,12 @@ export const acceptDM = (dm, inboxUser) => { export const writeMedia = (filename, attachment) => { logger('write media', filename, attachment.type); - writeMediaFile(filename, JSON.stringify(attachment)); // store the JSON, so we have the type and data in a single file + writeMediaFile(filename, attachment); // store the JSON, so we have the type and data in a single file } export const readMedia = (filename) => { logger('read media', filename); - return JSON.parse(readMediaFile(filename)); + return readMediaFile(filename); } export const isMyPost = (activity) => { diff --git a/lib/storage.js b/lib/storage.js index c0a8d5d..19551c5 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -234,13 +234,13 @@ export const readJSONDictionary = (path, defaultVal = []) => { // data is JSON stringified string containing {type: mimetype, data: base64} export const writeMediaFile = (filename, data) => { logger('write media', filename); - fs.writeFileSync(path.join(pathToMedia, filename), data); + fs.writeFileSync(path.join(pathToMedia, filename), JSON.stringify(data)); } // returns JSON stringified string containing {type: mimetype, data: base64} export const readMediaFile = (filename) => { logger('read media', filename); - return fs.readFileSync(path.join(pathToMedia, filename)); + return JSON.parse(fs.readFileSync(path.join(pathToMedia, filename))); } export const writeJSONDictionary = (path, data) => { From 0c2d5a898d6d3e2f9030003c45b2def8d8996a27 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 19:40:18 +0000 Subject: [PATCH 07/11] For media, store 2 files. for the binary data, .json for the metadata --- lib/account.js | 16 ++++++++-------- lib/storage.js | 12 +++++++++--- routes/admin.js | 13 ++++++------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/account.js b/lib/account.js index cb98d90..0ca81be 100644 --- a/lib/account.js +++ b/lib/account.js @@ -120,9 +120,9 @@ export const acceptDM = (dm, inboxUser) => { } -export const writeMedia = (filename, attachment) => { - logger('write media', filename, attachment.type); - writeMediaFile(filename, attachment); // store the JSON, so we have the type and data in a single file +export const writeMedia = (attachment) => { + logger('write media', attachment.hash, attachment.type); + writeMediaFile(attachment.hash, attachment); // store the JSON, so we have the type and data in a single file } export const readMedia = (filename) => { @@ -347,7 +347,7 @@ export const sendToFollowers = async (object) => { } -export const createNote = async (body, cw, inReplyTo, toUser, relativeAttachment) => { +export const createNote = async (body, cw, inReplyTo, toUser, attachmentInfo) => { const publicAddress = "https://www.w3.org/ns/activitystreams#Public"; let d = new Date(); @@ -363,13 +363,13 @@ export const createNote = async (body, cw, inReplyTo, toUser, relativeAttachment ]; let attachment; - if (relativeAttachment) { + if (attachmentInfo) { attachment = [ { type: 'Document', - mediaType: relativeAttachment.type.split('/')[0], - url: `https://${ DOMAIN }${relativeAttachment.relativeUrl}`, - name: '', + mediaType: attachmentInfo.type.split('/')[0], + url: `https://${ DOMAIN }/media/${attachmentInfo.hash}`, + name: attachmentInfo.name, focalPoint: "0.0,0.0", blurhash: null // not providing a blurhash seems to make Mastodon generate one itself, so it shows "Not available for a few seconds" } diff --git a/lib/storage.js b/lib/storage.js index 19551c5..03c731a 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -232,15 +232,21 @@ export const readJSONDictionary = (path, defaultVal = []) => { } // data is JSON stringified string containing {type: mimetype, data: base64} -export const writeMediaFile = (filename, data) => { +export const writeMediaFile = (filename, attachment) => { logger('write media', filename); - fs.writeFileSync(path.join(pathToMedia, filename), JSON.stringify(data)); + // write just the data part to file called + fs.writeFileSync(path.join(pathToMedia, filename), attachment.data); + delete attachment.data; + // write the remaining metadata to .json + fs.writeFileSync(path.join(pathToMedia, filename) + '.json', JSON.stringify(attachment)); } // returns JSON stringified string containing {type: mimetype, data: base64} export const readMediaFile = (filename) => { logger('read media', filename); - return JSON.parse(fs.readFileSync(path.join(pathToMedia, filename))); + let attachment = JSON.parse(fs.readFileSync(path.join(pathToMedia, filename) + '.json')); + attachment.data = fs.readFileSync(path.join(pathToMedia, filename)); + return attachment; } export const writeJSONDictionary = (path, data) => { diff --git a/routes/admin.js b/routes/admin.js index d15eaef..c27d34e 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -347,15 +347,14 @@ router.post('/post', async (req, res) => { let attachment; if (req.body.attachment) { - // get data from base64 to generate a hash - let data = Buffer.from(req.body.attachment.data, 'base64'); - let hash = createHash('md5').update(data).digest("hex"); - // use hash as filename, save the JSON (to keep mime type record as told by browser) - writeMedia(hash, req.body.attachment); + // convert attachment.data to raw buffer attachment = { type: req.body.attachment.type, - relativeUrl: `/media/${hash}` + data: Buffer.from(req.body.attachment.data, 'base64') }; + attachment.hash = createHash('md5').update(attachment.data).digest("hex"); + // use hash as filename + writeMedia(attachment); } const post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, req.body.to, attachment); @@ -562,4 +561,4 @@ router.post('/boost', async (req, res) => { }); } writeBoosts(boosts); -}); \ No newline at end of file +}); From 1508961e95d2ed3c072ad014ba889497045eb6ac Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 19:56:22 +0000 Subject: [PATCH 08/11] Add alt-text (description) for media uploads --- design/partials/composer.handlebars | 1 + lib/account.js | 12 ++++++++++-- public/app.js | 4 +++- routes/admin.js | 6 +++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/design/partials/composer.handlebars b/design/partials/composer.handlebars index ed18b47..50bb30d 100644 --- a/design/partials/composer.handlebars +++ b/design/partials/composer.handlebars @@ -14,6 +14,7 @@ + diff --git a/lib/account.js b/lib/account.js index 0ca81be..a9266e9 100644 --- a/lib/account.js +++ b/lib/account.js @@ -369,11 +369,19 @@ export const createNote = async (body, cw, inReplyTo, toUser, attachmentInfo) => type: 'Document', mediaType: attachmentInfo.type.split('/')[0], url: `https://${ DOMAIN }/media/${attachmentInfo.hash}`, - name: attachmentInfo.name, + name: attachmentInfo.description, focalPoint: "0.0,0.0", - blurhash: null // not providing a blurhash seems to make Mastodon generate one itself, so it shows "Not available for a few seconds" + blurhash: attachmentInfo.blurhash, + width: attachmentInfo.width, + height: attachmentInfo.height } ]; + if (attachmentInfo.width) { + attachment.width = attachmentInfo.width; + } + if (attachmentInfo.height) { + attachment.height = attachmentInfo.height; + } } // Contains mentions diff --git a/public/app.js b/public/app.js index bf1fd0c..b303c4d 100644 --- a/public/app.js +++ b/public/app.js @@ -189,6 +189,7 @@ const app = { const cw = document.getElementById('cw'); const inReplyTo = document.getElementById('inReplyTo'); const to = document.getElementById('to'); + const description = document.getElementById('description'); app.readAttachment().then((attachment) => { const Http = new XMLHttpRequest(); @@ -200,7 +201,8 @@ const app = { cw: cw.value, inReplyTo: inReplyTo.value, to: to.value, - attachment: attachment + attachment: attachment, + description: description.value })); Http.onreadystatechange = () => { diff --git a/routes/admin.js b/routes/admin.js index c27d34e..a12cf9a 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -350,7 +350,11 @@ router.post('/post', async (req, res) => { // convert attachment.data to raw buffer attachment = { type: req.body.attachment.type, - data: Buffer.from(req.body.attachment.data, 'base64') + data: Buffer.from(req.body.attachment.data, 'base64'), + description: req.body.description || '', + blurhash: null, + width: null, + height: null }; attachment.hash = createHash('md5').update(attachment.data).digest("hex"); // use hash as filename From 70c2917151c9c5b2bc04c8b08d228fe8f9fa515b Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 20:13:34 +0000 Subject: [PATCH 09/11] Add blurhash and image dimensions support Fix bug where sensitive was enabled by default (cw == undefined, not null) --- lib/account.js | 4 ++-- package.json | 4 +++- routes/admin.js | 19 +++++++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/account.js b/lib/account.js index a9266e9..9dba9ff 100644 --- a/lib/account.js +++ b/lib/account.js @@ -370,7 +370,7 @@ export const createNote = async (body, cw, inReplyTo, toUser, attachmentInfo) => mediaType: attachmentInfo.type.split('/')[0], url: `https://${ DOMAIN }/media/${attachmentInfo.hash}`, name: attachmentInfo.description, - focalPoint: "0.0,0.0", + focalPoint: attachmentInfo.focalPoint, blurhash: attachmentInfo.blurhash, width: attachmentInfo.width, height: attachmentInfo.height @@ -482,7 +482,7 @@ export const createNote = async (body, cw, inReplyTo, toUser, attachmentInfo) => "to": to, "cc": cc, directMessage, - "sensitive": cw !== null ? true : false, + "sensitive": cw ? true : false, "atomUri": activityId, "inReplyToAtomUri": null, "content": content, diff --git a/package.json b/package.json index fea5ed0..c7cdc8a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "md5": "^2.3.0", "moment": "^2.29.4", "node-fetch": "^3.3.0", - "rss-generator": "^0.0.3" + "rss-generator": "^0.0.3", + "blurhash": "^2.0.4", + "@andreekeberg/imagedata": "^1.0.2" }, "keywords": [ "fediverse", diff --git a/routes/admin.js b/routes/admin.js index a12cf9a..f838fed 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -33,6 +33,9 @@ import { import { ActivityPub } from '../lib/ActivityPub.js'; +import { encode as blurhashEncode } from 'blurhash'; +import { getSync as imageDataGetSync } from '@andreekeberg/imagedata' + const logger = debug('ono:admin'); router.get('/index', async (req, res) => { @@ -352,12 +355,20 @@ router.post('/post', async (req, res) => { type: req.body.attachment.type, data: Buffer.from(req.body.attachment.data, 'base64'), description: req.body.description || '', - blurhash: null, - width: null, - height: null }; + + // used as filename/id attachment.hash = createHash('md5').update(attachment.data).digest("hex"); - // use hash as filename + + if (attachment.type.split('/')[0] == 'image') { + // calculate dimensions and blurhash + let imageData = imageDataGetSync(attachment.data); + attachment.focalPoint = '0.0,0.0'; + attachment.width = imageData.width; + attachment.height = imageData.height; + attachment.blurhash = blurhashEncode(imageData.data, imageData.width, imageData.height, 4, 4); + } + writeMedia(attachment); } From 22759f6718c1b5f54b66f35c13de7613b306fce2 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 20:57:26 +0000 Subject: [PATCH 10/11] Use mimetype to choose meaningful filename for data file --- lib/storage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/storage.js b/lib/storage.js index 03c731a..eeae680 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -235,7 +235,7 @@ export const readJSONDictionary = (path, defaultVal = []) => { export const writeMediaFile = (filename, attachment) => { logger('write media', filename); // write just the data part to file called - fs.writeFileSync(path.join(pathToMedia, filename), attachment.data); + fs.writeFileSync(path.join(pathToMedia, filename + '.' + attachment.type.split('/')[1]), attachment.data); delete attachment.data; // write the remaining metadata to .json fs.writeFileSync(path.join(pathToMedia, filename) + '.json', JSON.stringify(attachment)); @@ -245,7 +245,7 @@ export const writeMediaFile = (filename, attachment) => { export const readMediaFile = (filename) => { logger('read media', filename); let attachment = JSON.parse(fs.readFileSync(path.join(pathToMedia, filename) + '.json')); - attachment.data = fs.readFileSync(path.join(pathToMedia, filename)); + attachment.data = fs.readFileSync(path.join(pathToMedia, filename + '.' + attachment.type.split('/')[1])); return attachment; } From 1b3b34bb68e4f240be546a3f4b75046bcd2b8de1 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 23:14:13 +0000 Subject: [PATCH 11/11] In the ActivityPub Note, set the URL of the media file to .ext (eg. abc123.png). This means that the media directory could be served by a static webserver. When a request comes in via existing /media/:id endpoint, drop the .ext and lookup the mime type (for Content-Type) via the metadata. --- lib/account.js | 2 +- lib/storage.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/account.js b/lib/account.js index 9dba9ff..44572ec 100644 --- a/lib/account.js +++ b/lib/account.js @@ -368,7 +368,7 @@ export const createNote = async (body, cw, inReplyTo, toUser, attachmentInfo) => { type: 'Document', mediaType: attachmentInfo.type.split('/')[0], - url: `https://${ DOMAIN }/media/${attachmentInfo.hash}`, + url: `https://${ DOMAIN }/media/${attachmentInfo.hash}.${attachmentInfo.type.split('/')[1]}`, name: attachmentInfo.description, focalPoint: attachmentInfo.focalPoint, blurhash: attachmentInfo.blurhash, diff --git a/lib/storage.js b/lib/storage.js index eeae680..65a4544 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -244,8 +244,10 @@ export const writeMediaFile = (filename, attachment) => { // returns JSON stringified string containing {type: mimetype, data: base64} export const readMediaFile = (filename) => { logger('read media', filename); - let attachment = JSON.parse(fs.readFileSync(path.join(pathToMedia, filename) + '.json')); - attachment.data = fs.readFileSync(path.join(pathToMedia, filename + '.' + attachment.type.split('/')[1])); + // remove any .{ext} from filename + let bareFilename = filename.replace(/\..*/, ''); + let attachment = JSON.parse(fs.readFileSync(path.join(pathToMedia, bareFilename) + '.json')); + attachment.data = fs.readFileSync(path.join(pathToMedia, bareFilename + '.' + attachment.type.split('/')[1])); return attachment; }