Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attachment support #56

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion design/partials/composer.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<input type="text" id="cw" placeholder="content warning" />
<input id="inReplyTo" placeholder="in reply to" value="{{inReplyTo}}" hidden />
<input id="to" placeholder="to" value="{{to}}" hidden />
<input id="attachment" type="file" />
<input id="description" type="text" placeholder="alt text" />
<button id="submit" type="submit">Post</button>
</form>
</div>
Expand All @@ -27,4 +29,4 @@
input.focus();
input.selectionStart = input.selectionEnd = input.value.length;

</script>
</script>
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '32mb' // allow large bodies as attachments are base64 in JSON
})); // support json encoded bodies
app.use(cookieParser())

Expand Down Expand Up @@ -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'));
});
});
});
41 changes: 37 additions & 4 deletions lib/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import {
createFileName,
getFileName,
boostsFile,
pathToDMs
pathToDMs,
benbrown marked this conversation as resolved.
Show resolved Hide resolved
writeMediaFile,
readMediaFile
} from './storage.js';
import {
getActivity,
Expand Down Expand Up @@ -118,6 +120,16 @@ export const acceptDM = (dm, inboxUser) => {

}

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) => {
logger('read media', filename);
return readMediaFile(filename);
}

export const isMyPost = (activity) => {
return (activity.id.startsWith(`https://${DOMAIN}/m/`));
}
Expand Down Expand Up @@ -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, attachmentInfo) => {
const publicAddress = "https://www.w3.org/ns/activitystreams#Public";

let d = new Date();
Expand All @@ -350,6 +362,27 @@ export const createNote = async (body, cw, inReplyTo, toUser) => {
ActivityPub.actor.followers
];

let attachment;
if (attachmentInfo) {
attachment = [
{
type: 'Document',
mediaType: attachmentInfo.type.split('/')[0],
url: `https://${ DOMAIN }/media/${attachmentInfo.hash}.${attachmentInfo.type.split('/')[1]}`,
name: attachmentInfo.description,
focalPoint: attachmentInfo.focalPoint,
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
const tags = [];
Expand Down Expand Up @@ -449,11 +482,11 @@ export const createNote = async (body, cw, inReplyTo, toUser) => {
"to": to,
"cc": cc,
directMessage,
"sensitive": cw !== null ? true : false,
"sensitive": cw ? true : false,
"atomUri": activityId,
"inReplyToAtomUri": null,
"content": content,
"attachment": [],
"attachment": attachment || [],
"tag": tags,
"replies": {
"id": `${activityId}/replies`,
Expand Down
30 changes: 28 additions & 2 deletions lib/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -199,7 +200,12 @@ const ensureDataFolder = () => {
recursive: true
});
}

if (!fs.existsSync(path.resolve(pathToMedia))) {
logger('mkdir', pathToMedia);
fs.mkdirSync(path.resolve(pathToMedia), {
recursive: true
});
}
}


Expand All @@ -225,6 +231,26 @@ export const readJSONDictionary = (path, defaultVal = []) => {
}
}

// data is JSON stringified string containing {type: mimetype, data: base64}
export const writeMediaFile = (filename, attachment) => {
logger('write media', filename);
// write just the data part to file called <hash>
fs.writeFileSync(path.join(pathToMedia, filename + '.' + attachment.type.split('/')[1]), attachment.data);
delete attachment.data;
// write the remaining metadata to <hash>.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);
// 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;
}

export const writeJSONDictionary = (path, data) => {
const now = new Date().getTime();
logger('write cache', path);
Expand All @@ -240,4 +266,4 @@ logger('BUILDING INDEX');
ensureDataFolder();
buildIndex().then(() => {
logger('INDEX BUILT!');
});
});
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
83 changes: 55 additions & 28 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,45 +162,72 @@ const app = {
}
return false;
},
readAttachment: async () => {
// read the file into base64, return mimetype and data
const files = document.getElementById('attachment').files;
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) {
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: () => {
const post = document.getElementById('post');
const cw = document.getElementById('cw');
const inReplyTo = document.getElementById('inReplyTo');
const to = document.getElementById('to');
const description = document.getElementById('description');

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,
}));
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,
description: description.value
}));

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) => {
Expand Down Expand Up @@ -265,4 +292,4 @@ const app = {
}
return false;
}
}
}
37 changes: 34 additions & 3 deletions routes/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,7 +21,8 @@ import {
isFollowing,
getInboxIndex,
getInbox,
writeInboxIndex
writeInboxIndex,
writeMedia,
} from '../lib/account.js';
import {
fetchUser
Expand All @@ -31,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) => {
Expand Down Expand Up @@ -341,7 +346,33 @@ 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) {
// 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");

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);
}

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', {
Expand Down Expand Up @@ -545,4 +576,4 @@ router.post('/boost', async (req, res) => {
});
}
writeBoosts(boosts);
});
});
17 changes: 15 additions & 2 deletions routes/public.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
getNote,
isMyPost,
getAccount,
getOutboxPosts
getOutboxPosts,
readMedia
} from '../lib/account.js';
import {
getActivity,
Expand Down Expand Up @@ -175,4 +176,16 @@ router.get('/notes/:guid', async (req, res) => {
});
}
}
});
});

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();
}
});