From 6e76a800673ccd1dbbf9481fdcb7a476090d367b Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 09:24:23 +0000 Subject: [PATCH 01/58] Modify /poll so that it blocks until an event is received Send an event when a notification is added --- lib/account.js | 4 ++++ public/app.js | 4 ++-- routes/admin.js | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/account.js b/lib/account.js index 9f705f0..c898ea1 100644 --- a/lib/account.js +++ b/lib/account.js @@ -31,6 +31,9 @@ import { getActivity, createActivity } from './notes.js'; +import { + UserEvent +} from '../lib/UserEvent.js'; import debug from 'debug'; import MarkdownIt from 'markdown-it'; @@ -228,6 +231,7 @@ export const addNotification = (notification) => { notification: notification, }); writeNotifications(notifications); + UserEvent.sendEvent('notification'); } const writeNotifications = (notifications) => { diff --git a/public/app.js b/public/app.js index fdce550..74a1606 100644 --- a/public/app.js +++ b/public/app.js @@ -89,7 +89,7 @@ const app = { fetch('/private/poll','get').then((json) => { const res = JSON.parse(json); app.alertNewPosts(res); - setTimeout(() => app.pollForPosts(), 30000); // poll every 5 seconds + setTimeout(() => app.pollForPosts(), 1000); // poll every 1 seconds, endpoint will stall until event occurs }).catch((err) => { console.error(err); }); @@ -242,4 +242,4 @@ const app = { } return false; } -} \ No newline at end of file +} diff --git a/routes/admin.js b/routes/admin.js index bc56357..28a768b 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -31,6 +31,9 @@ import { import { ActivityPub } from '../lib/ActivityPub.js'; +import { + UserEvent +} from '../lib/UserEvent.js'; const logger = debug('ono:admin'); router.get('/index', async (req, res) => { @@ -38,12 +41,12 @@ router.get('/index', async (req, res) => { }); router.get('/poll', async (req, res) => { + await UserEvent.waitForEvent(); const sincePosts = new Date(req.cookies.latestPost).getTime(); const sinceNotifications = parseInt(req.cookies.latestNotification); const notifications = getNotifications().filter((n) => n.time > sinceNotifications); - const { activitystream } = await getActivitySince(sincePosts, true); @@ -516,4 +519,4 @@ router.post('/boost', async (req, res) => { }); } writeBoosts(boosts); -}); \ No newline at end of file +}); From 9c3bf60223f4830acf8f757f16902597fde77e29 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 10:18:51 +0000 Subject: [PATCH 02/58] Add missing UserEvent.js file --- lib/UserEvent.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/UserEvent.js diff --git a/lib/UserEvent.js b/lib/UserEvent.js new file mode 100644 index 0000000..9c4de6b --- /dev/null +++ b/lib/UserEvent.js @@ -0,0 +1,28 @@ +import { once, EventEmitter } from 'events'; +import debug from 'debug'; + +const logger = debug('ono:event'); + +/** + * UserEvent - a class for handling real-time event updates + */ +export class UserEventClient { + + constructor() { + this.emitter = new EventEmitter(); + } + + sendEvent(e, data) { + logger('SENDING EVENT ' + data); + this.emitter.emit('poll', data); + } + + async waitForEvent() { + const [data] = await once(this.emitter, 'poll'); + logger('RECEIVED EVENT ' + data); + } + +} + +export const UserEvent = new UserEventClient(); + From f61d7509fafdb6dbc1ae1524b8b322dec140e7c8 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 14:23:45 +0000 Subject: [PATCH 03/58] 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 04/58] 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 05/58] 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 06/58] 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 07/58] 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 08/58] 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 09/58] 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 10/58] 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 11/58] 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 12/58] 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 3e437557ecf0636c64b4d0fc9f9d8582f0c823a1 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 21:12:55 +0000 Subject: [PATCH 13/58] On first poll, immediately return with results --- public/app.js | 4 +++- routes/admin.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app.js b/public/app.js index f33098b..b0cbbf8 100644 --- a/public/app.js +++ b/public/app.js @@ -38,6 +38,7 @@ const getCookie = (name) => { } const app = { + firstPoll: true, newPosts: 0, newNotifications: 0, latestPost: (date) => { @@ -102,7 +103,8 @@ const app = { }, pollForPosts: () => { - fetch('/private/poll','get').then((json) => { + fetch('/private/poll' + (app.firstPoll ? '?nowait=1' : ''),'get').then((json) => { + app.firstPoll = false; const res = JSON.parse(json); app.alertNewPosts(res); setTimeout(() => app.pollForPosts(), 1000); // poll every 1 seconds, endpoint will stall until event occurs diff --git a/routes/admin.js b/routes/admin.js index be50e1c..330197b 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -41,7 +41,9 @@ router.get('/index', async (req, res) => { }); router.get('/poll', async (req, res) => { - await UserEvent.waitForEvent(); + if (!req.query.nowait) { + await UserEvent.waitForEvent(); + } const sincePosts = new Date(req.cookies.latestPost).getTime(); const sinceNotifications = parseInt(req.cookies.latestNotification); From e89101221243c27afcbf6bf20696adfe700b5906 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 21:28:12 +0000 Subject: [PATCH 14/58] When /poll request is closed, abort polling. https://nodejs.org/api/events.html#eventsonemitter-eventname-options --- lib/UserEvent.js | 8 +++++++- routes/admin.js | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/UserEvent.js b/lib/UserEvent.js index 9c4de6b..cf5a07d 100644 --- a/lib/UserEvent.js +++ b/lib/UserEvent.js @@ -10,6 +10,7 @@ export class UserEventClient { constructor() { this.emitter = new EventEmitter(); + this.ac = new AbortController(); } sendEvent(e, data) { @@ -17,8 +18,13 @@ export class UserEventClient { this.emitter.emit('poll', data); } + abort() { + this.ac.abort(); + this.ac = new AbortController(); + } + async waitForEvent() { - const [data] = await once(this.emitter, 'poll'); + const [data] = await once(this.emitter, 'poll', { signal: this.ac.signal }); logger('RECEIVED EVENT ' + data); } diff --git a/routes/admin.js b/routes/admin.js index 330197b..ed3881f 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -42,9 +42,16 @@ router.get('/index', async (req, res) => { router.get('/poll', async (req, res) => { if (!req.query.nowait) { - await UserEvent.waitForEvent(); + req.on('close', function (err){ + UserEvent.abort(); + return; + }); + try { + await UserEvent.waitForEvent(); + } catch(e) { + // we got aborted + } } - const sincePosts = new Date(req.cookies.latestPost).getTime(); const sinceNotifications = parseInt(req.cookies.latestNotification); const notifications = getNotifications().filter((n) => n.time > sinceNotifications); From 1b3b34bb68e4f240be546a3f4b75046bcd2b8de1 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 23:14:13 +0000 Subject: [PATCH 15/58] 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; } From 3e48825445f0dd9093210ff1594326ba299c62f0 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Fri, 23 Dec 2022 00:14:44 +0000 Subject: [PATCH 16/58] Process hashtags in content body. For each found, add a Hashtag object in tags[] and rewrite link to domain/tags/xyz (which won't resolve yet). Posting a message with a hashtag makes it discoverable from another instance --- lib/account.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/account.js b/lib/account.js index c3fff64..203ce7d 100644 --- a/lib/account.js +++ b/lib/account.js @@ -432,8 +432,21 @@ export const createNote = async (body, cw, inReplyTo, toUser) => { } } + // if it contains hashtags, add them + body.match(/#[a-z]+/gi).forEach((tag) => { + tags.push({ + "type": "Hashtag", + "href": `https://${ DOMAIN }/tags/${tag}`, + "name": tag + }); + }); + + const mdContent = md.render(processedContent); + + // rewrite links to tags in content + let content = mdContent.replace(/#([a-z]+)/gi, ''); - const content = md.render(processedContent); + content = '' + content + ''; const activityId = `https://${ DOMAIN }/m/${guid}`; const url = `https://${ DOMAIN }/notes/${guid}`; From 1cd0f58d6129b6e7a8610f4b1583af27bcbe5a02 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Fri, 23 Dec 2022 00:40:16 +0000 Subject: [PATCH 17/58] Add route and placeholder template for /tags/:tag so clicking a tag doesn't error. --- design/public/tag.handlebars | 13 +++++++++++++ routes/public.js | 11 ++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 design/public/tag.handlebars diff --git a/design/public/tag.handlebars b/design/public/tag.handlebars new file mode 100644 index 0000000..a47cb93 --- /dev/null +++ b/design/public/tag.handlebars @@ -0,0 +1,13 @@ +
+ +
+ + Back to {{actor.preferredUsername}} +
+#{{tag}} +
diff --git a/routes/public.js b/routes/public.js index 842aff3..f8f64a2 100644 --- a/routes/public.js +++ b/routes/public.js @@ -175,4 +175,13 @@ router.get('/notes/:guid', async (req, res) => { }); } } -}); \ No newline at end of file +}); + +router.get('/tags/:tag', async (req, res) => { + res.render('public/tag', { + tag: req.params.tag, + layout: 'public', + domain: DOMAIN, + user: USERNAME + }); +}); From 943c2c99b077f6de249cb02c46a29b8eae189bc2 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Fri, 23 Dec 2022 02:05:32 +0000 Subject: [PATCH 18/58] Render Question type, just a disabled form for now, but less confusing when seeing a post --- design/partials/note.handlebars | 15 +++++++++++++++ index.js | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/design/partials/note.handlebars b/design/partials/note.handlebars index 27139b2..018f13c 100644 --- a/design/partials/note.handlebars +++ b/design/partials/note.handlebars @@ -14,6 +14,21 @@

Show Thread

{{/if}} + {{#if (isEq note.type 'Question')}} +
+ {{#each note.oneOf}} + + ({{replies.totalItems}})
+ {{/each}} + {{#each note.anyOf}} + + ({{replies.totalItems}})
+ {{/each}} +
+ Ends at: {{note.endTime}}
+ Votes cast: {{note.votersCount}}
+ {{/if}} + {{#each note.attachment}}
{{#isImage mediaType}} diff --git a/index.js b/index.js index c24f051..2b1135a 100644 --- a/index.js +++ b/index.js @@ -48,7 +48,7 @@ const hbs = create({ if (str && str.includes('image')) return options.fn(this); }, isEq: (a, b, options) => { - if (a === b) return options.fn(this); + return a === b }, or: (a, b, options) => { return a || b @@ -155,4 +155,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 +}); From d0cfbeec11d0613c80d087cdcbdbc602d50cbcc5 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Fri, 23 Dec 2022 09:54:54 +0000 Subject: [PATCH 19/58] Add voting in polls, with multiple Note sends for anyOf Questions. --- design/partials/composer.handlebars | 16 +++++++++++++++- design/partials/note.handlebars | 22 ++++++++++++---------- lib/account.js | 6 +++++- public/app.js | 19 ++++++++++++++++--- routes/admin.js | 18 ++++++++++++++++-- 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/design/partials/composer.handlebars b/design/partials/composer.handlebars index 5028afa..845de67 100644 --- a/design/partials/composer.handlebars +++ b/design/partials/composer.handlebars @@ -9,10 +9,24 @@
Compose
{{/if}}
+ {{#if names}} + Vote for: +
    + {{#each names}} +
  • {{this}}
  • + {{/each}} +
+ + + {{else}} + {{/if}} + {{#each names}} + + {{/each}}
@@ -27,4 +41,4 @@ input.focus(); input.selectionStart = input.selectionEnd = input.value.length; - \ No newline at end of file + diff --git a/design/partials/note.handlebars b/design/partials/note.handlebars index 018f13c..dd66aff 100644 --- a/design/partials/note.handlebars +++ b/design/partials/note.handlebars @@ -15,16 +15,18 @@ {{/if}} {{#if (isEq note.type 'Question')}} -
- {{#each note.oneOf}} - - ({{replies.totalItems}})
- {{/each}} - {{#each note.anyOf}} - - ({{replies.totalItems}})
- {{/each}} -
+
+
+ {{#each note.oneOf}} + + ({{replies.totalItems}})
+ {{/each}} + {{#each note.anyOf}} + + ({{replies.totalItems}})
+ {{/each}} +
+
Ends at: {{note.endTime}}
Votes cast: {{note.votersCount}}
{{/if}} diff --git a/lib/account.js b/lib/account.js index c3fff64..9ae778d 100644 --- a/lib/account.js +++ b/lib/account.js @@ -335,7 +335,7 @@ export const sendToFollowers = async (object) => { } -export const createNote = async (body, cw, inReplyTo, toUser) => { +export const createNote = async (body, cw, inReplyTo, name, toUser) => { const publicAddress = "https://www.w3.org/ns/activitystreams#Public"; let d = new Date(); @@ -467,6 +467,10 @@ export const createNote = async (body, cw, inReplyTo, toUser) => { } }; + if (name) { + object.name = name; + } + if (directMessage) { acceptDM(object, to[0]); } else { diff --git a/public/app.js b/public/app.js index 57635ad..bbbe779 100644 --- a/public/app.js +++ b/public/app.js @@ -167,6 +167,8 @@ const app = { const cw = document.getElementById('cw'); const inReplyTo = document.getElementById('inReplyTo'); const to = document.getElementById('to'); + // get hidden elements for poll choices + const names = Array.from(document.querySelectorAll('input[class="pollchoice"]')).map((item) => {return item.value}); const Http = new XMLHttpRequest(); const proxyUrl ='/private/post'; @@ -177,6 +179,7 @@ const app = { cw: cw.value, inReplyTo: inReplyTo.value, to: to.value, + names: names })); Http.onreadystatechange = () => { @@ -204,8 +207,18 @@ const app = { return false; }, replyTo: (activityId, mention) => { - - window.location = '/private/post?inReplyTo=' + activityId; + // get poll form response + let pollChoices = []; + Array.from(document.getElementById(activityId).getElementsByTagName('input')).forEach((inp) => { + if (inp.checked) { + pollChoices.push(inp.value); + } + }); + if (pollChoices.length > 0) { + window.location = '/private/post?inReplyTo=' + activityId + '&names=' + encodeURIComponent(JSON.stringify(pollChoices));; + } else { + window.location = '/private/post?inReplyTo=' + activityId; + } return; const inReplyTo = document.getElementById('inReplyTo'); @@ -265,4 +278,4 @@ const app = { } return false; } -} \ No newline at end of file +} diff --git a/routes/admin.js b/routes/admin.js index 167bb0e..0d6ee98 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -319,6 +319,7 @@ router.get('/post', async(req, res) => { const to = req.query.to; const inReplyTo = req.query.inReplyTo; + let names = []; let op; let actor; if (inReplyTo) { @@ -327,12 +328,17 @@ router.get('/post', async(req, res) => { actor = account.actor; } + if (req.query.names) { + names = JSON.parse(req.query.names); + } + res.status(200).render('partials/composer', { to, inReplyTo, actor, originalPost: op, me: req.app.get('account').actor, + names: names, layout: 'private' }); @@ -341,7 +347,15 @@ 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 post; + if (req.body.names.length > 0) { + // send multiple notes, one for each choice made in poll + for (const name of req.body.names) { + post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, name, req.body.to); + } + } else { + post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, null, req.body.to); + } if (post.directMessage === true) { // return html partial of the new post for insertion in the feed res.status(200).render('partials/dm', { @@ -545,4 +559,4 @@ router.post('/boost', async (req, res) => { }); } writeBoosts(boosts); -}); \ No newline at end of file +}); From a5c2e9ef4476f28a92697c23f26ad79cbc6dcd46 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Fri, 23 Dec 2022 16:59:30 +0000 Subject: [PATCH 20/58] Working construction and sending of polls. UI for construction is a bit clunky as it keeps regenerating DOM elements and losing focus. Nothing is done with incoming votes --- design/partials/composer.handlebars | 141 ++++++++++++++++++++++++---- lib/account.js | 23 ++++- public/app.js | 10 +- routes/admin.js | 5 +- 4 files changed, 153 insertions(+), 26 deletions(-) diff --git a/design/partials/composer.handlebars b/design/partials/composer.handlebars index 845de67..85d121c 100644 --- a/design/partials/composer.handlebars +++ b/design/partials/composer.handlebars @@ -9,36 +9,139 @@
Compose
{{/if}}
- {{#if names}} - Vote for: -
    - {{#each names}} -
  • {{this}}
  • - {{/each}} -
- - + {{#if names}} + Vote for: +
    + {{#each names}} +
  • {{this}}
  • + {{/each}} +
+ + {{else}} - - + + + + + {{/if}} - {{#each names}} + {{#each names}} {{/each}}
diff --git a/lib/account.js b/lib/account.js index 9ae778d..ca4a94e 100644 --- a/lib/account.js +++ b/lib/account.js @@ -335,7 +335,7 @@ export const sendToFollowers = async (object) => { } -export const createNote = async (body, cw, inReplyTo, name, toUser) => { +export const createNote = async (body, cw, inReplyTo, name, toUser, polldata) => { const publicAddress = "https://www.w3.org/ns/activitystreams#Public"; let d = new Date(); @@ -439,7 +439,7 @@ export const createNote = async (body, cw, inReplyTo, name, toUser) => { const url = `https://${ DOMAIN }/notes/${guid}`; const object = { "id": activityId, - "type": "Note", + "type": polldata ? "Question" : "Note", "summary": cw || null, "inReplyTo": inReplyTo, 'published': d.toISOString(), @@ -467,7 +467,24 @@ export const createNote = async (body, cw, inReplyTo, name, toUser) => { } }; - if (name) { + // construct anyOf/oneOf block in Question + if (polldata) { // posting a poll + object.endTime = new Date(new Date().getTime() + 1000 * polldata.time).toISOString(); + object.votersCount = 0; + object[polldata.type] = []; + polldata.choices.forEach((ch) => { + object[polldata.type].push({ + type: 'Note', + name: ch, + replies: { + type: 'Collection', + totalItems: 0 + } + }); + }); + } + + if (name) { // voting on a poll object.name = name; } diff --git a/public/app.js b/public/app.js index bbbe779..d622754 100644 --- a/public/app.js +++ b/public/app.js @@ -167,8 +167,13 @@ const app = { const cw = document.getElementById('cw'); const inReplyTo = document.getElementById('inReplyTo'); const to = document.getElementById('to'); - // get hidden elements for poll choices + // get hidden elements for poll choices (replying to poll) const names = Array.from(document.querySelectorAll('input[class="pollchoice"]')).map((item) => {return item.value}); + // get hidden element for poll designer (sending a new poll) + let polldata; + if (document.getElementById('polldata')) { + polldata = JSON.parse(document.getElementById('polldata').value); + } const Http = new XMLHttpRequest(); const proxyUrl ='/private/post'; @@ -179,7 +184,8 @@ const app = { cw: cw.value, inReplyTo: inReplyTo.value, to: to.value, - names: names + names: names, // list of things being voted for + polldata: polldata // poll being created by user })); Http.onreadystatechange = () => { diff --git a/routes/admin.js b/routes/admin.js index 0d6ee98..a887517 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -348,13 +348,14 @@ router.get('/post', async(req, res) => { router.post('/post', async (req, res) => { // TODO: this is probably supposed to be a post to /api/outbox let post; + if (req.body.names.length > 0) { // send multiple notes, one for each choice made in poll for (const name of req.body.names) { - post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, name, req.body.to); + post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, name, req.body.to, null); } } else { - post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, null, req.body.to); + post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, null, req.body.to, req.body.polldata); } if (post.directMessage === true) { // return html partial of the new post for insertion in the feed From 9204baf36bfccabf9f4d5ccaf7040ca678fe1694 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Fri, 23 Dec 2022 18:48:23 +0000 Subject: [PATCH 21/58] Add poll validation (can't have choices with no text) Remove debug Fix focus problems with poll generation. Only re-generate HTML when there's actually a visual change. --- design/partials/composer.handlebars | 60 ++++++++++++++--------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/design/partials/composer.handlebars b/design/partials/composer.handlebars index 85d121c..ec4fb7b 100644 --- a/design/partials/composer.handlebars +++ b/design/partials/composer.handlebars @@ -8,7 +8,7 @@ {{else}}
Compose
{{/if}} -
+ {{#if names}} Vote for:
    @@ -61,18 +61,17 @@ function addChoice() { polldata.choices.push(null); - updatePollHTML(); + updatePollHTML(false); } function deleteChoice(n) { polldata.choices.splice(n, 1); - updatePollHTML(); + updatePollHTML(false); } function changeChoiceText(n, value) { - console.log(n, value); polldata.choices[n] = value; - updatePollHTML(); + updatePollHTML(true); } function togglePollType() { @@ -81,45 +80,46 @@ } else { polldata.type = 'oneOf'; } - updatePollHTML(); + updatePollHTML(false); return false; } function updateTime(value) { polldata.time = parseInt(value); - updatePollHTML(); + updatePollHTML(true); } function validatePoll() { + if (document.getElementById('polldesigner').style.display === 'none') { + return true; + } return !polldata.choices.includes(null); // undefined elements in poll } - function updatePollHTML() { - // FIXME, removing and replacing DOM elements doesn't preserve focus. Need to manually restore or do it without complete rewrite of div + function updatePollHTML(dataOnly) { let div = document.getElementById('polldesigner'); - let html = ''; - html += ''; - for (let i=0;i'; - let nameBox = ''; - let deleteButton = '' + (i > 1 ? '❎' : '') + ''; - html += radioOrCheckbox + nameBox + deleteButton + '
    '; - } - - html += ''; - html += ''; - html += ''; + if (!dataOnly) { + let html = ''; + html += '
    '; + for (let i=0;i'; + let nameBox = ''; + let deleteButton = '' + (i > 1 ? '❎' : '') + ''; + html += radioOrCheckbox + nameBox + deleteButton + '
    '; + } + html += ''; + html += ''; + html += ''; + // update display + div.innerHTML = html; + } // update hidden field with data so that it gets posted document.getElementById('polldata').value = JSON.stringify(polldata); - console.log(polldata); - - // update display - div.innerHTML = html; } document.addEventListener("DOMContentLoaded", function(event) { @@ -139,7 +139,7 @@ div.style.display = 'none'; } else { - updatePollHTML(); + updatePollHTML(false); div.style.display = 'block'; } }; From 37a91342e5143d8f62abd199ae452937b9041181 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Fri, 23 Dec 2022 19:06:14 +0000 Subject: [PATCH 22/58] Fix case where poll is created then toggled off before posting --- design/partials/composer.handlebars | 26 ++++++++++++++++++-------- public/app.js | 5 ++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/design/partials/composer.handlebars b/design/partials/composer.handlebars index ec4fb7b..9a9c587 100644 --- a/design/partials/composer.handlebars +++ b/design/partials/composer.handlebars @@ -34,11 +34,7 @@ + + diff --git a/lib/account.js b/lib/account.js index 44572ec..c369bfa 100644 --- a/lib/account.js +++ b/lib/account.js @@ -643,3 +643,12 @@ export const ensureAccount = async (name, domain) => { export const getAccount = () => { return readJSONDictionary(accountFile, {}); } + +export const updateAccountActor = (data) => { + let account = readJSONDictionary(accountFile, {}); + Object.keys(data).forEach((k) => { + account.actor[k] = data[k]; + }); + writeJSONDictionary(accountFile, account); +} + diff --git a/public/app.js b/public/app.js index b303c4d..82d924f 100644 --- a/public/app.js +++ b/public/app.js @@ -162,9 +162,47 @@ const app = { } return false; }, - readAttachment: async () => { + settings: () => { + const summary = document.getElementById('summary'); + const preferredUsername = document.getElementById('preferredUsername'); + let attachment_header; + let attachment_avatar; + + app.readAttachment('avatarupload').then((att) => { + attachment_avatar = att; + return app.readAttachment('headerupload').then((att) => { + attachment_header = att; + + const Http = new XMLHttpRequest(); + const proxyUrl ='/private/settings'; + Http.open("POST", proxyUrl); + Http.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + Http.send(JSON.stringify({ + attachment_avatar: attachment_avatar, + attachment_header: attachment_header, + account: { + actor: { + summary: summary.value, + preferredUsername: preferredUsername.value, + } + } + })); + + Http.onreadystatechange = () => { + if (Http.readyState == 4 && Http.status == 200) { + console.log('posted!'); + window.location = '/private/settings'; + } else { + console.error('HTTP PROXY CHANGE', Http); + } + } + }); + }); + return false; + }, + readAttachment: async (id) => { // read the file into base64, return mimetype and data - const files = document.getElementById('attachment').files; + const files = document.getElementById(id).files; return new Promise((resolve, reject) => { if (files && files[0]) { let f = files[0]; // only read the first file @@ -191,7 +229,7 @@ const app = { const to = document.getElementById('to'); const description = document.getElementById('description'); - app.readAttachment().then((attachment) => { + app.readAttachment('attachment').then((attachment) => { const Http = new XMLHttpRequest(); const proxyUrl ='/private/post'; Http.open("POST", proxyUrl); diff --git a/public/css/secret.css b/public/css/secret.css index ece664a..9491912 100644 --- a/public/css/secret.css +++ b/public/css/secret.css @@ -403,9 +403,22 @@ input#cw { border: none; border-radius: 5px; float: right; +} + +#save { + padding: 0.1rem 1rem; + background: #0cc13f; + color: var(--text); + border: none; + border-radius: 5px; + float: right; +} +.settingsbutton { + float: right; } + .content .tools { flex-grow: 1; } .content .tools div { display: inline-block; } .content .tools button { font-size: 1rem; background: none; border: none; } diff --git a/routes/admin.js b/routes/admin.js index f838fed..95afc43 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -23,6 +23,7 @@ import { getInbox, writeInboxIndex, writeMedia, + updateAccountActor } from '../lib/account.js'; import { fetchUser @@ -35,6 +36,10 @@ import { } from '../lib/ActivityPub.js'; import { encode as blurhashEncode } from 'blurhash'; import { getSync as imageDataGetSync } from '@andreekeberg/imagedata' +const { + USERNAME, + DOMAIN +} = process.env; const logger = debug('ono:admin'); @@ -351,14 +356,8 @@ router.post('/post', async (req, res) => { if (req.body.attachment) { // convert attachment.data to raw buffer - attachment = { - type: req.body.attachment.type, - data: Buffer.from(req.body.attachment.data, 'base64'), - description: req.body.description || '', - }; - - // used as filename/id - attachment.hash = createHash('md5').update(attachment.data).digest("hex"); + attachment = calculateAttachmentHashAndData(req.body.attachment); + attachment.description = req.body.description || ''; if (attachment.type.split('/')[0] == 'image') { // calculate dimensions and blurhash @@ -577,3 +576,55 @@ router.post('/boost', async (req, res) => { } writeBoosts(boosts); }); + + + +router.get('/settings', async (req, res) => { + res.render('settings', { + layout: 'private', + actor: ActivityPub.actor + }); +}); + +function calculateAttachmentHashAndData(att) { + let attachment = { + type: att.type, + data: Buffer.from(att.data, 'base64'), + }; + attachment.hash = createHash('md5').update(att.data).digest("hex"); + return attachment; +} + +router.post('/settings', async (req, res) => { + if (req.body.attachment_avatar || req.body.attachment_header) { + if (!req.body.account) { // ensure account gets updated as we're changing the urls + req.body.account = {}; + } + if (!req.body.account.actor) { + req.body.account.actor = {}; + } + } + if (req.body.attachment_avatar) { + let att = calculateAttachmentHashAndData(req.body.attachment_avatar); + writeMedia(att); + req.body.account.actor.icon = { + type: 'Image', + mediaType: att.type, + url: `https://${ DOMAIN }/media/${att.hash}.${att.type.split('/')[1]}` + }; + } + if (req.body.attachment_header) { + let att = calculateAttachmentHashAndData(req.body.attachment_header); + writeMedia(att); + req.body.account.actor.image = { + type: 'Image', + mediaType: att.type, + url: `https://${ DOMAIN }/media/${att.hash}.${att.type.split('/')[1]}` + }; + } + if (req.body.account && req.body.account.actor) { + await updateAccountActor(req.body.account.actor); + } + res.status(200).send(); +}); + From 8afa147b33a3ab7209ef5fc6002f03aaaefb2035 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Sat, 24 Dec 2022 22:04:10 +0000 Subject: [PATCH 29/58] Add edit button and composer changes to allow editing of sent posts. Fill out compose with previous message (strips HTML from content), retains cw and inReplyTo Sends an Update containing Note with same id as original post --- design/dashboard.handlebars | 4 +-- design/partials/composer.handlebars | 33 ++++++++++++++++++---- design/partials/note.handlebars | 5 ++++ index.js | 5 ++-- lib/ActivityPub.js | 29 ++++++++++++++++++- lib/account.js | 44 ++++++++++++++++++++++++----- public/app.js | 8 +++++- routes/admin.js | 33 +++++++++++++++++----- routes/inbox.js | 2 +- 9 files changed, 136 insertions(+), 27 deletions(-) diff --git a/design/dashboard.handlebars b/design/dashboard.handlebars index 7872a15..97c6412 100644 --- a/design/dashboard.handlebars +++ b/design/dashboard.handlebars @@ -13,7 +13,7 @@ 🚀 boosted by {{booster.name}} {{/if}} - {{> note note=note}} + {{> note note=note me=../me}} {{/with}} {{/each}} @@ -37,4 +37,4 @@ {{/if}}{{/each}} app.pollForPosts(); - \ No newline at end of file + diff --git a/design/partials/composer.handlebars b/design/partials/composer.handlebars index 5028afa..e81e705 100644 --- a/design/partials/composer.handlebars +++ b/design/partials/composer.handlebars @@ -6,13 +6,34 @@ {{{originalPost.content}}} {{else}} -
    Compose
    + {{#if prev}} +
    Edit
    + {{else}} + {{/if}} {{/if}} +
    - - - - + + + + + {{#if prev}} + + {{/if}}
    @@ -27,4 +48,4 @@ input.focus(); input.selectionStart = input.selectionEnd = input.value.length; - \ No newline at end of file + diff --git a/design/partials/note.handlebars b/design/partials/note.handlebars index 27139b2..694444d 100644 --- a/design/partials/note.handlebars +++ b/design/partials/note.handlebars @@ -35,6 +35,11 @@ ⭐️ + {{#if (isEq note.attributedTo me.id)}} + + {{/if}} diff --git a/index.js b/index.js index c24f051..9e4679f 100644 --- a/index.js +++ b/index.js @@ -48,7 +48,7 @@ const hbs = create({ if (str && str.includes('image')) return options.fn(this); }, isEq: (a, b, options) => { - if (a === b) return options.fn(this); + return (a === b); }, or: (a, b, options) => { return a || b @@ -60,6 +60,7 @@ const hbs = create({ return ActivityPub.getUsername(user) }, stripProtocol: (str) => str.replace(/^https\:\/\//, ''), + stripHTML: (str) => str.replace(/(<([^>]+)>)/gi, "") } }); @@ -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/ActivityPub.js b/lib/ActivityPub.js index f55d084..df3c7e7 100644 --- a/lib/ActivityPub.js +++ b/lib/ActivityPub.js @@ -273,6 +273,33 @@ export class ActivityPubClient { return message; } + /** + * Send an outbound update activity to a follower or recipient of a message + * @param {*} recipient + * @param {*} object + * @returns + */ + async sendUpdate(recipient, object) { + let message = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `${ object.id }/activity`, + 'published': object.published, + 'type': 'Update', + 'actor': this.actor.id, + 'object': object, + "to": object.to, + "cc": object.cc, + }; + try { + await ActivityPub.send(recipient, message); + } catch(err) { + // TODO: retry + console.error('Failed to deliver outbound message', err); + } + return message; + } + + /** * Send an outbound create activity to a follower or recipient of a message @@ -498,4 +525,4 @@ export class ActivityPubClient { } -export const ActivityPub = new ActivityPubClient(); \ No newline at end of file +export const ActivityPub = new ActivityPubClient(); diff --git a/lib/account.js b/lib/account.js index c3fff64..a5ad074 100644 --- a/lib/account.js +++ b/lib/account.js @@ -320,8 +320,7 @@ export const getNote = async (id) => { return undefined; } -export const sendToFollowers = async (object) => { - +export const sendCreateToFollowers = async (object) => { const followers = await getFollowers(); followers.forEach(async (follower) => { const account = await fetchUser(follower); @@ -332,14 +331,33 @@ export const sendToFollowers = async (object) => { console.error('Failed to deliver outbound post', err); } }); - } +export const sendUpdateToFollowers = async (object) => { + const followers = await getFollowers(); + followers.forEach(async (follower) => { + const account = await fetchUser(follower); + try { + ActivityPub.sendUpdate(account.actor, object); + } catch(err) { + // TODO: should retry + console.error('Failed to deliver outbound post', err); + } + }); +} + -export const createNote = async (body, cw, inReplyTo, toUser) => { +export const createNote = async (body, cw, inReplyTo, toUser, editOf) => { const publicAddress = "https://www.w3.org/ns/activitystreams#Public"; let d = new Date(); - const guid = crypto.randomBytes(16).toString('hex'); + let guid; + if (editOf) { + // use same guid as post we're updating + guid = editOf.replace(`https://${ DOMAIN }/m/`, ''); + } else { + // generate new guid + guid = crypto.randomBytes(16).toString('hex'); + } let directMessage; // default to public @@ -467,6 +485,10 @@ export const createNote = async (body, cw, inReplyTo, toUser) => { } }; + if (editOf) { + object.updated = d.toISOString(); + } + if (directMessage) { acceptDM(object, to[0]); } else { @@ -488,11 +510,19 @@ export const createNote = async (body, cw, inReplyTo, toUser) => { if (recipient === publicAddress) { // do nothing } else if (recipient === ActivityPub.actor.followers) { - sendToFollowers(object); + if (editOf) { + sendUpdateToFollowers(object); + } else { + sendCreateToFollowers(object); + } } else { // otherwise, send it directly to the person const account = await fetchUser(recipient); - ActivityPub.sendCreate(account.actor, object); + if (editOf) { + ActivityPub.sendUpdate(account.actor, object); + } else { + ActivityPub.sendCreate(account.actor, object); + } } }) diff --git a/public/app.js b/public/app.js index 57635ad..ebef593 100644 --- a/public/app.js +++ b/public/app.js @@ -162,11 +162,16 @@ const app = { } return false; }, + editPost: (postId) => { + console.log("EDIT POST", postId); + window.location = '/private/post?edit=' + encodeURIComponent(postId); + }, post: () => { const post = document.getElementById('post'); const cw = document.getElementById('cw'); const inReplyTo = document.getElementById('inReplyTo'); const to = document.getElementById('to'); + const editOf = document.getElementById('editOf'); const Http = new XMLHttpRequest(); const proxyUrl ='/private/post'; @@ -177,6 +182,7 @@ const app = { cw: cw.value, inReplyTo: inReplyTo.value, to: to.value, + editOf: editOf ? editOf.value : null })); Http.onreadystatechange = () => { @@ -265,4 +271,4 @@ const app = { } return false; } -} \ No newline at end of file +} diff --git a/routes/admin.js b/routes/admin.js index 167bb0e..3e5c1bc 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -120,7 +120,6 @@ router.get('/following', async (req, res) => { followers = followers.filter((f) => f !== undefined); - if (req.query.json) { res.json(notes); } else { @@ -150,6 +149,8 @@ router.get('/', async (req, res) => { next } = await getActivityStream(10, offset); + + const notes = await Promise.all(activitystream.map(async (n) => { // handle boosted posts if (n.note.type === 'Announce') { @@ -179,10 +180,20 @@ router.get('/', async (req, res) => { return n; })); + // de-dupe notes by id, else will get two on edits + const uniqueIds = new Set(); + const uniqueNotes = notes.filter(element => { + const isDuplicate = uniqueIds.has(element.note.id); + uniqueIds.add(element.note.id); + if (!isDuplicate) { + return true; + } + return false; + }); + if (req.query.json) { - res.json(notes); + res.json(uniqueNotes); } else { - // set auth cookie res.cookie('token', ActivityPub.account.apikey, {maxAge: (7*24*60*60*1000)}); @@ -190,7 +201,7 @@ router.get('/', async (req, res) => { layout: 'private', me: ActivityPub.actor, next: next, - activitystream: notes, + activitystream: uniqueNotes, followers: followers, following: following, followersCount: followers.length, @@ -321,17 +332,25 @@ router.get('/post', async(req, res) => { const inReplyTo = req.query.inReplyTo; let op; let actor; + let prev; if (inReplyTo) { op = await getActivity(inReplyTo); const account = await fetchUser(op.attributedTo); actor = account.actor; } + if (req.query.edit) { + console.log("COMPOSING EDIT", req.query.edit); + prev = await getNote(req.query.edit); + //console.log("ORIGINAL", original); + } + res.status(200).render('partials/composer', { to, inReplyTo, actor, - originalPost: op, + originalPost: op, // original post being replied to + prev: prev, // previous version we posted, now editing me: req.app.get('account').actor, layout: 'private' }); @@ -341,7 +360,7 @@ 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); + const post = await createNote(req.body.post, req.body.cw, req.body.inReplyTo, req.body.to, req.body.editOf); if (post.directMessage === true) { // return html partial of the new post for insertion in the feed res.status(200).render('partials/dm', { @@ -545,4 +564,4 @@ router.post('/boost', async (req, res) => { }); } writeBoosts(boosts); -}); \ No newline at end of file +}); diff --git a/routes/inbox.js b/routes/inbox.js index 8cea8a4..56dabc4 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -180,4 +180,4 @@ router.post('/', async (req, res) => { logger('Unknown request format:', incomingRequest); } return res.status(200).send(); -}); \ No newline at end of file +}); From 611c96c6dbc97bb458bed18434a1870efa390f7c Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 14:18:10 +0000 Subject: [PATCH 30/58] Fix case where there are no hashtags --- lib/account.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/account.js b/lib/account.js index 203ce7d..3cfb9d0 100644 --- a/lib/account.js +++ b/lib/account.js @@ -433,13 +433,16 @@ export const createNote = async (body, cw, inReplyTo, toUser) => { } // if it contains hashtags, add them - body.match(/#[a-z]+/gi).forEach((tag) => { - tags.push({ - "type": "Hashtag", - "href": `https://${ DOMAIN }/tags/${tag}`, - "name": tag + let hashtags = body.match(/#[a-z]+/gi) + if (hashtags) { + hashtags.forEach((tag) => { + tags.push({ + "type": "Hashtag", + "href": `https://${ DOMAIN }/tags/${tag}`, + "name": tag + }); }); - }); + } const mdContent = md.render(processedContent); From 2a9b517494fe96e9750cd616a730d89380078bbe Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 15:11:37 +0000 Subject: [PATCH 31/58] Fix for vote notifications not showing properly --- design/notifications.handlebars | 5 ++++- index.js | 6 +++++- routes/admin.js | 2 +- routes/inbox.js | 2 +- routes/public.js | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/design/notifications.handlebars b/design/notifications.handlebars index 477bc51..8b01b2a 100644 --- a/design/notifications.handlebars +++ b/design/notifications.handlebars @@ -26,6 +26,9 @@ {{{...note.content}}} {{/isEq}} + {{#isEq notification.type "Vote"}} +
    🗳 {{../actor.name}} voted on your poll {{timesince ../time}}
    + {{/isEq}} {{#isEq notification.type "Follow"}}
    🤷🏽‍♂️ {{../actor.name}} followed you {{timesince ../time}}
    {{> byline actor=../actor}} @@ -42,4 +45,4 @@ {{/if}}{{/each}} app.pollForPosts(); - \ No newline at end of file + diff --git a/index.js b/index.js index 782fd4a..fab79a9 100644 --- a/index.js +++ b/index.js @@ -48,7 +48,11 @@ const hbs = create({ if (str && str.includes('image')) return options.fn(this); }, isEq: (a, b, options) => { - return a === b + if (typeof options.fn === 'function') { + if (a === b) return options.fn(this); + } else { + return a === b; + } }, or: (a, b, options) => { return a || b diff --git a/routes/admin.js b/routes/admin.js index a887517..6c8ac2c 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -209,7 +209,7 @@ router.get('/notifications', async (req, res) => { // TODO: check if user is in following list actor.isFollowing = isFollowing(actor.id); - if (notification.notification.type === 'Like' || notification.notification.type === 'Announce') { + if (notification.notification.type === 'Like' || notification.notification.type === 'Announce' || notification.notification.type === 'Vote') { note = await getNote(notification.notification.object); } if (notification.notification.type === 'Reply') { diff --git a/routes/inbox.js b/routes/inbox.js index 304ee68..1db8770 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -144,7 +144,7 @@ router.post('/', async (req, res) => { addNotification({ type: 'Vote', actor: incomingRequest.object.attributedTo, - object: incomingRequest.object.id + object: incomingRequest.object.inReplyTo }); } else { diff --git a/routes/public.js b/routes/public.js index 4f02be7..ef2187b 100644 --- a/routes/public.js +++ b/routes/public.js @@ -151,7 +151,7 @@ router.get('/notes/:guid', async (req, res) => { if (note === undefined) { return res.status(404).send(`No record found for ${guid}.`); } else { - if (req.accepts('application/activity+json')) { + if (req.accepts('application/activity+json') && !req.accepts('*/*')) { // non-browser client res.setHeader('Content-Type', 'application/activity+json'); res.status(200).send(note); } else { From 38577a419358ae524d1ca6455d55002855168fb0 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 16:11:13 +0000 Subject: [PATCH 32/58] Remove change of preferredUsername, this breaks search from Mastodon servers --- design/settings.handlebars | 6 ------ public/app.js | 4 +--- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/design/settings.handlebars b/design/settings.handlebars index 3bfab01..6f8690a 100644 --- a/design/settings.handlebars +++ b/design/settings.handlebars @@ -17,12 +17,6 @@
    {{actor.name}} {{getUsername actor.id}} -

    - - - - -

    diff --git a/public/app.js b/public/app.js index 82d924f..0313d89 100644 --- a/public/app.js +++ b/public/app.js @@ -164,7 +164,6 @@ const app = { }, settings: () => { const summary = document.getElementById('summary'); - const preferredUsername = document.getElementById('preferredUsername'); let attachment_header; let attachment_avatar; @@ -182,8 +181,7 @@ const app = { attachment_header: attachment_header, account: { actor: { - summary: summary.value, - preferredUsername: preferredUsername.value, + summary: summary.value } } })); From 4df90f291b7d9bfb5c7cec7e33aff44583cc6737 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 18:43:35 +0000 Subject: [PATCH 33/58] Raise notification for DM. Ignore self-DMs, remove tools (boost, like, edit) on notifications --- design/notifications.handlebars | 4 ++++ design/partials/note.handlebars | 2 ++ lib/account.js | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/design/notifications.handlebars b/design/notifications.handlebars index 477bc51..567a1b3 100644 --- a/design/notifications.handlebars +++ b/design/notifications.handlebars @@ -20,6 +20,10 @@

    💬 {{../actor.name}} mentioned you {{timesince ../time}}
    {{> note actor=../actor note=../note}} {{/isEq}} + {{#isEq notification.type "DM"}} +
    💬 {{../actor.name}} direct messaged you {{timesince ../time}}
    + {{> note actor=../actor note=../note}} + {{/isEq}} {{#isEq notification.type "Like"}}
    ⭐️ {{../actor.name}} liked your post {{timesince ../time}}
    diff --git a/design/partials/note.handlebars b/design/partials/note.handlebars index 27139b2..7b17205 100644 --- a/design/partials/note.handlebars +++ b/design/partials/note.handlebars @@ -25,6 +25,7 @@
    {{/each}}
    diff --git a/lib/account.js b/lib/account.js index e3551ad..b9d667b 100644 --- a/lib/account.js +++ b/lib/account.js @@ -119,6 +119,13 @@ export const acceptDM = (dm, inboxUser) => { writeInboxIndex(inboxIndex); + if (dm.attributedTo !== ActivityPub.actor.id) { + addNotification({ + type: 'DM', + actor: dm.attributedTo, + object: dm.inReplyTo + }); + } } export const isMyPost = (activity) => { From 6a2c6dc0498194bb6446d58c927c89bae3c48144 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 18:54:21 +0000 Subject: [PATCH 34/58] Render (disabled) poll in public note view --- design/public/note.handlebars | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/design/public/note.handlebars b/design/public/note.handlebars index 35c5360..ac23fa6 100644 --- a/design/public/note.handlebars +++ b/design/public/note.handlebars @@ -37,6 +37,27 @@ {{/isVideo}} {{/each}} + {{#if (isEq note.type 'Question')}} +
    +
    + {{#each note.oneOf}} + + ({{replies.totalItems}})
    + {{/each}} + {{#each note.anyOf}} + + ({{replies.totalItems}})
    + {{/each}} +
    +
    + {{#if (expired note.endTime)}} + Ended {{timesince note.endTime}} + {{else}} + Ends {{timesince note.endTime}} + {{/if}} +
    + Votes cast: {{note.votersCount}}
    + {{/if}}