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
3 changes: 2 additions & 1 deletion design/partials/composer.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<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" />
<button id="submit" type="submit">Post</button>
</form>
</div>
Expand All @@ -27,4 +28,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: '4mb' // allow large bodies as attachments are base64 in JSON
ringtailsoftware marked this conversation as resolved.
Show resolved Hide resolved
})); // 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'));
});
});
});
31 changes: 28 additions & 3 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 = (filename, attachment) => {
ringtailsoftware marked this conversation as resolved.
Show resolved Hide resolved
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/`));
}
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, relativeAttachment) => {
const publicAddress = "https://www.w3.org/ns/activitystreams#Public";

let d = new Date();
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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`,
Expand Down
22 changes: 20 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,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);
Expand All @@ -240,4 +258,4 @@ logger('BUILDING INDEX');
ensureDataFolder();
buildIndex().then(() => {
logger('INDEX BUILT!');
});
});
26 changes: 25 additions & 1 deletion public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,34 @@ const app = {
}
return false;
},
post: () => {
readAttachment: async () => {
// read the file into base64, return mimtype and data
ringtailsoftware marked this conversation as resolved.
Show resolved Hide resolved
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: 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';
Expand All @@ -177,6 +200,7 @@ const app = {
cw: cw.value,
inReplyTo: inReplyTo.value,
to: to.value,
attachment: attachment
}));

Http.onreadystatechange = () => {
Expand Down
21 changes: 19 additions & 2 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 Down Expand Up @@ -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', {
Expand Down
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();
}
});