Skip to content

Whitelist Request Reminder #3804

Whitelist Request Reminder

Whitelist Request Reminder #3804

name: Whitelist Request Reminder
on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: whitelist-reminder
cancel-in-progress: false
jobs:
reminder:
runs-on: ubuntu-latest
steps:
- name: Process incomplete whitelist requests
uses: actions/github-script@v7
with:
script: |
const PING_INTERVAL_MS = 10 * 60 * 1000;
const MAX_PINGS = 3;
const botMarker = "<!-- dotns-whitelist-bot -->";
const invalidMarker = "<!-- dotns-whitelist-invalid-attempt -->";
const pingCountRegex = /<!-- dotns-whitelist-ping-count:(\d+) -->/;
const pingAtRegex = /<!-- dotns-whitelist-ping-at:([^>]+) -->/;
const missingRegex = /<!-- dotns-whitelist-missing:([^>]*) -->/;
const { owner, repo } = context.repo;
function extractField(body, name) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = (body || "").match(new RegExp(`### ${escaped}\\s*\\n([\\s\\S]*?)(?=\\n### |$)`));
return match ? match[1].trim() : "";
}
function findMissingFields(body) {
const address = extractField(body, "Address").split("\n")[0].trim();
const addressType = extractField(body, "Address Type").split("\n")[0].trim();
const network = extractField(body, "Network").split("\n")[0].trim();
const description = extractField(body, "Description").trim();
const missing = [];
if (!address) missing.push("Address");
if (!["EVM (H160)", "Substrate (SS58)"].includes(addressType)) missing.push("Address Type");
if (!["Paseo V2"].includes(network)) missing.push("Network");
if (description.length < 30) missing.push("Description (at least 30 characters explaining why the bypass is needed)");
return missing;
}
const allOpenRequests = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
labels: "whitelist-request",
per_page: 100,
});
for (const issue of allOpenRequests) {
if (issue.pull_request) continue;
const labels = (issue.labels || []).map(l => (typeof l === "string" ? l : l.name));
if (labels.includes("needs-info") || labels.includes("whitelist-rejected")) continue;
const missing = findMissingFields(issue.body);
if (missing.length === 0) continue;
const author = issue.user && issue.user.login;
const missingStr = missing.join(", ");
const lines = [
botMarker,
invalidMarker,
"<!-- dotns-whitelist-ping-count:1 -->",
`<!-- dotns-whitelist-ping-at:${new Date().toISOString()} -->`,
`<!-- dotns-whitelist-missing:${missingStr} -->`,
"### Whitelist Request Incomplete",
"",
`@${author} this whitelist request is missing required form fields.`,
"",
`**Needs:** ${missingStr}`,
"",
"Please update the issue body so the form fields are correct.",
"",
"**Reminder 1 of 3.** Two further reminders will follow at roughly 10 minute intervals. If the request is still incomplete after the final reminder it will be rejected automatically.",
];
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: lines.join("\n"),
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issue.number,
labels: ["needs-info"],
});
if (labels.includes("whitelist-approved")) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issue.number,
name: "whitelist-approved",
});
} catch (error) {
if (error.status !== 404) throw error;
}
}
}
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
labels: "whitelist-request,needs-info",
per_page: 100,
});
for (const issue of issues) {
if (issue.pull_request) continue;
const issue_number = issue.number;
const author = issue.user && issue.user.login;
if (!author) continue;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const pingComments = comments.filter(c =>
c.body &&
pingCountRegex.test(c.body) &&
c.user && c.user.type === "Bot"
);
if (pingComments.length === 0) continue;
const latest = pingComments[pingComments.length - 1];
const countMatch = latest.body.match(pingCountRegex);
const atMatch = latest.body.match(pingAtRegex);
const missingMatch = latest.body.match(missingRegex);
if (!countMatch || !atMatch) continue;
const count = Number(countMatch[1]);
const lastPingAt = new Date(atMatch[1]);
if (Number.isNaN(lastPingAt.getTime())) continue;
const missing = (missingMatch && missingMatch[1].trim()) || "the required form fields";
const responded = comments.some(c =>
c.user && c.user.login === author &&
new Date(c.created_at) > lastPingAt
);
if (responded) continue;
const elapsed = Date.now() - lastPingAt.getTime();
if (elapsed < PING_INTERVAL_MS) continue;
if (count < MAX_PINGS) {
const nextCount = count + 1;
const isFinal = nextCount === MAX_PINGS;
const lines = [
botMarker,
invalidMarker,
`<!-- dotns-whitelist-ping-count:${nextCount} -->`,
`<!-- dotns-whitelist-ping-at:${new Date().toISOString()} -->`,
`<!-- dotns-whitelist-missing:${missing} -->`,
`### Whitelist Request Reminder ${nextCount} of ${MAX_PINGS}`,
"",
`@${author} this whitelist request is still incomplete.`,
"",
`**Needs:** ${missing}`,
"",
isFinal
? "**Final reminder.** If this request is still incomplete by the next check the request will be rejected automatically."
: `Please update the issue body so the form fields are correct. ${MAX_PINGS - nextCount} further reminder${MAX_PINGS - nextCount === 1 ? "" : "s"} will follow before rejection.`,
];
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: lines.join("\n"),
});
} else {
const lines = [
botMarker,
"<!-- dotns-whitelist-rejected -->",
`<!-- dotns-whitelist-rejected-at:${new Date().toISOString()} -->`,
`<!-- dotns-whitelist-missing:${missing} -->`,
"### Whitelist Request Rejected",
"",
`@${author} after 3 reminders without an update from you, this whitelist request has been rejected automatically.`,
"",
`**Reason:** the request was still missing required form fields after the final reminder.`,
`**Missing at time of rejection:** ${missing}`,
"",
"The issue is being left open so the rejection reason stays visible. If you would like to whitelist this address you can open a new request with the complete form.",
];
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: lines.join("\n"),
});
for (const label of ["whitelist-request", "whitelist-approved", "needs-info"]) {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label });
} catch (error) {
if (error.status !== 404) throw error;
}
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: ["whitelist-rejected"],
});
}
}