WebSocket disconnects from zmeventserver after app is backgrounded and resumed #116
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Moderate first-time spam links | |
| # Redacts links posted by first-time contributors to any host outside a small | |
| # allowlist (GitHub, image/video hosts), then pings the maintainer. Allowlist | |
| # beats a blocklist here: an unknown throwaway host is flagged by default, so the | |
| # "here's a fix in a zip" malware pattern (issue #221) can't evade it with a new | |
| # domain or an extension-less link. See issue #222. | |
| on: | |
| issue_comment: | |
| types: [created, edited] | |
| issues: | |
| types: [opened, edited] | |
| permissions: | |
| issues: write | |
| jobs: | |
| moderate: | |
| runs-on: ubuntu-latest | |
| # Skip anything a bot posted, including this workflow's own edits/comments, | |
| # so it cannot loop on the edits it makes. | |
| if: github.event.sender.type != 'Bot' | |
| steps: | |
| - name: Redact first-time download links | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const REDACTION = '«link removed by spam moderation»'; | |
| const MAINTAINER = 'pliablepixels'; | |
| const UNTRUSTED = new Set(['FIRST_TIME_CONTRIBUTOR', 'FIRST_TIMER', 'NONE', 'MANNEQUIN']); | |
| // Hosts a first-time contributor may legitimately link. Suffix-matched, | |
| // so 'github.com' also covers gist.github.com, raw.githubusercontent.com, | |
| // etc. Deliberately excludes text-paste and file-locker hosts (pastebin, | |
| // 0x0.st, mega, dropbox, drive.google, ...): those are the payload | |
| // channel these bots use, and unknown hosts are flagged by default. | |
| const HOST_ALLOWLIST = [ | |
| 'github.com', 'githubusercontent.com', 'github.io', | |
| 'imgur.com', | |
| 'youtube.com', 'youtu.be', 'streamable.com', | |
| 'zoneminder.com', | |
| ]; | |
| const hostOf = (url) => { | |
| try { return new URL(url).hostname.toLowerCase().replace(/^www\./, ''); } | |
| catch { return ''; } | |
| }; | |
| const isAllowed = (host) => | |
| !!host && HOST_ALLOWLIST.some((h) => host === h || host.endsWith('.' + h)); | |
| // Any http(s) link whose host is not on the allowlist is flaggable. | |
| const findFlaggable = (body) => { | |
| if (!body || body.includes(REDACTION)) return []; | |
| const hits = []; | |
| const seen = new Set(); | |
| const md = /\[([^\]]*)\]\(\s*(https?:\/\/[^\s)]+)\s*\)/gi; | |
| let m; | |
| while ((m = md.exec(body)) !== null) { | |
| const [raw, label, url] = m; | |
| if (!isAllowed(hostOf(url))) { hits.push({ raw, url, label }); seen.add(url); } | |
| } | |
| const bare = /(https?:\/\/[^\s<>()\[\]]+)/gi; | |
| while ((m = bare.exec(body)) !== null) { | |
| const url = m[1].replace(/[)\].,'"]+$/, ''); | |
| if (seen.has(url)) continue; | |
| if (!isAllowed(hostOf(url))) hits.push({ raw: m[1], url, label: '' }); | |
| } | |
| return hits; | |
| }; | |
| const isComment = !!context.payload.comment; | |
| const target = isComment ? context.payload.comment : context.payload.issue; | |
| if (!target) { core.info('No comment/issue in payload.'); return; } | |
| const association = target.author_association || ''; | |
| if (!UNTRUSTED.has(association)) { | |
| core.info(`Skip: author_association=${association} is trusted.`); | |
| return; | |
| } | |
| const body = target.body || ''; | |
| let hits = findFlaggable(body); | |
| if (hits.length === 0) { core.info('No flaggable download links.'); return; } | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const issueNumber = context.payload.issue.number; | |
| const author = target.user.login; | |
| // A first-timer quoting or re-pasting a link a trusted user already | |
| // posted in this thread is not spam (issue #268): drop those hits. | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, repo, issue_number: issueNumber, per_page: 100, | |
| }); | |
| const trustedText = comments | |
| .filter((c) => c.user.type !== 'Bot' && !UNTRUSTED.has(c.author_association || '')) | |
| .map((c) => c.body || '') | |
| .concat(UNTRUSTED.has(context.payload.issue.author_association || '') ? [] : [context.payload.issue.body || '']) | |
| .join('\n'); | |
| hits = hits.filter((h) => !trustedText.includes(h.url)); | |
| if (hits.length === 0) { core.info('All flagged links already posted by trusted users.'); return; } | |
| let newBody = body; | |
| for (const h of hits) newBody = newBody.split(h.raw).join(REDACTION); | |
| if (isComment) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: target.id, body: newBody }); | |
| } else { | |
| await github.rest.issues.update({ owner, repo, issue_number: issueNumber, body: newBody }); | |
| } | |
| const where = isComment ? `comment (${target.html_url})` : 'issue body'; | |
| const list = hits | |
| .map((h) => `- \`${h.label || '(bare link)'}\` → ${hostOf(h.url) || h.url}`) | |
| .join('\n'); | |
| const historyOf = isComment ? "comment's" : "issue's"; | |
| const message = [ | |
| `Hi \`@${author}\`, links from new accounts are automatically held for review. The following off-site link was removed from your ${isComment ? 'comment' : 'issue'} for now; a maintainer will restore it shortly if it is legitimate:`, | |
| '', | |
| list, | |
| '', | |
| `Please do not download or run anything from links posted by new accounts until a maintainer confirms them.`, | |
| '', | |
| `@${MAINTAINER} first-time contributor (\`@${author}\`, \`${association}\`) posted an off-allowlist link in the ${where}. It stays in the ${historyOf} edit history, so restore it if this is a false positive; hide/delete and report the account if it is spam.`, | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body: message }); | |
| core.info(`Redacted ${hits.length} link(s) from ${where} by @${author}.`); |