fix common-good setup

somehow it's cli arguments changed
This commit is contained in:
Henry 2020-10-19 09:27:10 +02:00
parent 3bdca12a21
commit e7dd215f4d
6 changed files with 400 additions and 290 deletions

View File

@ -16,7 +16,7 @@
"dev": "nodemon --inspect src/index.js --debug --no-open",
"fix": "common-good fix",
"start": "node src/index.js",
"test": "tap --timeout 240 && common-good check --dependency-check-suffix '-i changelog-version -i mkdirp -i nodemon -i stylelint-config-recommended'",
"test": "tap --timeout 240 && common-good test --dependency-check-suffix '-i changelog-version -i mkdirp -i nodemon -i stylelint-config-recommended'",
"preversion": "npm test",
"version": "mv docs/CHANGELOG.md ./ && changelog-version && mv CHANGELOG.md docs/ && git add docs/CHANGELOG.md"
},

View File

@ -113,7 +113,8 @@ a {
color: var(--fg-light);
}
button, .file-button {
button,
.file-button {
cursor: pointer;
background: var(--fg);
color: var(--bg);
@ -123,19 +124,19 @@ button, .file-button {
}
.file-button {
width: 58px;
font-size: 8pt;
float: right;
margin: 0;
background: transparent;
color: var(--fg);
width: 58px;
font-size: 8pt;
float: right;
margin: 0;
background: transparent;
color: var(--fg);
}
#blob {
visibility: hidden;
height: 0;
padding: 0;
margin: 0;
visibility: hidden;
height: 0;
padding: 0;
margin: 0;
}
section.post-preview {
@ -174,10 +175,9 @@ section > footer > div > form > button {
section > footer > div > a:hover,
section > footer > div > form > button:hover {
text-decoration: underline;
text-decoration: underline;
}
section > footer > div > form > button {
display: inline-block;
border: 0;
@ -360,7 +360,7 @@ nav > ul > li > a:hover {
}
nav > ul > li > a.current {
font-weight: bold;
font-weight: bold;
}
section {
@ -375,9 +375,9 @@ section {
.indent section,
.thread-container section {
margin: unset;
border-radius: unset;
border-bottom: var(--fg-alt) solid 1px;
margin: unset;
border-radius: unset;
border-bottom: var(--fg-alt) solid 1px;
}
.indent details[open] {
@ -386,7 +386,7 @@ section {
.indent section:last-of-type,
.thread-container section:last-of-type {
border-bottom: unset;
border-bottom: unset;
}
.mentions-container {
@ -453,7 +453,7 @@ section > header {
}
.author-action > a {
text-decoration: underline;
text-decoration: underline;
}
section > header > div {
@ -522,7 +522,7 @@ section > footer > div > * {
}
section > footer > div > form > button:first-of-type {
font-size: 100%;
font-size: 100%;
}
section > footer > div > form > button.liked {

View File

@ -146,60 +146,58 @@ const { about, blob, friend, meta, post, vote } = require("./models")({
// enhance the users' input text by expanding @name to [@name](@feedPub.key)
// and slurps up blob uploads and appends a markdown link for it to the text (see handleBlobUpload)
const preparePreview = async function(ctx) {
const preparePreview = async function (ctx) {
let text = String(ctx.request.body.text);
// find all the @mentions that are not inside a link already
// stores name:[matches...]
// TODO: sort by relationship
// TODO: filter duplicates
const mentions = {}
const mentions = {};
// This matches for @string followed by a space or other punctuations like ! , or .
// The idea here is to match a plain @name but not [@name](...)
// also: re.exec is stateful => regex is consumed and thus needs to be re-instantiated for each call
const rex = /(?!\[)@([a-zA-Z0-9-]+)([\s\.,!?)~]{1}|$)/g
const rex = /(?!\[)@([a-zA-Z0-9-]+)([\s\.,!?)~]{1}|$)/g;
// ^ sentence ^
// delimiters
// find @mentions using rex and use about.named() to get the info for them
let m
let m;
while ((m = rex.exec(text)) !== null) {
const [match, name] = m;
let matches = about.named(name)
const name = m[1];
let matches = about.named(name);
for (const feed of matches) {
let found = mentions[name] || []
found.push(feed)
mentions[name] = found
let found = mentions[name] || [];
found.push(feed);
mentions[name] = found;
}
}
// filter the matches depending on the follow relation
Object.keys(mentions).forEach(name => {
let matches = mentions[name]
Object.keys(mentions).forEach((name) => {
let matches = mentions[name];
// if we find mention matches for a name, and we follow them / they follow us,
// then use those matches as suggestions
const meaningfulMatches = matches.filter(m => {
const meaningfulMatches = matches.filter((m) => {
return (m.rel.followsMe || m.rel.following) && m.rel.blocking === false;
})
});
if (meaningfulMatches.length > 0) {
matches = meaningfulMatches
matches = meaningfulMatches;
}
mentions[name] = matches
})
mentions[name] = matches;
});
// replace the text with a markdown link if we have unambiguous match
const replacer = (match, name, sign) => {
let matches = mentions[name]
let matches = mentions[name];
if (matches && matches.length === 1) {
// we found an exact match, don't send it to frontend as a suggestion
delete mentions[name]
delete mentions[name];
// format markdown link and put the correct sign back at the end
return `[@${matches[0].name}](${matches[0].feed})${sign ? sign : ''}`
return `[@${matches[0].name}](${matches[0].feed})${sign ? sign : ""}`;
}
return match
}
return match;
};
text = text.replace(rex, replacer);
// add blob new blob to the end of the document.
@ -211,10 +209,10 @@ const preparePreview = async function(ctx) {
id: ssb.id,
name: await about.name(ssb.id),
image: await about.image(ssb.id),
}
};
return { authorMeta, text , mentions}
}
return { authorMeta, text, mentions };
};
// handleBlobUpload ingests an uploaded form file.
// it takes care of maximum blob size (5meg), exif stripping and mime detection.
@ -242,15 +240,16 @@ const handleBlobUpload = async function (ctx) {
// implementation borrowed from ssb-blob-files
// (which operates on a slightly different data structure, sadly)
// https://github.com/ssbc/ssb-blob-files/blob/master/async/image-process.js
data = Buffer.from(removeExif(dataString), "binary")
data = Buffer.from(removeExif(dataString), "binary");
function removeExif (fileData) {
function removeExif(fileData) {
const exifOrientation = exif.load(fileData);
const orientation = exifOrientation['0th'][exif.ImageIFD.Orientation];
const clean = exif.remove(fileData);
if (orientation !== undefined) { // preserve img orientation
const exifData = { '0th': {} }
exifData['0th'][exif.ImageIFD.Orientation] = orientation;
const orientation = exifOrientation["0th"][exif.ImageIFD.Orientation];
const clean = exif.remove(fileData);
if (orientation !== undefined) {
// preserve img orientation
const exifData = { "0th": {} };
exifData["0th"][exif.ImageIFD.Orientation] = orientation;
const exifStr = exif.dump(exifData);
return exif.insert(exifStr, clean);
} else {
@ -258,8 +257,10 @@ const handleBlobUpload = async function (ctx) {
}
}
} catch (e) {
console.warn(e)
console.warn("blob was likely not a jpeg -- no exif data to remove. proceeding with blob upload");
console.warn(e);
console.warn(
"blob was likely not a jpeg -- no exif data to remove. proceeding with blob upload"
);
}
const addBlob = new Promise((resolve, reject) => {
@ -267,43 +268,42 @@ const handleBlobUpload = async function (ctx) {
pull.values([data]),
ssb.blobs.add((err, hashedBlobRef) => {
if (err) return reject(err);
console.log("added", hashedBlobRef)
resolve(hashedBlobRef)
resolve(hashedBlobRef);
})
)
})
);
});
blob = {
id: await addBlob,
name: blobUpload.name
}
const FileType = require('file-type');
name: blobUpload.name,
};
const FileType = require("file-type");
try {
let ftype = await FileType.fromBuffer(data)
blob.mime = ftype.mime
let ftype = await FileType.fromBuffer(data);
blob.mime = ftype.mime;
} catch (error) {
console.warn(error)
blob.mime = "application/octet-stream"
console.warn(error);
blob.mime = "application/octet-stream";
}
}
}
// append uploaded blob as markdown to the end of the input text
if (typeof blob !== "boolean") {
if (blob.mime.startsWith("image/")) {
text += `\n![${blob.name}](${blob.id})`
text += `\n![${blob.name}](${blob.id})`;
} else if (blob.mime.startsWith("audio/")) {
text += `\n![audio:${blob.name}](${blob.id})`
text += `\n![audio:${blob.name}](${blob.id})`;
} else if (blob.mime.startsWith("video/")) {
text += `\n![video:${blob.name}](${blob.id})`
text += `\n![video:${blob.name}](${blob.id})`;
} else {
text += `\n[${blob.name}](${blob.id})`
text += `\n[${blob.name}](${blob.id})`;
}
}
return text
}
return text;
};
const resolveCommentComponents = async function (ctx) {
const { message } = ctx.params;
const parentId = message
const parentId = message;
const parentMessage = await post.get(parentId);
const myFeedId = await meta.myFeedId();
@ -316,22 +316,21 @@ const resolveCommentComponents = async function (ctx) {
const rootMessage = hasRoot
? hasFork
? parentMessage
: await post.get(parentMessage.value.content.root)
? parentMessage
: await post.get(parentMessage.value.content.root)
: parentMessage;
const messages = await post.topicComments(rootMessage.key);
messages.push(rootMessage);
let contentWarning
let contentWarning;
if (ctx.request.body) {
const rawContentWarning = String(ctx.request.body.contentWarning).trim();
contentWarning =
rawContentWarning.length > 0 ? rawContentWarning : undefined;
}
return { messages, myFeedId, parentMessage, contentWarning }
}
return { messages, myFeedId, parentMessage, contentWarning };
};
const {
authorView,
@ -779,24 +778,37 @@ router
ctx.body = await publishView();
})
.get("/comment/:message", async (ctx) => {
const { messages, myFeedId, parentMessage } = await resolveCommentComponents(ctx)
ctx.body = await commentView({ messages, myFeedId, parentMessage })
const {
messages,
myFeedId,
parentMessage,
} = await resolveCommentComponents(ctx);
ctx.body = await commentView({ messages, myFeedId, parentMessage });
})
.post("/subtopic/preview/:message", koaBody({ multipart: true }), async (ctx) => {
const { message } = ctx.params;
const rootMessage = await post.get(message);
const myFeedId = await meta.myFeedId();
.post(
"/subtopic/preview/:message",
koaBody({ multipart: true }),
async (ctx) => {
const { message } = ctx.params;
const rootMessage = await post.get(message);
const myFeedId = await meta.myFeedId();
const rawContentWarning = String(ctx.request.body.contentWarning).trim();
const contentWarning =
rawContentWarning.length > 0 ? rawContentWarning : undefined;
const rawContentWarning = String(ctx.request.body.contentWarning).trim();
const contentWarning =
rawContentWarning.length > 0 ? rawContentWarning : undefined;
const messages = [rootMessage];
const messages = [rootMessage];
const previewData = await preparePreview(ctx);
const previewData = await preparePreview(ctx);
ctx.body = await previewSubtopicView({ messages, myFeedId, previewData, contentWarning });
})
ctx.body = await previewSubtopicView({
messages,
myFeedId,
previewData,
contentWarning,
});
}
)
.post("/subtopic/:message", koaBody(), async (ctx) => {
const { message } = ctx.params;
const text = String(ctx.request.body.text);
@ -818,13 +830,28 @@ router
ctx.body = await publishSubtopic({ message, text });
ctx.redirect(`/thread/${encodeURIComponent(message)}`);
})
.post("/comment/preview/:message", koaBody({ multipart: true }), async (ctx) => {
const { messages, contentWarning, myFeedId, parentMessage } = await resolveCommentComponents(ctx)
const previewData = await preparePreview(ctx);
.post(
"/comment/preview/:message",
koaBody({ multipart: true }),
async (ctx) => {
const {
messages,
contentWarning,
myFeedId,
parentMessage,
} = await resolveCommentComponents(ctx);
ctx.body = await previewCommentView({ messages, myFeedId, contentWarning, parentMessage, previewData });
})
const previewData = await preparePreview(ctx);
ctx.body = await previewCommentView({
messages,
myFeedId,
contentWarning,
parentMessage,
previewData,
});
}
)
.post("/comment/:message", koaBody(), async (ctx) => {
const { message } = ctx.params;
const text = String(ctx.request.body.text);
@ -854,7 +881,7 @@ router
rawContentWarning.length > 0 ? rawContentWarning : undefined;
const previewData = await preparePreview(ctx);
ctx.body = await previewView({previewData, contentWarning});
ctx.body = await previewView({ previewData, contentWarning });
})
.post("/publish/", koaBody(), async (ctx) => {
const text = String(ctx.request.body.text);

View File

@ -121,114 +121,127 @@ module.exports = ({ cooler, isPublic }) => {
// TODO: an alternative would be using ssb.names if available and just loading this as a fallback
// Two lookup tables to remove old and duplicate names
const feeds_to_name = {}
let all_the_names = {}
const feeds_to_name = {};
let all_the_names = {};
let dirty = false // just stop mindless work (nothing changed) could be smarter thou
let running = false // don't run twice
let dirty = false; // just stop mindless work (nothing changed) could be smarter thou
let running = false; // don't run twice
// transposeLookupTable flips the lookup around (form feed->name to name->feed)
// and also enhances the entries with image and relationship info
const transposeLookupTable = () => {
if (!dirty) return
if (running) return
running = true
if (!dirty) return;
if (running) return;
running = true;
// invalidate old cache
// regenerate a new thing because we don't know which entries will be gone
all_the_names = {}
all_the_names = {};
const allFeeds = Object.keys(feeds_to_name)
console.log(`updating ${allFeeds.length} feeds`)
console.time('transpose-name-index')
const allFeeds = Object.keys(feeds_to_name);
console.log(`updating ${allFeeds.length} feeds`);
console.time("transpose-name-index");
const lookups = []
const lookups = [];
for (const feed of allFeeds) {
const e = feeds_to_name[feed]
let pair = { feed, name: e.name }
lookups.push(enhanceFeedInfo(pair))
const e = feeds_to_name[feed];
let pair = { feed, name: e.name };
lookups.push(enhanceFeedInfo(pair));
}
// wait for all image and follow lookups
Promise.all(lookups).then(() => {
dirty = false // all updated
running = false
console.timeEnd('transpose-name-index')
}).catch((err) => {
running = false
console.warn('lookup transposition failed:', err)
})
}
Promise.all(lookups)
.then(() => {
dirty = false; // all updated
running = false;
console.timeEnd("transpose-name-index");
})
.catch((err) => {
running = false;
console.warn("lookup transposition failed:", err);
});
};
// this function adds the avater image and relationship to the all_the_names lookup table
const enhanceFeedInfo = ({feed, name}) => {
const enhanceFeedInfo = ({ feed, name }) => {
return new Promise((resolve, reject) => {
getAbout({feedId: feed, key: "image"}).then((img) => {
if (img !== null && typeof img !== "string" && typeof img === "object" && typeof img.link === "string") {
img = img.link
} else if (img === null) {
img = nullImage // default empty image if we dont have one
}
getAbout({ feedId: feed, key: "image" })
.then((img) => {
if (
img !== null &&
typeof img !== "string" &&
typeof img === "object" &&
typeof img.link === "string"
) {
img = img.link;
} else if (img === null) {
img = nullImage; // default empty image if we dont have one
}
models.friend.getRelationship(feed).then((rel) => {
// append and update lookup table
let feeds_named = all_the_names[name] || []
feeds_named.push({feed, name, rel, img })
all_the_names[name.toLowerCase()] = feeds_named
resolve()
models.friend
.getRelationship(feed)
.then((rel) => {
// append and update lookup table
let feeds_named = all_the_names[name] || [];
feeds_named.push({ feed, name, rel, img });
all_the_names[name.toLowerCase()] = feeds_named;
resolve();
// TODO: append if these fail!?
}).catch(reject)
}).catch(reject)
})
}
// TODO: append if these fail!?
})
.catch(reject);
})
.catch(reject);
});
};
cooler.open().then((ssb) => {
console.time('about-name-warmup') // benchmark the time it takes to stream all existing abouts
console.time("about-name-warmup"); // benchmark the time it takes to stream all existing abouts
pull(
ssb.query.read({
live: true, // keep streaming new messages as they arrive
query: [
{
$filter: { // all messages of type:about that have a name field that is typeof string
$filter: {
// all messages of type:about that have a name field that is typeof string
value: {
content: {
type: "about",
name: { $is: "string" }
name: { $is: "string" },
},
},
}
}
]
},
},
],
}),
pull.filter((msg) => {
// backlog of data is done, only new values from now on
if (msg.sync && msg.sync === true) {
console.timeEnd('about-name-warmup')
transposeLookupTable() // fire once now
setInterval(transposeLookupTable, 1000*60) // and then every 60 seconds
return false
console.timeEnd("about-name-warmup");
transposeLookupTable(); // fire once now
setInterval(transposeLookupTable, 1000 * 60); // and then every 60 seconds
return false;
}
// only pick messages about self
return msg.value.author == msg.value.content.about
return msg.value.author == msg.value.content.about;
}),
pull.drain((msg) => {
const name = msg.value.content.name
const ts = msg.value.timestamp
const feed = msg.value.author
const name = msg.value.content.name;
const ts = msg.value.timestamp;
const feed = msg.value.author;
const newEntry = { name, ts }
const currentEntry = feeds_to_name[feed]
if (typeof currentEntry == 'undefined') {
dirty = true
feeds_to_name[feed] = newEntry
} else if (currentEntry.ts < ts) { // overwrite entry if it's newer
dirty = true
feeds_to_name[feed] = newEntry
const newEntry = { name, ts };
const currentEntry = feeds_to_name[feed];
if (typeof currentEntry == "undefined") {
dirty = true;
feeds_to_name[feed] = newEntry;
} else if (currentEntry.ts < ts) {
// overwrite entry if it's newer
dirty = true;
feeds_to_name[feed] = newEntry;
}
})
)
);
});
models.about = {
@ -253,14 +266,14 @@ module.exports = ({ cooler, isPublic }) => {
); // First 8 chars of public key
},
named: (name) => {
let found = []
let matched = Object.keys(all_the_names).filter(n => {
return n.startsWith(name.toLowerCase())
})
let found = [];
let matched = Object.keys(all_the_names).filter((n) => {
return n.startsWith(name.toLowerCase());
});
for (const m of matched) {
found = found.concat(all_the_names[m])
found = found.concat(all_the_names[m]);
}
return found
return found;
},
image: async (feedId) => {
if (isPublic && (await models.about.publicWebHosting(feedId)) === false) {
@ -321,13 +334,15 @@ module.exports = ({ cooler, isPublic }) => {
},
want: async ({ blobId }) => {
debug("want blob: %s", blobId);
cooler.open().then(ssb => {
// This does not wait for the blob.
ssb.blobs.want(blobId);
}).catch(err => {
console.warn(`failed to want blob:${blobId}: ${err}`)
})
cooler
.open()
.then((ssb) => {
// This does not wait for the blob.
ssb.blobs.want(blobId);
})
.catch((err) => {
console.warn(`failed to want blob:${blobId}: ${err}`);
});
},
search: async ({ query }) => {
debug("blob search: %s", query);
@ -367,7 +382,7 @@ module.exports = ({ cooler, isPublic }) => {
following,
blocking,
};
transposeLookupTable() // invalidate @mentions table
transposeLookupTable(); // invalidate @mentions table
return ssb.publish(content);
},
follow: (feedId) =>

View File

@ -74,8 +74,15 @@ const nbsp = "\xa0";
* @param {{href: string, emoji: string, text: string }} input
*/
const template = (titlePrefix, ...elements) => {
const navLink = ({ href, emoji, text }, prefix) =>
li(a({ href, class: titlePrefix === text ? "current" : "" }, span({ class: "emoji" }, emoji), nbsp, text));
const navLink = ({ href, emoji, text }) =>
li(
a(
{ href, class: titlePrefix === text ? "current" : "" },
span({ class: "emoji" }, emoji),
nbsp,
text
)
);
const nodes = html(
{ lang: "en" },
@ -201,9 +208,9 @@ const thread = (messages) => {
const nextAuthor = lodash.get(nextMsg, "value.meta.author.name");
const nextSnippet = postSnippet(
lodash.has(nextMsg, "value.content.contentWarning") ?
lodash.get(nextMsg, "value.content.contentWarning") :
lodash.get(nextMsg, "value.content.text")
lodash.has(nextMsg, "value.content.contentWarning")
? lodash.get(nextMsg, "value.content.contentWarning")
: lodash.get(nextMsg, "value.content.text")
);
msgList.push(summary(`${nextAuthor}: ${nextSnippet}`).outerHTML);
@ -222,7 +229,10 @@ const thread = (messages) => {
}
const htmlStrings = lodash.flatten(msgList);
return div({}, { class: "thread-container", innerHTML: htmlStrings.join("") });
return div(
{},
{ class: "thread-container", innerHTML: htmlStrings.join("") }
);
};
const postSnippet = (text) => {
@ -656,14 +666,34 @@ exports.authorView = ({
return template(i18n.profile, prefix, items);
};
exports.previewCommentView = async ({ previewData, messages, myFeedId, parentMessage, contentWarning }) => {
exports.previewCommentView = async ({
previewData,
messages,
myFeedId,
parentMessage,
contentWarning,
}) => {
const publishAction = `/comment/${encodeURIComponent(messages[0].key)}`;
const preview = generatePreview({ previewData, contentWarning, action: publishAction })
return exports.commentView({ messages, myFeedId, parentMessage }, preview, previewData.text, contentWarning)
const preview = generatePreview({
previewData,
contentWarning,
action: publishAction,
});
return exports.commentView(
{ messages, myFeedId, parentMessage },
preview,
previewData.text,
contentWarning
);
};
exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, text, contentWarning) => {
exports.commentView = async (
{ messages, myFeedId, parentMessage },
preview,
text,
contentWarning
) => {
let markdownMention;
const messageElements = await Promise.all(
@ -692,10 +722,8 @@ exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, tex
return template(
i18n.commentTitle({ authorName }),
div({ class: "thread-container" },
messageElements,
),
preview !== undefined ? preview : '',
div({ class: "thread-container" }, messageElements),
preview !== undefined ? preview : "",
p(
...i18n.commentLabel({ publicOrPrivate, markdownUrl }),
...maybeSubtopicText
@ -708,8 +736,7 @@ exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, tex
required: true,
name: "text",
},
text ? text :
isPrivate ? null : markdownMention
text ? text : isPrivate ? null : markdownMention
),
label(
i18n.contentWarningLabel,
@ -717,12 +744,12 @@ exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, tex
name: "contentWarning",
type: "text",
class: "contentWarning",
value: contentWarning ? contentWarning : '',
value: contentWarning ? contentWarning : "",
placeholder: i18n.contentWarningPlaceholder,
})
),
button({ type: "submit" }, i18n.preview),
label({ class: "file-button", for: "blob"}, i18n.attachFiles),
button({ type: "submit" }, i18n.preview),
label({ class: "file-button", for: "blob" }, i18n.attachFiles),
input({ type: "file", id: "blob", name: "blob" })
)
);
@ -805,14 +832,14 @@ exports.publishView = (preview, text, contentWarning) => {
section(
h1(i18n.publish),
form(
{
{
action: "/publish/preview",
method: "post",
enctype: "multipart/form-data",
},
label(
i18n.publishLabel({ markdownUrl, linkTarget: "_blank" }),
textarea({ required: true, name: "text" }, text ? text : '')
textarea({ required: true, name: "text" }, text ? text : "")
),
label(
i18n.contentWarningLabel,
@ -820,24 +847,24 @@ exports.publishView = (preview, text, contentWarning) => {
name: "contentWarning",
type: "text",
class: "contentWarning",
value: contentWarning ? contentWarning : '',
value: contentWarning ? contentWarning : "",
placeholder: i18n.contentWarningPlaceholder,
})
),
button({ type: "submit" }, i18n.preview),
label({ class: "file-button", for: "blob"}, i18n.attachFiles),
label({ class: "file-button", for: "blob" }, i18n.attachFiles),
input({ type: "file", id: "blob", name: "blob" })
)
),
preview ? preview : '',
preview ? preview : "",
p(i18n.publishCustomInfo({ href: "/publish/custom" }))
);
};
const generatePreview = ({ previewData, contentWarning, action }) => {
const { authorMeta, text, mentions } = previewData
const { authorMeta, text, mentions } = previewData;
// craft message that looks like it came from the db
// craft message that looks like it came from the db
// cb: this kinda fragile imo? this is for getting a proper post styling ya?
const msg = {
key: "%non-existant.preview",
@ -845,8 +872,8 @@ const generatePreview = ({ previewData, contentWarning, action }) => {
author: authorMeta.id,
// sequence: -1,
content: {
type:"post",
text: text
type: "post",
text: text,
},
timestamp: Date.now(),
meta: {
@ -855,65 +882,82 @@ const generatePreview = ({ previewData, contentWarning, action }) => {
author: {
name: authorMeta.name,
avatar: {
url: `/image/64/${encodeURIComponent(authorMeta.image)}`
}
url: `/image/64/${encodeURIComponent(authorMeta.image)}`,
},
},
}
}
}
if (contentWarning) msg.value.content.contentWarning = contentWarning
},
},
};
if (contentWarning) msg.value.content.contentWarning = contentWarning;
const ts = new Date(msg.value.timestamp);
lodash.set(msg, "value.meta.timestamp.received.iso8601", ts.toISOString());
const ago = Date.now() - Number(ts);
const prettyAgo = prettyMs(ago, { compact: true });
lodash.set(msg, "value.meta.timestamp.received.since", prettyAgo);
return div(
Object.keys(mentions).length === 0 ? "" :
section({ class: "mention-suggestions"},
h2(i18n.mentionsMatching),
Object.keys(mentions).map((name) => {
let matches = mentions[name]
Object.keys(mentions).length === 0
? ""
: section(
{ class: "mention-suggestions" },
h2(i18n.mentionsMatching),
Object.keys(mentions).map((name) => {
let matches = mentions[name];
return div(
matches.map(m => {
let relationship = { emoji: "", desc: "" }
if (m.rel.followsMe && m.rel.following) {
// mutuals get the handshake emoji
relationship.emoji = "🤝"
relationship.desc = i18n.relationshipMutuals
} else if (m.rel.following) {
// if we're following that's an eyes emoji
relationship.emoji = "👀"
relationship.desc = i18n.relationshipFollowing
} else if (m.rel.followsMe) {
// follower has waving-hand emoji
relationship.emoji = "👋"
relationship.desc = i18n.relationshipTheyFollow
} else {
// no relationship has question mark emoji
relationship.emoji = "❓"
relationship.desc = i18n.relationshipNotFollowing
}
return div({ class: "mentions-container" },
a({ class: "mentions-image",
href: `/author/${encodeURIComponent(m.feed)}` },
img({ src: `/image/64/${encodeURIComponent(m.img)}`})
),
a(
{ class: "mentions-name", href: `/author/${encodeURIComponent(m.feed)}` },
m.name
),
div({ class: "emo-rel" },
span({ class: "emoji", title: relationship.desc }, relationship.emoji),
span({ class: "mentions-listing" }, `[@${m.name}](${m.feed})`)
)
)
return div(
matches.map((m) => {
let relationship = { emoji: "", desc: "" };
if (m.rel.followsMe && m.rel.following) {
// mutuals get the handshake emoji
relationship.emoji = "🤝";
relationship.desc = i18n.relationshipMutuals;
} else if (m.rel.following) {
// if we're following that's an eyes emoji
relationship.emoji = "👀";
relationship.desc = i18n.relationshipFollowing;
} else if (m.rel.followsMe) {
// follower has waving-hand emoji
relationship.emoji = "👋";
relationship.desc = i18n.relationshipTheyFollow;
} else {
// no relationship has question mark emoji
relationship.emoji = "❓";
relationship.desc = i18n.relationshipNotFollowing;
}
return div(
{ class: "mentions-container" },
a(
{
class: "mentions-image",
href: `/author/${encodeURIComponent(m.feed)}`,
},
img({ src: `/image/64/${encodeURIComponent(m.img)}` })
),
a(
{
class: "mentions-name",
href: `/author/${encodeURIComponent(m.feed)}`,
},
m.name
),
div(
{ class: "emo-rel" },
span(
{ class: "emoji", title: relationship.desc },
relationship.emoji
),
span(
{ class: "mentions-listing" },
`[@${m.name}](${m.feed})`
)
)
);
})
);
})
)
})
),
section({ class: "post-preview" },
post({msg}),
),
section(
{ class: "post-preview" },
post({ msg }),
// doesn't need blobs, preview adds them to the text
form(
@ -922,24 +966,28 @@ const generatePreview = ({ previewData, contentWarning, action }) => {
name: "contentWarning",
type: "hidden",
value: contentWarning,
}),
input({
name: "text",
type: "hidden",
value: text,
}),
button({ type: "submit" }, i18n.publish),
),
input({
name: "text",
type: "hidden",
value: text,
}),
button({ type: "submit" }, i18n.publish)
)
)
)
}
);
};
exports.previewView = ({ previewData, contentWarning }) => {
const publishAction = "/publish";
const preview = generatePreview({ previewData, contentWarning, action: publishAction })
return exports.publishView(preview, previewData.text, contentWarning)
}
const preview = generatePreview({
previewData,
contentWarning,
action: publishAction,
});
return exports.publishView(preview, previewData.text, contentWarning);
};
/**
* @param {{status: object, peers: any[], theme: string, themeNames: string[], version: string }} input
@ -1173,14 +1221,33 @@ exports.threadsView = ({ messages }) => {
});
};
exports.previewSubtopicView = async ({ previewData, messages, myFeedId, contentWarning }) => {
exports.previewSubtopicView = async ({
previewData,
messages,
myFeedId,
contentWarning,
}) => {
const publishAction = `/subtopic/${encodeURIComponent(messages[0].key)}`;
const preview = generatePreview({ previewData, contentWarning, action: publishAction })
return exports.subtopicView({ messages, myFeedId }, preview, previewData.text, contentWarning)
const preview = generatePreview({
previewData,
contentWarning,
action: publishAction,
});
return exports.subtopicView(
{ messages, myFeedId },
preview,
previewData.text,
contentWarning
);
};
exports.subtopicView = async ({ messages, myFeedId }, preview, text, contentWarning) => {
exports.subtopicView = async (
{ messages, myFeedId },
preview,
text,
contentWarning
) => {
const subtopicForm = `/subtopic/preview/${encodeURIComponent(
messages[messages.length - 1].key
)}`;
@ -1206,10 +1273,8 @@ exports.subtopicView = async ({ messages, myFeedId }, preview, text, contentWarn
return template(
i18n.subtopicTitle({ authorName }),
div({ class: "thread-container" },
messageElements,
),
preview !== undefined ? preview : '',
div({ class: "thread-container" }, messageElements),
preview !== undefined ? preview : "",
p(i18n.subtopicLabel({ markdownUrl })),
form(
{ action: subtopicForm, method: "post", enctype: "multipart/form-data" },
@ -1227,12 +1292,12 @@ exports.subtopicView = async ({ messages, myFeedId }, preview, text, contentWarn
name: "contentWarning",
type: "text",
class: "contentWarning",
value: contentWarning ? contentWarning : '',
value: contentWarning ? contentWarning : "",
placeholder: i18n.contentWarningPlaceholder,
})
),
button({ type: "submit" }, i18n.preview),
label({ class: "file-button", for: "blob"}, i18n.attachFiles),
button({ type: "submit" }, i18n.preview),
label({ class: "file-button", for: "blob" }, i18n.attachFiles),
input({ type: "file", id: "blob", name: "blob" })
)
);

View File

@ -31,38 +31,41 @@ const paths = [
tap.setTimeout(0);
tap.test("DNS rebind attack fails", (t) => {
t.plan(1);
supertest(app)
.get("/inbox")
.set("Host", "example.com")
.expect(400)
.end(t.error);
.end((err) => {
t.equal(err, null);
t.end();
});
});
tap.test("CSRF attack should fail with no referer", (t) => {
t.plan(1);
supertest(app).post("/conn/settings/stop").expect(400).end(t.error);
supertest(app).post("/conn/settings/stop").expect(400).end(t.end);
});
tap.test("CSRF attack should fail with wrong referer", (t) => {
t.plan(1);
supertest(app)
.post("/conn/settings/stop")
.set("Host", "example.com")
.expect(400)
.end(t.error);
.end((err) => {
t.equal(err, null);
t.end();
});
});
paths.forEach((path) => {
tap.test(path, (t) => {
t.plan(1);
supertest(app)
.get(path)
.set("Host", "localhost")
.expect(200)
.end((err) => {
console.log(path);
t.error(err);
t.equal(err, null);
console.log("done:" + path);
t.end();
});
});
});