Two processes, one NNTP connection each (the coordinator 40 -> 38): - spots:read (service spotter): free.pt's overviews after the last article seen, each spot's full header verified (lib/spotnet from voyager 1, as Spotweb), its description, website and NZB articles; the password its title or text names; delete/dispose messages applied. - spots:inspect (service spot-inspector): each spot's compressed NZB read, its files listed (spot_files), each matched to the header pass's release by the same release identity in one of its groups within 48 h (release_spots); unmatched spots matched again hourly, after 24 h a release of their own (source spot) with the spot's NZB. - release_passwords: per release and password, its source (nzb, spot-title, spot-text) and spot; the catalog keeps only passworded. XoverClient.head(); PROBE_CONNECTIONS default 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.9 KiB
TypeScript
84 lines
3.9 KiB
TypeScript
/**
|
|
* Archive passwords that spotters put in the title or description instead of
|
|
* the NZB's `<meta type="password">`:
|
|
* `Title{{secret}}` (the newznab/SABnzbd convention),
|
|
* `Wachtwoord: KKKK2`, `[b]Password[/b]: x`, `WW: p3IU5Rz9R`, `ww= 0112358`,
|
|
* a few words between keyword and separator (`Het wachtwoord voor de nzb is: dw0067`,
|
|
* `ww in nzb: X`, `Wachtwoord inbegrepen: wysiwyg`),
|
|
* the value on the next line (`WW:` / `tijdloos`),
|
|
* `wachtwoord is arn2`, `… voorzien van een wachtwoord. Deze is: X`,
|
|
* `WW FC0401` (abbreviation without separator: with a digit in it, or as the whole line).
|
|
* The value must be the rest of its line and a single token, so sentences
|
|
* such as "Wachtwoord: deze download is zonder wachtwoord geplaatst" or
|
|
* "met een password beschermd" don't count (LOG.md 2026-10-08 (10)).
|
|
*/
|
|
const KEYWORD = String.raw`(?:wachtwoord|wachtw|password|passwort|passwd|pwd|pass|ww|pw)(?![a-z])`
|
|
const WITH_SEPARATOR = new RegExp(String.raw`(?:^|[^a-z0-9])${KEYWORD}(?:\s+[a-zà-ÿ]+){0,3}?\s*(?:is\s*)?[:=]\s*(.*)$`, 'i')
|
|
const IS_VALUE = new RegExp(String.raw`(?:^|[^a-z0-9])${KEYWORD}\s+is\s+(\S+)\s*$`, 'i')
|
|
/** "Deze is voorzien van een wachtwoord. Deze is: X": the value in the next sentence. */
|
|
const NEXT_SENTENCE = /(?:^|[.!?]\s*)(?:deze|dit|het|die|this|it)\s+is\s*[:=]\s*(\S+)\s*$/i
|
|
const MENTIONS = new RegExp(String.raw`(?:^|[^a-z0-9])${KEYWORD}`, 'i')
|
|
const ABBREVIATION = /(?:^|[^a-z0-9])(?:ww|pw)\s+(\S+)\s*$/i
|
|
/** "WW carrie" as a line of its own; "WW II" is the war. */
|
|
const WHOLE_LINE_ABBREVIATION = /^(?:ww|pw)\s+(\S+)$/i
|
|
const NOT_PASSWORD_TOKEN = /^(?:i{1,3}|iv|v|[12])$/i
|
|
const NO_PASSWORD = /\b(zonder|geen|no|without|none|niet|kein|ohne)\b.{0,20}\b(wachtwoord|password|passwort|ww|pw)\b|\b(wachtwoord|password|passwort)\b.{0,20}\b(niet nodig|not needed|none|geen|nvt|n\/a)\b/i
|
|
const NOT_A_VALUE = /^(?:-+|geen|none|no|nee|nvt|n\/a|niet|not|leeg|empty|zonder|without|onbekend|unknown|zie|see|in|staat|volgt|nzb|spot)$/i
|
|
|
|
/** Lines of a spot description: UBB/HTML line breaks split, other tags dropped. */
|
|
function lines(description: string): string[] {
|
|
return description
|
|
.replace(/\[br\]|<br\s*\/?>/gi, '\n')
|
|
.replace(/\[\/?[a-z]+(?:=[^\]]*)?\]|<\/?[a-z][^>]*>/gi, '')
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
}
|
|
|
|
function clean(value: string): string | null {
|
|
const token = value
|
|
.trim()
|
|
// "Wachtwoord : yakowyke !!"
|
|
.replace(/^[!*~#]+\s*|\s*[!*~#]+$/g, '')
|
|
.replace(/^["'“‘«(<]+|["'”’»)>.,;!]+$/g, '')
|
|
if (!token || /\s/.test(token) || token.length < 2 || token.length > 64 || NOT_A_VALUE.test(token)) return null
|
|
return token
|
|
}
|
|
|
|
export function passwordFromSpot(title: string, description?: string | null): string | null {
|
|
const braces = /\{\{([^{}]{1,64})\}\}/.exec(title)
|
|
if (braces && clean(braces[1])) return clean(braces[1])
|
|
const all = lines(description ?? '')
|
|
for (const [i, line] of all.entries()) {
|
|
if (!line || NO_PASSWORD.test(line)) continue
|
|
const separated = WITH_SEPARATOR.exec(line)
|
|
if (separated) {
|
|
const value = separated[1].trim() ? clean(separated[1]) : nextLineValue(all, i)
|
|
if (value) return value
|
|
continue
|
|
}
|
|
const is = IS_VALUE.exec(line)
|
|
if (is) {
|
|
const value = clean(is[1])
|
|
if (value) return value
|
|
}
|
|
const sentence = MENTIONS.test(line) ? NEXT_SENTENCE.exec(line) : null
|
|
if (sentence) {
|
|
const value = clean(sentence[1])
|
|
if (value) return value
|
|
}
|
|
const whole = WHOLE_LINE_ABBREVIATION.exec(line)
|
|
const abbreviated = whole ?? ABBREVIATION.exec(line)
|
|
if (abbreviated && (whole ? !NOT_PASSWORD_TOKEN.test(abbreviated[1]) : /\d/.test(abbreviated[1]))) {
|
|
const value = clean(abbreviated[1])
|
|
if (value) return value
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** `WW:` with the value alone on the next non-empty line. */
|
|
function nextLineValue(all: string[], i: number): string | null {
|
|
const next = all.slice(i + 1).find((line) => line !== '')
|
|
return next ? clean(next) : null
|
|
}
|