lib/nntp (XOVER client, Xref, GROUP), lib/headers (subjects, file keys, segments from overview lines; the yEnc path stays behind for phase 2), lib/release (identity, random names, now in lib), lib/files (aggregator, volume names), lib/rules (posting tools, owner by priority, release verdicts), lib/chunks (release chunk codec). Their unit tests came along; new ones for codec, rules and volumes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
import { createHash } from 'node:crypto'
|
|
|
|
/**
|
|
* Upload signatures (CONCEPT.md D42): which tool posted an article, read from
|
|
* its Message-ID, and the group it was posted to first. Together they predict
|
|
* whether a random-named upload will ever get a name: ngPost uploads in
|
|
* a.b.ath almost never do, nyuu uploads in a.b.friends always do (LOG.md
|
|
* 2026-10-08 (5)).
|
|
*/
|
|
const TOOLS: [string, RegExp][] = [
|
|
// FmFwIaTpGgUiSfYcMwFlMeRo-1790637260478@nyuu: 12 random capital+small letter pairs, a ms timestamp
|
|
['nyuu', /^(?:[A-Z][a-z]){12}-\d{13}@nyuu$/],
|
|
['nyuu-host', /^(?:[A-Z][a-z]){12}-\d{13}@.+$/],
|
|
// b94268bc702040f7b811d5318f727e4f@ngPost: a UUID's hex
|
|
['ngPost', /^[0-9a-f]{32}@ngPost$/],
|
|
['yuun', /^[0-9a-f]{32}@[0-9a-f]{32}\.yuun$/],
|
|
['hex32', /^[0-9a-f]{32}@[A-Za-z0-9.-]+$/],
|
|
['JBinUp', /@JBin(?:Up|Down)\.local$/],
|
|
// part1of69.lOlWzvSHabJCBTVrs0ts@AdultPoster.local
|
|
['partNofM', /^part\d+of\d+\./i],
|
|
// 1790923856877$1729905757859$1@localhost
|
|
['ts-dollar', /^\d{13}\$\d+\$\d+@localhost$/],
|
|
// t3Y4mV92YCp0McxDc5@zLZlsw.RYqes: random local part and random host
|
|
['random', /^[A-Za-z0-9]{10,40}@[A-Za-z0-9]{3,12}\.[A-Za-z0-9]{2,6}$/],
|
|
]
|
|
|
|
/** The posting tool of a Message-ID (without angle brackets), or 'other'. */
|
|
export function uploadTool(messageId: string | null | undefined): string {
|
|
if (!messageId) return 'other'
|
|
for (const [tool, pattern] of TOOLS) if (pattern.test(messageId)) return tool
|
|
return 'other'
|
|
}
|
|
|
|
/** The group of a signature: the first the upload was posted to, as stored in `groups`. */
|
|
export function signatureGroup(groups: unknown): string {
|
|
return Array.isArray(groups) && typeof groups[0] === 'string' ? groups[0] : ''
|
|
}
|
|
|
|
/** The key of a signature in a Set. */
|
|
export function signatureKey(tool: string, group: string) {
|
|
return `${tool}\t${group}`
|
|
}
|
|
|
|
/**
|
|
* Whether an upload is in the share of dropped signatures that is still kept
|
|
* (and probed), so their rates keep being measured. Decided per release key,
|
|
* so a release is kept or dropped as a whole.
|
|
*/
|
|
export function explored(key: string, percent: number): boolean {
|
|
if (percent <= 0) return false
|
|
return createHash('md5').update(key).digest().readUInt32BE(0) % 10_000 < percent * 100
|
|
}
|