Comparing version 0.1.11 to 0.1.12
757
cjs/main.js
@@ -18,20 +18,2 @@ var __defProp = Object.defineProperty; | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
var __accessCheck = (obj, member, msg) => { | ||
if (!member.has(obj)) | ||
throw TypeError("Cannot " + msg); | ||
}; | ||
var __privateGet = (obj, member, getter) => { | ||
__accessCheck(obj, member, "read from private field"); | ||
return getter ? getter.call(obj) : member.get(obj); | ||
}; | ||
var __privateAdd = (obj, member, value) => { | ||
if (member.has(obj)) | ||
throw TypeError("Cannot add the same private member more than once"); | ||
member instanceof WeakSet ? member.add(obj) : member.set(obj, value); | ||
}; | ||
var __privateSet = (obj, member, value, setter) => { | ||
__accessCheck(obj, member, "write to private field"); | ||
setter ? setter.call(obj, value) : member.set(obj, value); | ||
return value; | ||
}; | ||
@@ -53,2 +35,5 @@ // https://raw.githubusercontent.com/skymethod/minipub/master/src/threadcap/threadcap.ts | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/check.ts | ||
function isNonEmpty(value) { | ||
return value.trim().length > 0; | ||
} | ||
function isStringRecord(obj) { | ||
@@ -64,3 +49,3 @@ return typeof obj === "object" && obj !== null && !Array.isArray(obj) && obj.constructor === Object; | ||
// https:/raw.githubusercontent.com/skymethod/denoflare/171f4cdb1266928d2ff67f8a851827ab51bae937/common/bytes.ts | ||
// https:/raw.githubusercontent.com/skymethod/denoflare/v0.5.12/common/bytes.ts | ||
var _Bytes = class { | ||
@@ -175,553 +160,5 @@ constructor(bytes) { | ||
// https:/deno.land/std@0.173.0/datetime/constants.ts | ||
var SECOND = 1e3; | ||
var MINUTE = SECOND * 60; | ||
var HOUR = MINUTE * 60; | ||
var DAY = HOUR * 24; | ||
var WEEK = DAY * 7; | ||
// https:/deno.land/std@0.220.1/encoding/_util.ts | ||
var encoder = new TextEncoder(); | ||
// https:/deno.land/std@0.173.0/datetime/_common.ts | ||
var Tokenizer = class { | ||
constructor(rules = []) { | ||
this.rules = rules; | ||
} | ||
addRule(test, fn) { | ||
this.rules.push({ test, fn }); | ||
return this; | ||
} | ||
tokenize(string, receiver = (token) => token) { | ||
function* generator(rules) { | ||
let index = 0; | ||
for (const rule of rules) { | ||
const result = rule.test(string); | ||
if (result) { | ||
const { value, length } = result; | ||
index += length; | ||
string = string.slice(length); | ||
const token = { ...rule.fn(value), index }; | ||
yield receiver(token); | ||
yield* generator(rules); | ||
} | ||
} | ||
} | ||
const tokenGenerator = generator(this.rules); | ||
const tokens = []; | ||
for (const token of tokenGenerator) { | ||
tokens.push(token); | ||
} | ||
if (string.length) { | ||
throw new Error(`parser error: string not fully parsed! ${string.slice(0, 25)}`); | ||
} | ||
return tokens; | ||
} | ||
}; | ||
function digits(value, count = 2) { | ||
return String(value).padStart(count, "0"); | ||
} | ||
function createLiteralTestFunction(value) { | ||
return (string) => { | ||
return string.startsWith(value) ? { value, length: value.length } : void 0; | ||
}; | ||
} | ||
function createMatchTestFunction(match) { | ||
return (string) => { | ||
const result = match.exec(string); | ||
if (result) | ||
return { value: result, length: result[0].length }; | ||
}; | ||
} | ||
var defaultRules = [ | ||
{ | ||
test: createLiteralTestFunction("yyyy"), | ||
fn: () => ({ type: "year", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("yy"), | ||
fn: () => ({ type: "year", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("MM"), | ||
fn: () => ({ type: "month", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("M"), | ||
fn: () => ({ type: "month", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("dd"), | ||
fn: () => ({ type: "day", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("d"), | ||
fn: () => ({ type: "day", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("HH"), | ||
fn: () => ({ type: "hour", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("H"), | ||
fn: () => ({ type: "hour", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("hh"), | ||
fn: () => ({ | ||
type: "hour", | ||
value: "2-digit", | ||
hour12: true | ||
}) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("h"), | ||
fn: () => ({ | ||
type: "hour", | ||
value: "numeric", | ||
hour12: true | ||
}) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("mm"), | ||
fn: () => ({ type: "minute", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("m"), | ||
fn: () => ({ type: "minute", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("ss"), | ||
fn: () => ({ type: "second", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("s"), | ||
fn: () => ({ type: "second", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("SSS"), | ||
fn: () => ({ type: "fractionalSecond", value: 3 }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("SS"), | ||
fn: () => ({ type: "fractionalSecond", value: 2 }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("S"), | ||
fn: () => ({ type: "fractionalSecond", value: 1 }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("a"), | ||
fn: (value) => ({ | ||
type: "dayPeriod", | ||
value | ||
}) | ||
}, | ||
{ | ||
test: createMatchTestFunction(/^(')(?<value>\\.|[^\']*)\1/), | ||
fn: (match) => ({ | ||
type: "literal", | ||
value: match.groups.value | ||
}) | ||
}, | ||
{ | ||
test: createMatchTestFunction(/^.+?\s*/), | ||
fn: (match) => ({ | ||
type: "literal", | ||
value: match[0] | ||
}) | ||
} | ||
]; | ||
var _format; | ||
var DateTimeFormatter = class { | ||
constructor(formatString, rules = defaultRules) { | ||
__privateAdd(this, _format, void 0); | ||
const tokenizer = new Tokenizer(rules); | ||
__privateSet(this, _format, tokenizer.tokenize(formatString, ({ type, value, hour12 }) => { | ||
const result = { | ||
type, | ||
value | ||
}; | ||
if (hour12) | ||
result.hour12 = hour12; | ||
return result; | ||
})); | ||
} | ||
format(date, options = {}) { | ||
let string = ""; | ||
const utc = options.timeZone === "UTC"; | ||
for (const token of __privateGet(this, _format)) { | ||
const type = token.type; | ||
switch (type) { | ||
case "year": { | ||
const value = utc ? date.getUTCFullYear() : date.getFullYear(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2).slice(-2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "month": { | ||
const value = (utc ? date.getUTCMonth() : date.getMonth()) + 1; | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "day": { | ||
const value = utc ? date.getUTCDate() : date.getDate(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "hour": { | ||
let value = utc ? date.getUTCHours() : date.getHours(); | ||
value -= token.hour12 && date.getHours() > 12 ? 12 : 0; | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "minute": { | ||
const value = utc ? date.getUTCMinutes() : date.getMinutes(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "second": { | ||
const value = utc ? date.getUTCSeconds() : date.getSeconds(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "fractionalSecond": { | ||
const value = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); | ||
string += digits(value, Number(token.value)); | ||
break; | ||
} | ||
case "timeZoneName": { | ||
break; | ||
} | ||
case "dayPeriod": { | ||
string += token.value ? date.getHours() >= 12 ? "PM" : "AM" : ""; | ||
break; | ||
} | ||
case "literal": { | ||
string += token.value; | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: { ${token.type} ${token.value} }`); | ||
} | ||
} | ||
return string; | ||
} | ||
parseToParts(string) { | ||
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q; | ||
const parts = []; | ||
for (const token of __privateGet(this, _format)) { | ||
const type = token.type; | ||
let value = ""; | ||
switch (token.type) { | ||
case "year": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_a = /^\d{1,4}/.exec(string)) == null ? void 0 : _a[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_b = /^\d{1,2}/.exec(string)) == null ? void 0 : _b[0]; | ||
break; | ||
} | ||
} | ||
break; | ||
} | ||
case "month": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_c = /^\d{1,2}/.exec(string)) == null ? void 0 : _c[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_d = /^\d{2}/.exec(string)) == null ? void 0 : _d[0]; | ||
break; | ||
} | ||
case "narrow": { | ||
value = (_e = /^[a-zA-Z]+/.exec(string)) == null ? void 0 : _e[0]; | ||
break; | ||
} | ||
case "short": { | ||
value = (_f = /^[a-zA-Z]+/.exec(string)) == null ? void 0 : _f[0]; | ||
break; | ||
} | ||
case "long": { | ||
value = (_g = /^[a-zA-Z]+/.exec(string)) == null ? void 0 : _g[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "day": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_h = /^\d{1,2}/.exec(string)) == null ? void 0 : _h[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_i = /^\d{2}/.exec(string)) == null ? void 0 : _i[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "hour": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_j = /^\d{1,2}/.exec(string)) == null ? void 0 : _j[0]; | ||
if (token.hour12 && parseInt(value) > 12) { | ||
console.error(`Trying to parse hour greater than 12. Use 'H' instead of 'h'.`); | ||
} | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_k = /^\d{2}/.exec(string)) == null ? void 0 : _k[0]; | ||
if (token.hour12 && parseInt(value) > 12) { | ||
console.error(`Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`); | ||
} | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "minute": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_l = /^\d{1,2}/.exec(string)) == null ? void 0 : _l[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_m = /^\d{2}/.exec(string)) == null ? void 0 : _m[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "second": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_n = /^\d{1,2}/.exec(string)) == null ? void 0 : _n[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_o = /^\d{2}/.exec(string)) == null ? void 0 : _o[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "fractionalSecond": { | ||
value = (_p = new RegExp(`^\\d{${token.value}}`).exec(string)) == null ? void 0 : _p[0]; | ||
break; | ||
} | ||
case "timeZoneName": { | ||
value = token.value; | ||
break; | ||
} | ||
case "dayPeriod": { | ||
value = (_q = /^(A|P)M/.exec(string)) == null ? void 0 : _q[0]; | ||
break; | ||
} | ||
case "literal": { | ||
if (!string.startsWith(token.value)) { | ||
throw Error(`Literal "${token.value}" not found "${string.slice(0, 25)}"`); | ||
} | ||
value = token.value; | ||
break; | ||
} | ||
default: | ||
throw Error(`${token.type} ${token.value}`); | ||
} | ||
if (!value) { | ||
throw Error(`value not valid for token { ${type} ${value} } ${string.slice(0, 25)}`); | ||
} | ||
parts.push({ type, value }); | ||
string = string.slice(value.length); | ||
} | ||
if (string.length) { | ||
throw Error(`datetime string was not fully parsed! ${string.slice(0, 25)}`); | ||
} | ||
return parts; | ||
} | ||
sortDateTimeFormatPart(parts) { | ||
let result = []; | ||
const typeArray = [ | ||
"year", | ||
"month", | ||
"day", | ||
"hour", | ||
"minute", | ||
"second", | ||
"fractionalSecond" | ||
]; | ||
for (const type of typeArray) { | ||
const current = parts.findIndex((el) => el.type === type); | ||
if (current !== -1) { | ||
result = result.concat(parts.splice(current, 1)); | ||
} | ||
} | ||
result = result.concat(parts); | ||
return result; | ||
} | ||
partsToDate(parts) { | ||
const date = new Date(); | ||
const utc = parts.find((part) => part.type === "timeZoneName" && part.value === "UTC"); | ||
const dayPart = parts.find((part) => part.type === "day"); | ||
utc ? date.setUTCHours(0, 0, 0, 0) : date.setHours(0, 0, 0, 0); | ||
for (const part of parts) { | ||
switch (part.type) { | ||
case "year": { | ||
const value = Number(part.value.padStart(4, "20")); | ||
utc ? date.setUTCFullYear(value) : date.setFullYear(value); | ||
break; | ||
} | ||
case "month": { | ||
const value = Number(part.value) - 1; | ||
if (dayPart) { | ||
utc ? date.setUTCMonth(value, Number(dayPart.value)) : date.setMonth(value, Number(dayPart.value)); | ||
} else { | ||
utc ? date.setUTCMonth(value) : date.setMonth(value); | ||
} | ||
break; | ||
} | ||
case "day": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCDate(value) : date.setDate(value); | ||
break; | ||
} | ||
case "hour": { | ||
let value = Number(part.value); | ||
const dayPeriod = parts.find((part2) => part2.type === "dayPeriod"); | ||
if ((dayPeriod == null ? void 0 : dayPeriod.value) === "PM") | ||
value += 12; | ||
utc ? date.setUTCHours(value) : date.setHours(value); | ||
break; | ||
} | ||
case "minute": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCMinutes(value) : date.setMinutes(value); | ||
break; | ||
} | ||
case "second": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCSeconds(value) : date.setSeconds(value); | ||
break; | ||
} | ||
case "fractionalSecond": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCMilliseconds(value) : date.setMilliseconds(value); | ||
break; | ||
} | ||
} | ||
} | ||
return date; | ||
} | ||
parse(string) { | ||
const parts = this.parseToParts(string); | ||
const sortParts = this.sortDateTimeFormatPart(parts); | ||
return this.partsToDate(sortParts); | ||
} | ||
}; | ||
_format = new WeakMap(); | ||
// https:/deno.land/std@0.173.0/datetime/to_imf.ts | ||
function toIMF(date) { | ||
function dtPad(v, lPad = 2) { | ||
return v.padStart(lPad, "0"); | ||
} | ||
const d = dtPad(date.getUTCDate().toString()); | ||
const h = dtPad(date.getUTCHours().toString()); | ||
const min = dtPad(date.getUTCMinutes().toString()); | ||
const s = dtPad(date.getUTCSeconds().toString()); | ||
const y = date.getUTCFullYear(); | ||
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; | ||
const months = [ | ||
"Jan", | ||
"Feb", | ||
"Mar", | ||
"Apr", | ||
"May", | ||
"Jun", | ||
"Jul", | ||
"Aug", | ||
"Sep", | ||
"Oct", | ||
"Nov", | ||
"Dec" | ||
]; | ||
return `${days[date.getUTCDay()]}, ${d} ${months[date.getUTCMonth()]} ${y} ${h}:${min}:${s} GMT`; | ||
} | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/crypto.ts | ||
@@ -732,3 +169,3 @@ async function computeHttpSignatureHeaders(opts) { | ||
const digest = body ? `SHA-256=${(await Bytes.ofUtf8(body).sha256()).base64()}` : void 0; | ||
const date = toIMF(new Date()); | ||
const date = new Date().toUTCString(); | ||
const signed = { | ||
@@ -774,2 +211,9 @@ "(request-target)": `${method.toLowerCase()} ${pathname}`, | ||
} | ||
function destructureThreadcapUrl(url) { | ||
const m = /^(at:\/\/)([^/]+)(\/.*?)$/.exec(url); | ||
const tmpUrl = m ? `${m[1]}${m[2].replaceAll(":", "%3A")}${m[3]}` : void 0; | ||
const { protocol, hostname: tmpHostname, pathname, searchParams } = new URL(tmpUrl != null ? tmpUrl : url); | ||
const hostname = tmpUrl ? tmpHostname.replaceAll("%3A", ":") : tmpHostname; | ||
return { protocol, hostname, pathname, searchParams }; | ||
} | ||
async function findOrFetchTextResponse(url, after, fetcher, cache, opts) { | ||
@@ -836,3 +280,3 @@ const existing = await cache.get(url, after); | ||
throw new Error(`Unexpected type for object: ${JSON.stringify(object)}`); | ||
if (!/^(Note|Article|Video|PodcastEpisode)$/.test(type)) | ||
if (!/^(Note|Article|Video|PodcastEpisode|Question)$/.test(type)) | ||
throw new Error(`Unexpected type: ${type}`); | ||
@@ -984,3 +428,4 @@ if (typeof id !== "string") | ||
throw new Error(`Expected 'published' to be a string, found ${JSON.stringify(published)}`); | ||
return { url, published, attachments, content, attributedTo, summary }; | ||
const questionOptions = computeQuestionOptions(object); | ||
return { url, published, attachments, content, attributedTo, summary, questionOptions }; | ||
} | ||
@@ -999,2 +444,25 @@ function computeUrl(url) { | ||
} | ||
function computeQuestionOptions(obj) { | ||
let rt; | ||
if (obj.type === "Question") { | ||
for (const prop of ["oneOf", "anyOf"]) { | ||
const val = obj[prop]; | ||
if (Array.isArray(val)) { | ||
for (const item of val) { | ||
if (isStringRecord(item) && item.type === "Note" && typeof item.name === "string") { | ||
if (!rt) | ||
rt = []; | ||
rt.push(item.name); | ||
} else { | ||
throw new Error(`Unsupported Question '${prop}' item: ${JSON.stringify(item)}`); | ||
} | ||
} | ||
return rt; | ||
} else if (val !== void 0) { | ||
throw new Error(`Unsupported Question '${prop}' value: ${JSON.stringify(val)}`); | ||
} | ||
} | ||
} | ||
return rt; | ||
} | ||
function computeAttributedTo(attributedTo) { | ||
@@ -1040,2 +508,4 @@ if (typeof attributedTo === "string") | ||
return { und: stringVal }; | ||
if (obj.type === "Video" && typeof obj.name === "string" && isNonEmpty(obj.name)) | ||
return { und: obj.name }; | ||
} | ||
@@ -1123,2 +593,125 @@ function computeAttachments(object) { | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/threadcap/threadcap_bluesky.ts | ||
var BlueskyProtocolImplementation = { | ||
async initThreadcap(url, opts) { | ||
const { uri, nodes, commenters } = await getThread(url, opts, 1e3); | ||
return { protocol: "bluesky", roots: [uri], nodes, commenters }; | ||
}, | ||
async fetchComment(id, opts) { | ||
const { uri, nodes } = await getThread(id, opts, 0); | ||
const node = nodes[uri]; | ||
if (!node) | ||
throw new Error(`fetchComment: no node!`); | ||
if (!node.comment) | ||
throw new Error(`fetchComment: no node comment!`); | ||
return node.comment; | ||
}, | ||
async fetchCommenter(attributedTo, opts) { | ||
const { updateTime, fetcher, cache, bearerToken } = opts; | ||
const res = await getProfile(attributedTo, { updateTime, fetcher, cache, bearerToken }); | ||
return computeCommenter2(res, updateTime); | ||
}, | ||
async fetchReplies(id, opts) { | ||
const { uri, nodes } = await getThread(id, opts, 1); | ||
const node = nodes[uri]; | ||
if (!node) | ||
throw new Error(`fetchReplies: no node!`); | ||
if (!node.replies) | ||
throw new Error(`fetchReplies: no node replies!`); | ||
return node.replies; | ||
} | ||
}; | ||
function makeUrl(url, queryParams) { | ||
const u = new URL(url); | ||
Object.entries(queryParams).forEach(([n, v]) => u.searchParams.set(n, v.toString())); | ||
return u.toString(); | ||
} | ||
function isGetPostThreadResponse(obj) { | ||
return isStringRecord(obj) && isThreadViewPost(obj.thread); | ||
} | ||
function isThreadViewPost(obj) { | ||
return isStringRecord(obj) && obj["$type"] === "app.bsky.feed.defs#threadViewPost" && isStringRecord(obj.post) && typeof obj.post.uri === "string" && isStringRecord(obj.post.author) && typeof obj.post.author.did === "string" && typeof obj.post.author.handle === "string" && (obj.post.author.displayName === void 0 || typeof obj.post.author.displayName === "string") && (obj.post.author.avatar === void 0 || typeof obj.post.author.avatar === "string") && Array.isArray(obj.post.author.labels) && isStringRecord(obj.post.record) && obj.post.record["$type"] === "app.bsky.feed.post" && typeof obj.post.record.text === "string" && (obj.post.replyCount === void 0 || typeof obj.post.replyCount === "number") && (obj.replies === void 0 || Array.isArray(obj.replies) && obj.replies.every(isThreadViewPost)); | ||
} | ||
function isGetProfileResponse(obj) { | ||
return isStringRecord(obj) && typeof obj.did === "string" && typeof obj.handle === "string" && typeof obj.displayName === "string" && (obj.avatar === void 0 || typeof obj.avatar === "string"); | ||
} | ||
async function fetchAppviewJson(url, { updateTime, fetcher, cache, bearerToken }) { | ||
return await findOrFetchJson(url, updateTime, fetcher, cache, { accept: "application/json", authorization: bearerToken ? `Bearer ${bearerToken}` : void 0 }); | ||
} | ||
async function getProfile(handleOrDid, { updateTime, fetcher, cache, bearerToken }) { | ||
const res = await fetchAppviewJson(makeUrl("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile", { actor: handleOrDid }), { updateTime, fetcher, cache, bearerToken }); | ||
if (!isGetProfileResponse(res)) | ||
throw new Error(JSON.stringify(res, void 0, 2)); | ||
return res; | ||
} | ||
async function getThread(url, opts, depth) { | ||
const { debug, fetcher, updateTime = new Date().toISOString(), cache, bearerToken } = opts; | ||
const { protocol, pathname } = destructureThreadcapUrl(url); | ||
const resolveDid = async (handleOrDid) => { | ||
if (handleOrDid.startsWith("did:")) | ||
return handleOrDid; | ||
const res2 = await getProfile(handleOrDid, { updateTime, fetcher, cache, bearerToken }); | ||
return res2.did; | ||
}; | ||
const atUri = await (async () => { | ||
var _a; | ||
if (protocol === "at:") | ||
return url; | ||
if (protocol === "https:") { | ||
const [_, handleOrDid, postId] = (_a = /^\/profile\/([^/]+)\/post\/([^/]+)$/.exec(pathname)) != null ? _a : []; | ||
if (handleOrDid && postId) | ||
return `at://${await resolveDid(handleOrDid)}/app.bsky.feed.post/${postId}`; | ||
} | ||
throw new Error(`Unexpected bluesky url: ${url}`); | ||
})(); | ||
const res = await fetchAppviewJson(makeUrl("https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread", { uri: atUri, depth, parentHeight: 0 }), { updateTime, fetcher, cache, bearerToken }); | ||
if (!isGetPostThreadResponse(res)) | ||
throw new Error(`Expected GetPostThreadResponse: ${JSON.stringify(res, void 0, 2)}`); | ||
if (debug) | ||
console.log(JSON.stringify(res, void 0, 2)); | ||
const nodes = {}; | ||
const commenters = {}; | ||
const processThread = (thread) => { | ||
const { uri: uri2, author, replyCount } = thread.post; | ||
let replies; | ||
let repliesAsof; | ||
if (replyCount === void 0) { | ||
if (thread.replies !== void 0) | ||
throw new Error(`Expected no thread.replies for undefined replyCount`); | ||
} else { | ||
if (thread.replies !== void 0) { | ||
replies = []; | ||
for (const reply of thread.replies) { | ||
const replyUri = processThread(reply); | ||
replies.push(replyUri); | ||
} | ||
repliesAsof = updateTime; | ||
} | ||
} | ||
nodes[uri2] = { | ||
replies, | ||
repliesAsof, | ||
comment: { | ||
attachments: [], | ||
content: { und: thread.post.record.text }, | ||
attributedTo: author.did | ||
}, | ||
commentAsof: updateTime | ||
}; | ||
commenters[author.did] = computeCommenter2(author, updateTime); | ||
return uri2; | ||
}; | ||
const uri = processThread(res.thread); | ||
return { uri, nodes, commenters }; | ||
} | ||
function computeCommenter2(author, updateTime) { | ||
var _a; | ||
return { | ||
asof: updateTime, | ||
name: (_a = author.displayName) != null ? _a : author.handle, | ||
fqUsername: author.handle, | ||
icon: author.avatar ? { url: author.avatar } : void 0 | ||
}; | ||
} | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/threadcap/threadcap_twitter.ts | ||
@@ -1287,10 +880,10 @@ var TwitterProtocolImplementation = { | ||
function isValidProtocol(protocol) { | ||
return protocol === "activitypub" || protocol === "twitter"; | ||
return protocol === "activitypub" || protocol === "twitter" || protocol === "bluesky"; | ||
} | ||
var MAX_LEVELS = 1e3; | ||
async function makeThreadcap(url, opts) { | ||
const { cache, userAgent, protocol, bearerToken, debug } = opts; | ||
const { cache, updateTime, userAgent, protocol, bearerToken, debug } = opts; | ||
const fetcher = makeFetcherWithUserAgent(opts.fetcher, userAgent); | ||
const implementation = computeProtocolImplementation(protocol); | ||
return await implementation.initThreadcap(url, { fetcher, cache, bearerToken, debug }); | ||
return await implementation.initThreadcap(url, { fetcher, cache, updateTime, bearerToken, debug }); | ||
} | ||
@@ -1442,2 +1035,4 @@ async function updateThreadcap(threadcap, opts) { | ||
return TwitterProtocolImplementation; | ||
if (protocol === "bluesky") | ||
return BlueskyProtocolImplementation; | ||
throw new Error(`Unsupported protocol: ${protocol}`); | ||
@@ -1453,8 +1048,12 @@ } | ||
const updateComment = !node.commentAsof || node.commentAsof < updateTime; | ||
if (updateComment) { | ||
const existingCommenter = node.comment ? threadcap.commenters[node.comment.attributedTo] : void 0; | ||
const updateCommenter = !existingCommenter || existingCommenter.asof < updateTime; | ||
if (updateComment || updateCommenter) { | ||
try { | ||
node.comment = await implementation.fetchComment(id, opts); | ||
if (updateComment) { | ||
node.comment = await implementation.fetchComment(id, opts); | ||
} | ||
const { attributedTo } = node.comment; | ||
const existingCommenter = threadcap.commenters[attributedTo]; | ||
if (!existingCommenter || existingCommenter.asof < updateTime) { | ||
const existingCommenter2 = threadcap.commenters[attributedTo]; | ||
if (!existingCommenter2 || existingCommenter2.asof < updateTime) { | ||
threadcap.commenters[attributedTo] = await implementation.fetchCommenter(attributedTo, opts); | ||
@@ -1461,0 +1060,0 @@ } |
758
esm/main.js
@@ -1,21 +0,5 @@ | ||
var __accessCheck = (obj, member, msg) => { | ||
if (!member.has(obj)) | ||
throw TypeError("Cannot " + msg); | ||
}; | ||
var __privateGet = (obj, member, getter) => { | ||
__accessCheck(obj, member, "read from private field"); | ||
return getter ? getter.call(obj) : member.get(obj); | ||
}; | ||
var __privateAdd = (obj, member, value) => { | ||
if (member.has(obj)) | ||
throw TypeError("Cannot add the same private member more than once"); | ||
member instanceof WeakSet ? member.add(obj) : member.set(obj, value); | ||
}; | ||
var __privateSet = (obj, member, value, setter) => { | ||
__accessCheck(obj, member, "write to private field"); | ||
setter ? setter.call(obj, value) : member.set(obj, value); | ||
return value; | ||
}; | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/check.ts | ||
function isNonEmpty(value) { | ||
return value.trim().length > 0; | ||
} | ||
function isStringRecord(obj) { | ||
@@ -31,3 +15,3 @@ return typeof obj === "object" && obj !== null && !Array.isArray(obj) && obj.constructor === Object; | ||
// https:/raw.githubusercontent.com/skymethod/denoflare/171f4cdb1266928d2ff67f8a851827ab51bae937/common/bytes.ts | ||
// https:/raw.githubusercontent.com/skymethod/denoflare/v0.5.12/common/bytes.ts | ||
var _Bytes = class { | ||
@@ -142,553 +126,5 @@ constructor(bytes) { | ||
// https:/deno.land/std@0.173.0/datetime/constants.ts | ||
var SECOND = 1e3; | ||
var MINUTE = SECOND * 60; | ||
var HOUR = MINUTE * 60; | ||
var DAY = HOUR * 24; | ||
var WEEK = DAY * 7; | ||
// https:/deno.land/std@0.220.1/encoding/_util.ts | ||
var encoder = new TextEncoder(); | ||
// https:/deno.land/std@0.173.0/datetime/_common.ts | ||
var Tokenizer = class { | ||
constructor(rules = []) { | ||
this.rules = rules; | ||
} | ||
addRule(test, fn) { | ||
this.rules.push({ test, fn }); | ||
return this; | ||
} | ||
tokenize(string, receiver = (token) => token) { | ||
function* generator(rules) { | ||
let index = 0; | ||
for (const rule of rules) { | ||
const result = rule.test(string); | ||
if (result) { | ||
const { value, length } = result; | ||
index += length; | ||
string = string.slice(length); | ||
const token = { ...rule.fn(value), index }; | ||
yield receiver(token); | ||
yield* generator(rules); | ||
} | ||
} | ||
} | ||
const tokenGenerator = generator(this.rules); | ||
const tokens = []; | ||
for (const token of tokenGenerator) { | ||
tokens.push(token); | ||
} | ||
if (string.length) { | ||
throw new Error(`parser error: string not fully parsed! ${string.slice(0, 25)}`); | ||
} | ||
return tokens; | ||
} | ||
}; | ||
function digits(value, count = 2) { | ||
return String(value).padStart(count, "0"); | ||
} | ||
function createLiteralTestFunction(value) { | ||
return (string) => { | ||
return string.startsWith(value) ? { value, length: value.length } : void 0; | ||
}; | ||
} | ||
function createMatchTestFunction(match) { | ||
return (string) => { | ||
const result = match.exec(string); | ||
if (result) | ||
return { value: result, length: result[0].length }; | ||
}; | ||
} | ||
var defaultRules = [ | ||
{ | ||
test: createLiteralTestFunction("yyyy"), | ||
fn: () => ({ type: "year", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("yy"), | ||
fn: () => ({ type: "year", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("MM"), | ||
fn: () => ({ type: "month", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("M"), | ||
fn: () => ({ type: "month", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("dd"), | ||
fn: () => ({ type: "day", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("d"), | ||
fn: () => ({ type: "day", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("HH"), | ||
fn: () => ({ type: "hour", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("H"), | ||
fn: () => ({ type: "hour", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("hh"), | ||
fn: () => ({ | ||
type: "hour", | ||
value: "2-digit", | ||
hour12: true | ||
}) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("h"), | ||
fn: () => ({ | ||
type: "hour", | ||
value: "numeric", | ||
hour12: true | ||
}) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("mm"), | ||
fn: () => ({ type: "minute", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("m"), | ||
fn: () => ({ type: "minute", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("ss"), | ||
fn: () => ({ type: "second", value: "2-digit" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("s"), | ||
fn: () => ({ type: "second", value: "numeric" }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("SSS"), | ||
fn: () => ({ type: "fractionalSecond", value: 3 }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("SS"), | ||
fn: () => ({ type: "fractionalSecond", value: 2 }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("S"), | ||
fn: () => ({ type: "fractionalSecond", value: 1 }) | ||
}, | ||
{ | ||
test: createLiteralTestFunction("a"), | ||
fn: (value) => ({ | ||
type: "dayPeriod", | ||
value | ||
}) | ||
}, | ||
{ | ||
test: createMatchTestFunction(/^(')(?<value>\\.|[^\']*)\1/), | ||
fn: (match) => ({ | ||
type: "literal", | ||
value: match.groups.value | ||
}) | ||
}, | ||
{ | ||
test: createMatchTestFunction(/^.+?\s*/), | ||
fn: (match) => ({ | ||
type: "literal", | ||
value: match[0] | ||
}) | ||
} | ||
]; | ||
var _format; | ||
var DateTimeFormatter = class { | ||
constructor(formatString, rules = defaultRules) { | ||
__privateAdd(this, _format, void 0); | ||
const tokenizer = new Tokenizer(rules); | ||
__privateSet(this, _format, tokenizer.tokenize(formatString, ({ type, value, hour12 }) => { | ||
const result = { | ||
type, | ||
value | ||
}; | ||
if (hour12) | ||
result.hour12 = hour12; | ||
return result; | ||
})); | ||
} | ||
format(date, options = {}) { | ||
let string = ""; | ||
const utc = options.timeZone === "UTC"; | ||
for (const token of __privateGet(this, _format)) { | ||
const type = token.type; | ||
switch (type) { | ||
case "year": { | ||
const value = utc ? date.getUTCFullYear() : date.getFullYear(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2).slice(-2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "month": { | ||
const value = (utc ? date.getUTCMonth() : date.getMonth()) + 1; | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "day": { | ||
const value = utc ? date.getUTCDate() : date.getDate(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "hour": { | ||
let value = utc ? date.getUTCHours() : date.getHours(); | ||
value -= token.hour12 && date.getHours() > 12 ? 12 : 0; | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "minute": { | ||
const value = utc ? date.getUTCMinutes() : date.getMinutes(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "second": { | ||
const value = utc ? date.getUTCSeconds() : date.getSeconds(); | ||
switch (token.value) { | ||
case "numeric": { | ||
string += value; | ||
break; | ||
} | ||
case "2-digit": { | ||
string += digits(value, 2); | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "fractionalSecond": { | ||
const value = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); | ||
string += digits(value, Number(token.value)); | ||
break; | ||
} | ||
case "timeZoneName": { | ||
break; | ||
} | ||
case "dayPeriod": { | ||
string += token.value ? date.getHours() >= 12 ? "PM" : "AM" : ""; | ||
break; | ||
} | ||
case "literal": { | ||
string += token.value; | ||
break; | ||
} | ||
default: | ||
throw Error(`FormatterError: { ${token.type} ${token.value} }`); | ||
} | ||
} | ||
return string; | ||
} | ||
parseToParts(string) { | ||
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q; | ||
const parts = []; | ||
for (const token of __privateGet(this, _format)) { | ||
const type = token.type; | ||
let value = ""; | ||
switch (token.type) { | ||
case "year": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_a = /^\d{1,4}/.exec(string)) == null ? void 0 : _a[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_b = /^\d{1,2}/.exec(string)) == null ? void 0 : _b[0]; | ||
break; | ||
} | ||
} | ||
break; | ||
} | ||
case "month": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_c = /^\d{1,2}/.exec(string)) == null ? void 0 : _c[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_d = /^\d{2}/.exec(string)) == null ? void 0 : _d[0]; | ||
break; | ||
} | ||
case "narrow": { | ||
value = (_e = /^[a-zA-Z]+/.exec(string)) == null ? void 0 : _e[0]; | ||
break; | ||
} | ||
case "short": { | ||
value = (_f = /^[a-zA-Z]+/.exec(string)) == null ? void 0 : _f[0]; | ||
break; | ||
} | ||
case "long": { | ||
value = (_g = /^[a-zA-Z]+/.exec(string)) == null ? void 0 : _g[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "day": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_h = /^\d{1,2}/.exec(string)) == null ? void 0 : _h[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_i = /^\d{2}/.exec(string)) == null ? void 0 : _i[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "hour": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_j = /^\d{1,2}/.exec(string)) == null ? void 0 : _j[0]; | ||
if (token.hour12 && parseInt(value) > 12) { | ||
console.error(`Trying to parse hour greater than 12. Use 'H' instead of 'h'.`); | ||
} | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_k = /^\d{2}/.exec(string)) == null ? void 0 : _k[0]; | ||
if (token.hour12 && parseInt(value) > 12) { | ||
console.error(`Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`); | ||
} | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "minute": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_l = /^\d{1,2}/.exec(string)) == null ? void 0 : _l[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_m = /^\d{2}/.exec(string)) == null ? void 0 : _m[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "second": { | ||
switch (token.value) { | ||
case "numeric": { | ||
value = (_n = /^\d{1,2}/.exec(string)) == null ? void 0 : _n[0]; | ||
break; | ||
} | ||
case "2-digit": { | ||
value = (_o = /^\d{2}/.exec(string)) == null ? void 0 : _o[0]; | ||
break; | ||
} | ||
default: | ||
throw Error(`ParserError: value "${token.value}" is not supported`); | ||
} | ||
break; | ||
} | ||
case "fractionalSecond": { | ||
value = (_p = new RegExp(`^\\d{${token.value}}`).exec(string)) == null ? void 0 : _p[0]; | ||
break; | ||
} | ||
case "timeZoneName": { | ||
value = token.value; | ||
break; | ||
} | ||
case "dayPeriod": { | ||
value = (_q = /^(A|P)M/.exec(string)) == null ? void 0 : _q[0]; | ||
break; | ||
} | ||
case "literal": { | ||
if (!string.startsWith(token.value)) { | ||
throw Error(`Literal "${token.value}" not found "${string.slice(0, 25)}"`); | ||
} | ||
value = token.value; | ||
break; | ||
} | ||
default: | ||
throw Error(`${token.type} ${token.value}`); | ||
} | ||
if (!value) { | ||
throw Error(`value not valid for token { ${type} ${value} } ${string.slice(0, 25)}`); | ||
} | ||
parts.push({ type, value }); | ||
string = string.slice(value.length); | ||
} | ||
if (string.length) { | ||
throw Error(`datetime string was not fully parsed! ${string.slice(0, 25)}`); | ||
} | ||
return parts; | ||
} | ||
sortDateTimeFormatPart(parts) { | ||
let result = []; | ||
const typeArray = [ | ||
"year", | ||
"month", | ||
"day", | ||
"hour", | ||
"minute", | ||
"second", | ||
"fractionalSecond" | ||
]; | ||
for (const type of typeArray) { | ||
const current = parts.findIndex((el) => el.type === type); | ||
if (current !== -1) { | ||
result = result.concat(parts.splice(current, 1)); | ||
} | ||
} | ||
result = result.concat(parts); | ||
return result; | ||
} | ||
partsToDate(parts) { | ||
const date = new Date(); | ||
const utc = parts.find((part) => part.type === "timeZoneName" && part.value === "UTC"); | ||
const dayPart = parts.find((part) => part.type === "day"); | ||
utc ? date.setUTCHours(0, 0, 0, 0) : date.setHours(0, 0, 0, 0); | ||
for (const part of parts) { | ||
switch (part.type) { | ||
case "year": { | ||
const value = Number(part.value.padStart(4, "20")); | ||
utc ? date.setUTCFullYear(value) : date.setFullYear(value); | ||
break; | ||
} | ||
case "month": { | ||
const value = Number(part.value) - 1; | ||
if (dayPart) { | ||
utc ? date.setUTCMonth(value, Number(dayPart.value)) : date.setMonth(value, Number(dayPart.value)); | ||
} else { | ||
utc ? date.setUTCMonth(value) : date.setMonth(value); | ||
} | ||
break; | ||
} | ||
case "day": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCDate(value) : date.setDate(value); | ||
break; | ||
} | ||
case "hour": { | ||
let value = Number(part.value); | ||
const dayPeriod = parts.find((part2) => part2.type === "dayPeriod"); | ||
if ((dayPeriod == null ? void 0 : dayPeriod.value) === "PM") | ||
value += 12; | ||
utc ? date.setUTCHours(value) : date.setHours(value); | ||
break; | ||
} | ||
case "minute": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCMinutes(value) : date.setMinutes(value); | ||
break; | ||
} | ||
case "second": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCSeconds(value) : date.setSeconds(value); | ||
break; | ||
} | ||
case "fractionalSecond": { | ||
const value = Number(part.value); | ||
utc ? date.setUTCMilliseconds(value) : date.setMilliseconds(value); | ||
break; | ||
} | ||
} | ||
} | ||
return date; | ||
} | ||
parse(string) { | ||
const parts = this.parseToParts(string); | ||
const sortParts = this.sortDateTimeFormatPart(parts); | ||
return this.partsToDate(sortParts); | ||
} | ||
}; | ||
_format = new WeakMap(); | ||
// https:/deno.land/std@0.173.0/datetime/to_imf.ts | ||
function toIMF(date) { | ||
function dtPad(v, lPad = 2) { | ||
return v.padStart(lPad, "0"); | ||
} | ||
const d = dtPad(date.getUTCDate().toString()); | ||
const h = dtPad(date.getUTCHours().toString()); | ||
const min = dtPad(date.getUTCMinutes().toString()); | ||
const s = dtPad(date.getUTCSeconds().toString()); | ||
const y = date.getUTCFullYear(); | ||
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; | ||
const months = [ | ||
"Jan", | ||
"Feb", | ||
"Mar", | ||
"Apr", | ||
"May", | ||
"Jun", | ||
"Jul", | ||
"Aug", | ||
"Sep", | ||
"Oct", | ||
"Nov", | ||
"Dec" | ||
]; | ||
return `${days[date.getUTCDay()]}, ${d} ${months[date.getUTCMonth()]} ${y} ${h}:${min}:${s} GMT`; | ||
} | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/crypto.ts | ||
@@ -699,3 +135,3 @@ async function computeHttpSignatureHeaders(opts) { | ||
const digest = body ? `SHA-256=${(await Bytes.ofUtf8(body).sha256()).base64()}` : void 0; | ||
const date = toIMF(new Date()); | ||
const date = new Date().toUTCString(); | ||
const signed = { | ||
@@ -741,2 +177,9 @@ "(request-target)": `${method.toLowerCase()} ${pathname}`, | ||
} | ||
function destructureThreadcapUrl(url) { | ||
const m = /^(at:\/\/)([^/]+)(\/.*?)$/.exec(url); | ||
const tmpUrl = m ? `${m[1]}${m[2].replaceAll(":", "%3A")}${m[3]}` : void 0; | ||
const { protocol, hostname: tmpHostname, pathname, searchParams } = new URL(tmpUrl != null ? tmpUrl : url); | ||
const hostname = tmpUrl ? tmpHostname.replaceAll("%3A", ":") : tmpHostname; | ||
return { protocol, hostname, pathname, searchParams }; | ||
} | ||
async function findOrFetchTextResponse(url, after, fetcher, cache, opts) { | ||
@@ -803,3 +246,3 @@ const existing = await cache.get(url, after); | ||
throw new Error(`Unexpected type for object: ${JSON.stringify(object)}`); | ||
if (!/^(Note|Article|Video|PodcastEpisode)$/.test(type)) | ||
if (!/^(Note|Article|Video|PodcastEpisode|Question)$/.test(type)) | ||
throw new Error(`Unexpected type: ${type}`); | ||
@@ -951,3 +394,4 @@ if (typeof id !== "string") | ||
throw new Error(`Expected 'published' to be a string, found ${JSON.stringify(published)}`); | ||
return { url, published, attachments, content, attributedTo, summary }; | ||
const questionOptions = computeQuestionOptions(object); | ||
return { url, published, attachments, content, attributedTo, summary, questionOptions }; | ||
} | ||
@@ -966,2 +410,25 @@ function computeUrl(url) { | ||
} | ||
function computeQuestionOptions(obj) { | ||
let rt; | ||
if (obj.type === "Question") { | ||
for (const prop of ["oneOf", "anyOf"]) { | ||
const val = obj[prop]; | ||
if (Array.isArray(val)) { | ||
for (const item of val) { | ||
if (isStringRecord(item) && item.type === "Note" && typeof item.name === "string") { | ||
if (!rt) | ||
rt = []; | ||
rt.push(item.name); | ||
} else { | ||
throw new Error(`Unsupported Question '${prop}' item: ${JSON.stringify(item)}`); | ||
} | ||
} | ||
return rt; | ||
} else if (val !== void 0) { | ||
throw new Error(`Unsupported Question '${prop}' value: ${JSON.stringify(val)}`); | ||
} | ||
} | ||
} | ||
return rt; | ||
} | ||
function computeAttributedTo(attributedTo) { | ||
@@ -1007,2 +474,4 @@ if (typeof attributedTo === "string") | ||
return { und: stringVal }; | ||
if (obj.type === "Video" && typeof obj.name === "string" && isNonEmpty(obj.name)) | ||
return { und: obj.name }; | ||
} | ||
@@ -1090,2 +559,125 @@ function computeAttachments(object) { | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/threadcap/threadcap_bluesky.ts | ||
var BlueskyProtocolImplementation = { | ||
async initThreadcap(url, opts) { | ||
const { uri, nodes, commenters } = await getThread(url, opts, 1e3); | ||
return { protocol: "bluesky", roots: [uri], nodes, commenters }; | ||
}, | ||
async fetchComment(id, opts) { | ||
const { uri, nodes } = await getThread(id, opts, 0); | ||
const node = nodes[uri]; | ||
if (!node) | ||
throw new Error(`fetchComment: no node!`); | ||
if (!node.comment) | ||
throw new Error(`fetchComment: no node comment!`); | ||
return node.comment; | ||
}, | ||
async fetchCommenter(attributedTo, opts) { | ||
const { updateTime, fetcher, cache, bearerToken } = opts; | ||
const res = await getProfile(attributedTo, { updateTime, fetcher, cache, bearerToken }); | ||
return computeCommenter2(res, updateTime); | ||
}, | ||
async fetchReplies(id, opts) { | ||
const { uri, nodes } = await getThread(id, opts, 1); | ||
const node = nodes[uri]; | ||
if (!node) | ||
throw new Error(`fetchReplies: no node!`); | ||
if (!node.replies) | ||
throw new Error(`fetchReplies: no node replies!`); | ||
return node.replies; | ||
} | ||
}; | ||
function makeUrl(url, queryParams) { | ||
const u = new URL(url); | ||
Object.entries(queryParams).forEach(([n, v]) => u.searchParams.set(n, v.toString())); | ||
return u.toString(); | ||
} | ||
function isGetPostThreadResponse(obj) { | ||
return isStringRecord(obj) && isThreadViewPost(obj.thread); | ||
} | ||
function isThreadViewPost(obj) { | ||
return isStringRecord(obj) && obj["$type"] === "app.bsky.feed.defs#threadViewPost" && isStringRecord(obj.post) && typeof obj.post.uri === "string" && isStringRecord(obj.post.author) && typeof obj.post.author.did === "string" && typeof obj.post.author.handle === "string" && (obj.post.author.displayName === void 0 || typeof obj.post.author.displayName === "string") && (obj.post.author.avatar === void 0 || typeof obj.post.author.avatar === "string") && Array.isArray(obj.post.author.labels) && isStringRecord(obj.post.record) && obj.post.record["$type"] === "app.bsky.feed.post" && typeof obj.post.record.text === "string" && (obj.post.replyCount === void 0 || typeof obj.post.replyCount === "number") && (obj.replies === void 0 || Array.isArray(obj.replies) && obj.replies.every(isThreadViewPost)); | ||
} | ||
function isGetProfileResponse(obj) { | ||
return isStringRecord(obj) && typeof obj.did === "string" && typeof obj.handle === "string" && typeof obj.displayName === "string" && (obj.avatar === void 0 || typeof obj.avatar === "string"); | ||
} | ||
async function fetchAppviewJson(url, { updateTime, fetcher, cache, bearerToken }) { | ||
return await findOrFetchJson(url, updateTime, fetcher, cache, { accept: "application/json", authorization: bearerToken ? `Bearer ${bearerToken}` : void 0 }); | ||
} | ||
async function getProfile(handleOrDid, { updateTime, fetcher, cache, bearerToken }) { | ||
const res = await fetchAppviewJson(makeUrl("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile", { actor: handleOrDid }), { updateTime, fetcher, cache, bearerToken }); | ||
if (!isGetProfileResponse(res)) | ||
throw new Error(JSON.stringify(res, void 0, 2)); | ||
return res; | ||
} | ||
async function getThread(url, opts, depth) { | ||
const { debug, fetcher, updateTime = new Date().toISOString(), cache, bearerToken } = opts; | ||
const { protocol, pathname } = destructureThreadcapUrl(url); | ||
const resolveDid = async (handleOrDid) => { | ||
if (handleOrDid.startsWith("did:")) | ||
return handleOrDid; | ||
const res2 = await getProfile(handleOrDid, { updateTime, fetcher, cache, bearerToken }); | ||
return res2.did; | ||
}; | ||
const atUri = await (async () => { | ||
var _a; | ||
if (protocol === "at:") | ||
return url; | ||
if (protocol === "https:") { | ||
const [_, handleOrDid, postId] = (_a = /^\/profile\/([^/]+)\/post\/([^/]+)$/.exec(pathname)) != null ? _a : []; | ||
if (handleOrDid && postId) | ||
return `at://${await resolveDid(handleOrDid)}/app.bsky.feed.post/${postId}`; | ||
} | ||
throw new Error(`Unexpected bluesky url: ${url}`); | ||
})(); | ||
const res = await fetchAppviewJson(makeUrl("https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread", { uri: atUri, depth, parentHeight: 0 }), { updateTime, fetcher, cache, bearerToken }); | ||
if (!isGetPostThreadResponse(res)) | ||
throw new Error(`Expected GetPostThreadResponse: ${JSON.stringify(res, void 0, 2)}`); | ||
if (debug) | ||
console.log(JSON.stringify(res, void 0, 2)); | ||
const nodes = {}; | ||
const commenters = {}; | ||
const processThread = (thread) => { | ||
const { uri: uri2, author, replyCount } = thread.post; | ||
let replies; | ||
let repliesAsof; | ||
if (replyCount === void 0) { | ||
if (thread.replies !== void 0) | ||
throw new Error(`Expected no thread.replies for undefined replyCount`); | ||
} else { | ||
if (thread.replies !== void 0) { | ||
replies = []; | ||
for (const reply of thread.replies) { | ||
const replyUri = processThread(reply); | ||
replies.push(replyUri); | ||
} | ||
repliesAsof = updateTime; | ||
} | ||
} | ||
nodes[uri2] = { | ||
replies, | ||
repliesAsof, | ||
comment: { | ||
attachments: [], | ||
content: { und: thread.post.record.text }, | ||
attributedTo: author.did | ||
}, | ||
commentAsof: updateTime | ||
}; | ||
commenters[author.did] = computeCommenter2(author, updateTime); | ||
return uri2; | ||
}; | ||
const uri = processThread(res.thread); | ||
return { uri, nodes, commenters }; | ||
} | ||
function computeCommenter2(author, updateTime) { | ||
var _a; | ||
return { | ||
asof: updateTime, | ||
name: (_a = author.displayName) != null ? _a : author.handle, | ||
fqUsername: author.handle, | ||
icon: author.avatar ? { url: author.avatar } : void 0 | ||
}; | ||
} | ||
// https:/raw.githubusercontent.com/skymethod/minipub/master/src/threadcap/threadcap_twitter.ts | ||
@@ -1254,10 +846,10 @@ var TwitterProtocolImplementation = { | ||
function isValidProtocol(protocol) { | ||
return protocol === "activitypub" || protocol === "twitter"; | ||
return protocol === "activitypub" || protocol === "twitter" || protocol === "bluesky"; | ||
} | ||
var MAX_LEVELS = 1e3; | ||
async function makeThreadcap(url, opts) { | ||
const { cache, userAgent, protocol, bearerToken, debug } = opts; | ||
const { cache, updateTime, userAgent, protocol, bearerToken, debug } = opts; | ||
const fetcher = makeFetcherWithUserAgent(opts.fetcher, userAgent); | ||
const implementation = computeProtocolImplementation(protocol); | ||
return await implementation.initThreadcap(url, { fetcher, cache, bearerToken, debug }); | ||
return await implementation.initThreadcap(url, { fetcher, cache, updateTime, bearerToken, debug }); | ||
} | ||
@@ -1409,2 +1001,4 @@ async function updateThreadcap(threadcap, opts) { | ||
return TwitterProtocolImplementation; | ||
if (protocol === "bluesky") | ||
return BlueskyProtocolImplementation; | ||
throw new Error(`Unsupported protocol: ${protocol}`); | ||
@@ -1420,8 +1014,12 @@ } | ||
const updateComment = !node.commentAsof || node.commentAsof < updateTime; | ||
if (updateComment) { | ||
const existingCommenter = node.comment ? threadcap.commenters[node.comment.attributedTo] : void 0; | ||
const updateCommenter = !existingCommenter || existingCommenter.asof < updateTime; | ||
if (updateComment || updateCommenter) { | ||
try { | ||
node.comment = await implementation.fetchComment(id, opts); | ||
if (updateComment) { | ||
node.comment = await implementation.fetchComment(id, opts); | ||
} | ||
const { attributedTo } = node.comment; | ||
const existingCommenter = threadcap.commenters[attributedTo]; | ||
if (!existingCommenter || existingCommenter.asof < updateTime) { | ||
const existingCommenter2 = threadcap.commenters[attributedTo]; | ||
if (!existingCommenter2 || existingCommenter2.asof < updateTime) { | ||
threadcap.commenters[attributedTo] = await implementation.fetchCommenter(attributedTo, opts); | ||
@@ -1428,0 +1026,0 @@ } |
@@ -40,4 +40,4 @@ /** | ||
export declare type Instant = string; | ||
/** Supported protocols for capturing comment threads: activitypub, twitter */ | ||
export declare type Protocol = 'activitypub' | 'twitter'; | ||
/** Supported protocols for capturing comment threads: activitypub, twitter, bluesky */ | ||
export declare type Protocol = 'activitypub' | 'twitter' | 'bluesky'; | ||
export declare function isValidProtocol(protocol: string): protocol is Protocol; | ||
@@ -115,2 +115,6 @@ /** | ||
readonly summary?: Record<string, string>; | ||
/** | ||
* Choices for polls, found in ActivityPub Question objects. | ||
*/ | ||
readonly questionOptions?: string[]; | ||
} | ||
@@ -260,2 +264,3 @@ /** Media attachments to a comment */ | ||
cache: Cache; | ||
updateTime?: Instant; | ||
protocol?: Protocol; | ||
@@ -262,0 +267,0 @@ bearerToken?: string; |
{ | ||
"name": "threadcap", | ||
"version": "0.1.11", | ||
"version": "0.1.12", | ||
"description": "Threadcap helps you take and update snapshots of a public ActivityPub comment thread, given a root post url.", | ||
@@ -5,0 +5,0 @@ "repository": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
124076
2470