diff --git a/.github/workflows/batch_close_issues.yml b/.github/workflows/batch_close_issues.yml new file mode 100644 index 0000000000..d8418e9a0c --- /dev/null +++ b/.github/workflows/batch_close_issues.yml @@ -0,0 +1,25 @@ +name: "Batch close stale issues" + +on: + # Monthly + schedule: + - cron: '0 0 1 * *' + # Manual trigger + workflow_dispatch: + inputs: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # Close everything older than ~6 months + days-before-issue-stale: 180 + days-before-issue-close: 0 + exempt-issue-labels: "do-not-autoclose,Meta request" + close-issue-message: "To have a more manageable issue backlog, we're closing older requests that weren't addressed since there's a low chance of them being addressed if they haven't already. If your request is still relevant, please [open a new request](https://github.com/rebelonion/Dantotsu/issues/new/choose)." + close-issue-reason: not_planned + ascending: true + operations-per-run: 250 diff --git a/.github/workflows/extension-issue-handling.yml b/.github/workflows/extension-issue-handling.yml index 2f043dc0aa..5c5c57ff5f 100644 --- a/.github/workflows/extension-issue-handling.yml +++ b/.github/workflows/extension-issue-handling.yml @@ -1,112 +1,180 @@ name: Extension Issue Handling on: issues: - types: [opened, labeled] + types: [opened] jobs: - handle-extension-issues: + check-extension-issue: runs-on: ubuntu-latest steps: + - name: Fetch Extension Data + id: fetch-extensions + uses: actions/github-script@v6 + with: + script: | + const repos = [ + "https://raw.githubusercontent.com/Dark25/aniyomi-extensions/repo/index.min.json", + "https://raw.githubusercontent.com/Kohi-den/extensions/main/index.min.json", + "https://raw.githubusercontent.com/keiyoushi/extensions/repo/index.min.json" + ]; + + const extensionNames = new Set(); + + for (const repo of repos) { + try { + const response = await fetch(repo); + const data = await response.json(); + data.forEach(extension => { + if (extension.name) { + extensionNames.add(extension.name.toLowerCase()); + } + }); + console.log(`āœ… Successfully fetched extensions from ${repo}`); + } catch (error) { + console.error(`āŒ Error fetching ${repo}:`, error); + } + } + + console.log('šŸ“ Found extensions:', Array.from(extensionNames).join(', ')); + core.setOutput('extension_names', Array.from(extensionNames).join(',')); + - name: Check Issue Content id: check-issue - env: - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_BODY: ${{ github.event.issue.body }} - run: | - # Regex patterns for extension-related issues - EXTENSION_REGEX_PATTERNS=( - # Extension not working (more flexible match) - ".*(\w+)\s*(extension)?\s*(not working|doesn't work|does not work|cant work|can't work).*" + uses: actions/github-script@v6 + with: + script: | + function normalizeExtensionName(name) { + const original = name; + const normalized = name + .toLowerCase() + .replace(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>/?]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + console.log(`šŸ” Normalizing: "${original}" -> "${normalized}"`); + return normalized; + } + + function findExtensionMatch(content, extensionNames) { + console.log('\nšŸ“– Checking content:', content); + for (const extension of extensionNames) { + const normalizedExtension = normalizeExtensionName(extension); + console.log(`\nāš” Checking against extension: ${extension}`); + + // Check if extension name appears as a complete word or phrase + if (content.toLowerCase().includes(` ${normalizedExtension} `) || + content.toLowerCase().startsWith(`${normalizedExtension} `) || + content.toLowerCase().endsWith(` ${normalizedExtension}`)) { + console.log(`āœ… Complete word/phrase match found! "${extension}"`); + return extension; + } + console.log(`āŒ No match found for "${extension}"`); + } + console.log('āŒ No extension match found in content'); + return null; + } + + function isExtensionRelatedIssue(title, body) { + const commonProblemWords = [ + 'doesnt', 'doesn\'t', 'not', 'error', 'broken', 'issue', 'problem', + 'crash', 'fail', 'failed', 'failing', 'down', 'work', 'working', + 'loading', 'load', 'dead', 'fix', 'broken', 'bug', 'bugs', 'problems', + 'issues', 'errors', 'cant', 'can\'t', 'cannot', 'wrong', 'help', + 'stuck', 'freezing', 'freeze', 'frozen', 'stopped', 'stopping', + 'blank', 'empty', 'missing', 'unavailable', 'slow' + ]; + + const content = (title + ' ' + body).toLowerCase(); + const foundWords = commonProblemWords.filter(word => content.includes(word)); + + if (foundWords.length > 0) { + console.log('āœ… Found problem-indicating words:', foundWords.join(', ')); + return true; + } + console.log('āŒ No problem-indicating words found'); + return false; + } + + const extensionNames = process.env.EXTENSION_NAMES.split(','); + const issueTitle = context.payload.issue.title; + const issueBody = context.payload.issue.body || ''; + + console.log('\nšŸŽÆ Checking Issue:'); + console.log('Title:', issueTitle); + console.log('Body:', issueBody); - # No extension available - ".*(no|can't find|cannot find|missing).*extension.*" + let isExtensionIssue = false; + let detectedExtension = null; - # No repo or repositories available - ".*(no|can't find|cannot find|missing).*repo(s)?\s*(available|found|accessible).*" + console.log('\nšŸ” Step 1: Checking for extension matches...'); + detectedExtension = findExtensionMatch(issueTitle, extensionNames) || + findExtensionMatch(issueBody, extensionNames); - # Specific server/stream issues - ".*(no streams|server).*(available|working).*" + console.log('\nšŸ” Step 2: Checking if issue is extension-related...'); + if (detectedExtension) { + isExtensionIssue = isExtensionRelatedIssue(issueTitle, issueBody); + console.log(`Extension found: ${detectedExtension}`); + console.log(`Is problem-related: ${isExtensionIssue}`); + } else { + console.log('āŒ No extension found, skipping problem check'); + } - # Variants of extension problems - ".*{.*}.*not working.*" - ".*{.*}.*extension.*(issue|problem).*" - ) - - # Convert to lowercase for case-insensitive matching - LOWER_TITLE=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]') - LOWER_BODY=$(echo "$ISSUE_BODY" | tr '[:upper:]' '[:lower:]') - - # Flag to track issue type - IS_EXTENSION_ISSUE=false - IS_NO_EXTENSION_ISSUE=false - - # Check title and body against regex patterns - for pattern in "${EXTENSION_REGEX_PATTERNS[@]}"; do - if [[ "$LOWER_TITLE" =~ $pattern ]] || [[ "$LOWER_BODY" =~ $pattern ]]; then - IS_EXTENSION_ISSUE=true - - # Special check for no extensions available - if [[ "$LOWER_TITLE" =~ "no extension" ]] || [[ "$LOWER_TITLE" =~ "can't find extension" ]]; then - IS_NO_EXTENSION_ISSUE=true - fi - - break - fi - done - - # Explicitly output boolean values - if [ "$IS_EXTENSION_ISSUE" = true ]; then - echo "is_extension_issue=true" >> $GITHUB_OUTPUT - else - echo "is_extension_issue=false" >> $GITHUB_OUTPUT - fi - - if [ "$IS_NO_EXTENSION_ISSUE" = true ]; then - echo "is_no_extension_issue=true" >> $GITHUB_OUTPUT - else - echo "is_no_extension_issue=false" >> $GITHUB_OUTPUT - fi + console.log('\nšŸ“Š Final Results:'); + console.log('Is Extension Issue:', isExtensionIssue); + console.log('Detected Extension:', detectedExtension || 'None'); + + core.setOutput('is_extension_issue', isExtensionIssue.toString()); + core.setOutput('detected_extension', detectedExtension || 'Unknown Extension'); + env: + EXTENSION_NAMES: ${{ steps.fetch-extensions.outputs.extension_names }} - name: Comment and Close Extension Issue if: steps.check-issue.outputs.is_extension_issue == 'true' uses: actions/github-script@v6 with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | const issueNumber = context.issue.number; + const reportedExtension = "${{ steps.check-issue.outputs.detected_extension || 'Unknown Extension' }}"; + console.log('šŸ”’ Closing issue:', issueNumber); + console.log('Extension reported:', reportedExtension); - // Check if it's a "No Extension" issue - if (${{ steps.check-issue.outputs.is_no_extension_issue }}) { - // DMCA notice message - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: "# Automated Message\n" + - "On 13 June 2024, the official Aniyomi repository got a DMCA notice and had to remove all of their extensions. Because of this, we will not be providing anyone with any links or extensions to avoid legal problems.\n" + - "# How to add repos?\n" + - "Although we do not give or maintain any repositories, we support adding custom repository links to your Dantotsu. \n" + - "Go to `Profile > Settings > Extensions` then paste your anime or manga links there.\n" + - "# How to find repos?\n" + - "It's very easy. Search on Google. But remember that the URL must end with index.min.json or else it won't work.\n" + - "`TLDR: We will not give repo links.`" - }); - } else { - // Standard extension issue message - await github.rest.issues.createComment({ + const currentLabels = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + }); + + for (const label of currentLabels.data) { + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, - body: `Dantotsu doesn't maintain extensions. - If the extension doesn't work we cannot help you. - Contact the owner of Respective Repo for extension-related problems` + name: label.name }); } - // Close the issue + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['wontfix'] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `# Not Our Business! + Dantotsu doesn't maintain extensions. + If the extension doesn't work, we cannot help you. + Contact the owner of the respective repository for extension-related problems.` + }); + await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, state: 'closed' }); + + console.log('āœ… Issue processed and closed successfully');