203 lines
No EOL
8.5 KiB
JavaScript
203 lines
No EOL
8.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// TODO: .env compatibility.
|
|
const { confirm, editor, input } = require("@inquirer/prompts");
|
|
|
|
const commonmark = require("commonmark");
|
|
const chrono = require('chrono-node');
|
|
const path = require('path');
|
|
const fs = require('node:fs/promises');
|
|
|
|
(async () => {
|
|
// Queries with enquirer
|
|
// User to post as, autofill .env
|
|
const user = await input({ message: "User to post as ", default: "rallias" });
|
|
// Domain to post as, autofill .env
|
|
const domain = await input({ message: "Domain to post as ", default: "dev.brainz.social" });
|
|
// Post to reply to, autofill empty
|
|
// Subject, autofill empty
|
|
const subject = await input({ message: "Subject ", default: "" });
|
|
// Post content, autofill empty
|
|
const postContent = (await editor({ message: "Post content " })).replace(/\n$/, "");
|
|
// Poll yes/no
|
|
let pollOptions = [], endTime = Date.now()+86400000, multipleChoice = false;
|
|
const isPoll = await confirm({ message: "Is this a poll post? ", default: false });
|
|
if (isPoll) {
|
|
let pollDone = false;
|
|
while (!pollDone) {
|
|
const pollOption = await input({ message: "Enter poll option (empty to finish) ", default: "" });
|
|
if (pollOption === "") {
|
|
pollDone = true;
|
|
} else {
|
|
pollOptions.push(pollOption);
|
|
}
|
|
}
|
|
multipleChoice = await confirm({ message: "Allow multiple selection? ", default: false });
|
|
let hasEndTime = false;
|
|
while (!hasEndTime) {
|
|
endTime = chrono.parseDate(await(input({message: "How long should poll run (time in future/length of time)? "})), new Date(Date.now()), {forwardDate: true});
|
|
if (endTime > Date.now()) {
|
|
hasEndTime = true;
|
|
} else {
|
|
console.log("Unable to parse date. Try something like \"In one day\" or \"25th of December at Noon\". See \"chrono-node\" NPM module for further details.");
|
|
}
|
|
}
|
|
}
|
|
|
|
let parsedPostContent = "";
|
|
let tags = [];
|
|
|
|
let chars = Array.from(postContent);
|
|
for ( let i = 0; i < chars.length; i++ ) {
|
|
if ( chars[i] !== "@" )
|
|
parsedPostContent = `${parsedPostContent}${chars[i]}`;
|
|
if ( chars[i] === "@" ) {
|
|
let potentialMentionUsername = "";
|
|
let fulfilledPotentialMention = false;
|
|
let fulfilledReallyLikelyPotentialMention = false;
|
|
let potentialMentionDomain = "";
|
|
let lameDuckSpacing = "";
|
|
i++;
|
|
while ( i < chars.length && fulfilledPotentialMention === false ) {
|
|
if ( chars[i] !== "@" ) {
|
|
potentialMentionUsername = `${potentialMentionUsername}${chars[i]}`;
|
|
}
|
|
if ( chars[i] === "@" ) {
|
|
fulfilledPotentialMention = true;
|
|
i++
|
|
while ( i < chars.length && fulfilledReallyLikelyPotentialMention === false ) {
|
|
potentialMentionDomain = `${potentialMentionDomain}${chars[i]}`;
|
|
if ( chars[i].match(/\s/) ) {
|
|
fulfilledReallyLikelyPotentialMention = true;
|
|
lameDuckSpacing = chars[i];
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
if ( chars[i].match(/\s/) ) {
|
|
fulfilledPotentialMention = true;
|
|
}
|
|
i++;
|
|
}
|
|
if ( fulfilledPotentialMention ) {
|
|
if ( fulfilledReallyLikelyPotentialMention || i === chars.length ) {
|
|
const allowTag = await confirm({message: `Tag @${potentialMentionUsername}@${potentialMentionDomain}`, default: true});
|
|
if ( allowTag ) {
|
|
// TODO: Parse this at post creation time, rather than rely on dynamic content.
|
|
// In order to accomplish this, we'll need to be able to create a stub user and signed HTTP requests.
|
|
parsedPostContent = `${parsedPostContent}[@${potentialMentionUsername}](https://${domain}/resolveUser?user=${potentialMentionUsername}&domain=${potentialMentionDomain})`;
|
|
// TODO: Finger the user at post creation time, rather than rely on dynamic content.
|
|
tags.push(`https://${domain}/resolveWebfinger?user=${potentialMentionUsername}&domain=${potentialMentionDomain}`);
|
|
} else {
|
|
parsedPostContent = `${parsedPostContent}@${potentialMentionUsername}@${potentialMentionDomain}`;
|
|
}
|
|
if ( fulfilledReallyLikelyPotentialMention ) {
|
|
parsedPostContent = `${parsedPostContent}${lameDuckSpacing}`;
|
|
}
|
|
} else {
|
|
parsedPostContent = `${parsedPostContent}@${potentialMentionUsername}`;
|
|
}
|
|
} else {
|
|
parsedPostContent = `${parsedPostContent}@${potentialMentionUsername}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Support attachments.
|
|
let postContentHtml = (new commonmark.HtmlRenderer()).render(new commonmark.Parser().parse(parsedPostContent)).replace(/\n$/, "");
|
|
let timestamp = Date.now();
|
|
let postId = `${timestamp}`;
|
|
|
|
let postObj = {
|
|
"@context": [
|
|
"https://www.w3.org/ns/activitystreams",
|
|
{
|
|
"ostatus": "http://ostatus.org#",
|
|
"sensitive": "as:sensitive",
|
|
"toot": "http://joinmastodon.org/ns#",
|
|
"votersCount": "toot:votersCount",
|
|
"litepub": "http://litepub.social/ns#",
|
|
"directMessage": "litepub:directMessage"
|
|
}
|
|
],
|
|
"id": `https://${domain}/users/${user}/statuses/${postId}`,
|
|
"type": "Note",
|
|
"inReplyTo": null,
|
|
"published": (new Date(Date.now())).toISOString(),
|
|
"url": `https://${domain}/users/${user}/statuses/${postId}`,
|
|
"attributedTo": `https://${domain}/users/${user}`,
|
|
"to": [
|
|
"https://www.w3.org/ns/activitystreams#Public"
|
|
],
|
|
"cc": [
|
|
`https://${domain}/users/${user}/followers`,
|
|
...tags
|
|
],
|
|
"sensitive": false,
|
|
"content": postContentHtml,
|
|
"contentMap": {
|
|
// TODO: Multiple Language Support
|
|
"en": postContentHtml
|
|
},
|
|
"attachment": [],
|
|
"tag": [],
|
|
"replies": {
|
|
"id": `https://${domain}/users/${user}/statuses/${postId}/replies`,
|
|
"type": "Collection",
|
|
"first": {
|
|
"type": "CollectionPage",
|
|
"next": `https://${domain}/users/${user}/statuses/${postId}/replies?only_other_accounts=true\u0026page=true`,
|
|
"partOf": `https://${domain}/users/${user}/statuses/${postId}/replies`,
|
|
"items": []
|
|
}
|
|
}
|
|
};
|
|
|
|
if (subject) {
|
|
postObj.summary = subject;
|
|
postObj.sensitive = true;
|
|
}
|
|
|
|
if (isPoll) {
|
|
postObj.type = "Question";
|
|
const options = pollOptions.map((value) => {
|
|
return {
|
|
type: "Note",
|
|
name: value,
|
|
replies: {
|
|
type: "Collection",
|
|
totalItems: 0
|
|
}
|
|
}
|
|
});
|
|
if ( multipleChoice ) {
|
|
postObj.anyOf = options;
|
|
} else {
|
|
postObj.oneOf = options;
|
|
}
|
|
postObj.endTime = new Date(endTime).toISOString();
|
|
postObj.votersCount = 0;
|
|
};
|
|
const writeDir = path.join(__dirname, "json", "users", user, "statuses");
|
|
const writePath = path.join(writeDir, `${postId}.json`);
|
|
await fs.mkdir(writeDir, {recursive: true});
|
|
await fs.writeFile(writePath, JSON.stringify(postObj));
|
|
console.log(`Post created successfully! ${writePath}`);
|
|
const postActivity = {
|
|
object: postObj,
|
|
"@context": postObj["@context"],
|
|
to: postObj.to,
|
|
cc: postObj.cc,
|
|
type: "Create",
|
|
actor: postObj.attributedTo,
|
|
id: `${postObj.id}/activity`,
|
|
published: postObj.published
|
|
};
|
|
delete postActivity.object["@context"];
|
|
const activityWriteDir = path.join(writeDir, postId);
|
|
const activityWritePath = path.join(activityWriteDir, "activity.json");
|
|
await fs.mkdir(activityWriteDir, {recursive: true});
|
|
await fs.writeFile(activityWritePath, JSON.stringify(postActivity));
|
|
console.log(`Activity created successfully! ${activityWritePath}`);
|
|
})(); |