mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 19:58:28 +01:00
90 lines
3.6 KiB
JavaScript
90 lines
3.6 KiB
JavaScript
import { join } from 'node:path'
|
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
|
|
import { jiraUrl, wikiUrl } from './source-urls.mjs'
|
|
|
|
// Robots.txt on these sites allows these paths and asks for one second between requests.
|
|
const crawlDelay = 1000
|
|
const defaultRetryAfter = 60
|
|
const [sitesPath, rawFolder] = process.argv.slice(2)
|
|
// Keeps a fetch to minutes.
|
|
const pagesPerListing = 20
|
|
const retries = 5
|
|
const sites = JSON.parse(readFileSync(sitesPath, 'utf8'))
|
|
const partialFolder = join(rawFolder, 'documents.partial')
|
|
rmSync(partialFolder, { force: true, recursive: true })
|
|
mkdirSync(partialFolder, { recursive: true })
|
|
|
|
async function getJson(url, retriesLeft = retries) {
|
|
await delay(crawlDelay)
|
|
const response = await fetch(url, { headers: { Accept: 'application/json', 'User-Agent': 'adf-codec-corpus-gathering (https://github.com/larvit/adf-codec)' } })
|
|
if (response.status === 429 && retriesLeft > 0) {
|
|
const asked = Number(response.headers.get('retry-after'))
|
|
await delay((asked > 0 ? asked : defaultRetryAfter) * 1000)
|
|
return getJson(url, retriesLeft - 1)
|
|
}
|
|
if (!response.ok) throw new Error(`${url} answered ${response.status}`)
|
|
return response.json()
|
|
}
|
|
|
|
let written = 0
|
|
|
|
function keep(name, source, document) {
|
|
if (document?.type !== 'doc' || !Array.isArray(document.content) || document.content.length === 0) return
|
|
writeFileSync(join(partialFolder, `${name}.json`), JSON.stringify({ document, retrieved: new Date().toISOString(), source }))
|
|
written += 1
|
|
}
|
|
|
|
async function fetchConfluence(site, collection) {
|
|
const base = `https://${site}.atlassian.net`
|
|
let next = `/wiki/api/v2/${collection}?limit=100&body-format=atlas_doc_format`
|
|
for (let page = 0; next !== undefined && page < pagesPerListing; page += 1) {
|
|
const answer = await getJson(`${base}${next}`)
|
|
for (const result of answer.results) {
|
|
const body = result.body?.atlas_doc_format?.value
|
|
if (body) keep(`${site}-wiki-${collection}-${result.id}`, wikiUrl(base, result._links.webui), JSON.parse(body))
|
|
}
|
|
next = answer._links?.next
|
|
}
|
|
}
|
|
|
|
async function fetchJira(site) {
|
|
const base = `https://${site}.atlassian.net`
|
|
let token
|
|
for (let page = 0; page < pagesPerListing; page += 1) {
|
|
const query = new URLSearchParams({ fields: 'comment,description', jql: 'created >= "2015-01-01" ORDER BY created DESC', maxResults: '100', ...(token ? { nextPageToken: token } : {}) })
|
|
const answer = await getJson(`${base}/rest/api/3/search/jql?${query}`)
|
|
for (const issue of answer.issues) {
|
|
keep(`${site}-jira-${issue.key}-description`, jiraUrl(base, issue.key), issue.fields.description)
|
|
for (const comment of issue.fields.comment?.comments ?? []) keep(`${site}-jira-${issue.key}-comment-${comment.id}`, jiraUrl(base, issue.key, comment.id), comment.body)
|
|
}
|
|
token = answer.nextPageToken
|
|
if (token === undefined) break
|
|
}
|
|
}
|
|
|
|
const failures = []
|
|
for (const { products, site } of sites) {
|
|
for (const product of Object.keys(products)) {
|
|
try {
|
|
if (product === 'wiki') {
|
|
await fetchConfluence(site, 'pages')
|
|
await fetchConfluence(site, 'footer-comments')
|
|
} else {
|
|
await fetchJira(site)
|
|
}
|
|
} catch (error) {
|
|
console.error(`${site} ${product}: ${error.message}`)
|
|
failures.push(`${site} ${product}`)
|
|
}
|
|
console.error(`${site} ${product}: ${written} documents so far`)
|
|
}
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`No corpus source is complete without: ${failures.join(', ')}. Fetch again, or take the site out of sites.json.`)
|
|
process.exit(1)
|
|
}
|
|
renameSync(partialFolder, join(rawFolder, 'documents'))
|