Skip to content

Commit df712f8

Browse files
committed
[#2258] antispam workflow
1 parent 0459ef4 commit df712f8

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

.github/workflows/antispam.yml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Anti-Spam Issue & PR Checker
2+
3+
on:
4+
workflow_dispatch: # manual testing only
5+
issues:
6+
types: [opened, reopened]
7+
pull_request:
8+
types: [opened, reopened]
9+
10+
jobs:
11+
check-spam:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/github-script@v7
16+
env:
17+
SHA: '${{env.parentSHA}}'
18+
with:
19+
script: |
20+
const script = require('.github/workflows/scripts/antispam.js')
21+
await script({github, context, core})

.github/workflows/scripts/antispam.js

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
const configuration = {
2+
label_if_suspicious: false,
3+
comment_if_suspicious: false,
4+
close_if_suspicious: false,
5+
suspicious_criteria_tolerated: 0
6+
};
7+
8+
async function make_information_report({ user }) {
9+
// we might also create a (pre-)report for spam to GH using the following informations:
10+
return `> [!WARNING] About the author:
11+
>
12+
> | information | value |
13+
> | ----------- | ----- |
14+
> | email | ${user.email} |
15+
> | login | ${user.login} |
16+
> | name | ${user.name} |
17+
> | location | ${user.location} |
18+
> | blog | ${user.blog} |
19+
> | location | ${user.location} |
20+
`
21+
}
22+
23+
async function when_suspicious({ github, context, failed_checks }){
24+
25+
// REFACTO: might wanna use a score of confidence (how suspicious it is), then react on that
26+
27+
const reasons = failed_checks.map(check => `> - ${check.reason}`).join("\n");
28+
const commentBody = `> [!WARNING] This issue/PR has been automatically flagged as [suspicious] as it might not meet contribution requirements.
29+
> Please read our contribution guide before submitting.
30+
>
31+
> Reason(s):
32+
>
33+
${reasons}
34+
`;
35+
36+
console.log("Body of the produced comment:\n", commentBody);
37+
38+
if (context.eventName === 'workflow_dispatch') // so we can test manually
39+
return;
40+
41+
const { owner, repo } = context.repo;
42+
const issueNumber = context.payload.number; // either issue or PR
43+
44+
if (configuration.comment_if_suspicious) {
45+
await github.rest.issues.createComment({
46+
owner,
47+
repo,
48+
issue_number: issueNumber,
49+
body: `${commentBody}`
50+
});
51+
}
52+
if (! configuration.label_if_suspicious) {
53+
await github.rest.issues.addLabels({
54+
owner,
55+
repo,
56+
issue_number: issueNumber,
57+
labels: ["suspicious"]
58+
});
59+
}
60+
if (configuration.close_if_suspicious) {
61+
await github.rest.issues.update({
62+
owner,
63+
repo,
64+
issue_number: issueNumber,
65+
state: "closed"
66+
});
67+
}
68+
}
69+
70+
class Check {
71+
constructor({ predicate, reason }) {
72+
this.predicate = predicate;
73+
this.reason = reason;
74+
}
75+
76+
async pass() {
77+
const result = await this.predicate();
78+
if (typeof result !== "boolean")
79+
console.error("Check: invalid argument: not a predicate");
80+
81+
console.debug("- check: ", (result ? "PASSED" : "FAILED"), " => ", this.reason)
82+
83+
return result;
84+
}
85+
}
86+
87+
module.exports = async ({ github, context, core }) => {
88+
89+
// const {SHA} = process.env; // for octokit.rest.repos.getCommit
90+
const username = context.actor;
91+
const { data: user } = await github.rest.users.getByUsername({ username: username });
92+
93+
const isAuthorOnlyContributionOnGH = await (async () => {
94+
// WARNING: Depending on the time of day, event latency can be anywhere from 30s to 6h. (source: https://octokit.github.io/rest.js/v21/)
95+
const { data: events } = await github.rest.activity.listEventsForAuthenticatedUser({
96+
username: username,
97+
per_page: 1
98+
});
99+
return events.length === 0;
100+
})();
101+
const WasAuthorRecentlyCreated = (() => {
102+
103+
const time_point = (() => {
104+
let value = new Date();
105+
value.setHours(value.getHours() - 2);
106+
return value;
107+
})();
108+
const create_at = new Date(user.created_at);
109+
return create_at >= time_point;
110+
})();
111+
112+
const isTitleOrBodyTooShort = (() => {
113+
114+
if (context.eventName === 'workflow_dispatch') // issues or pull_request
115+
return false;
116+
117+
const payload = context.payload;
118+
const title = payload.issue?.title || payload.pull_request?.title || "";
119+
const body = payload.issue?.body || payload.pull_request?.body || "";
120+
121+
const threshold = 20;
122+
return title.length < threshold
123+
|| body.length < threshold;
124+
})();
125+
126+
const checks = [
127+
new Check({
128+
predicate: () => false, // ! WasAuthorRecentlyCreated,
129+
reason: "Author account was recently created"
130+
}),
131+
new Check({
132+
predicate: () => ! isAuthorOnlyContributionOnGH,
133+
reason: "Author first contribution to any GitHub project"
134+
}),
135+
new Check({
136+
predicate: () => user.followers !== 0 && user.following !== 0,
137+
reason: "Author has no relationships"
138+
}),
139+
new Check({
140+
predicate: () => user.public_repos !== 0 && user.public_gists !== 0,
141+
reason: "Author has no public repo/gist"
142+
}),
143+
new Check({
144+
predicate: () => ! isTitleOrBodyTooShort,
145+
reason: "Issue/PR title or body too short"
146+
}),
147+
];
148+
149+
// IDEA: mandatory checks -> if any fails, then reject
150+
// for other checks
151+
// then use a weights/factors instead of booleans,
152+
// compute a confidence score to check against a threshold => if below, then reject
153+
154+
async function failedChecks(checks) {
155+
const results = await Promise.all(
156+
checks.map(async (check) => ({
157+
check,
158+
passed: await check.pass(),
159+
}))
160+
);
161+
return results
162+
.filter(({ passed }) => ! passed)
163+
.map(({ check }) => check);
164+
}
165+
166+
failedChecks(checks).then(failed_checks => {
167+
168+
console.log("Checks: ", {
169+
passed: checks.length - failed_checks.length,
170+
failed: failed_checks.length
171+
})
172+
173+
if (failed_checks.length <= configuration.suspicious_criteria_tolerated) {
174+
console.info("Not suspicious");
175+
return;
176+
}
177+
178+
when_suspicious({ github, context, failed_checks});
179+
180+
make_information_report({ user: user }).then(user_information_as_comment => {
181+
// do stuffs with user_information_as_comment
182+
console.log("user_information_as_comment", user_information_as_comment);
183+
});
184+
});
185+
};

0 commit comments

Comments
 (0)