Skip to content

Commit b79807e

Browse files
authored
Merge pull request #124 from kaifcodec/feat/email-osint
refactor: reorganize username checks to prepare for email OSINT support
2 parents 898a0db + 2fdab0e commit b79807e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1694
-47
lines changed

.github/workflows/lint-pr.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Lint & Type Checks
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
lint:
11+
name: Lint & types (ruff + mypy)
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: [3.11]
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v4
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install linters
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install httpx colorama types-colorama
29+
pip install ruff mypy
30+
31+
- name: Run ruff
32+
run: ruff check .
33+
34+
- name: Run mypy
35+
run: mypy user_scanner
Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ name: Lint & Type Checks
22

33
on:
44
push:
5-
pull_request:
65
branches: [ main ]
76

87
permissions:
@@ -30,18 +29,8 @@ jobs:
3029
pip install httpx colorama types-colorama
3130
pip install ruff mypy
3231
33-
- name: Show installed linters
34-
run: |
35-
ruff --version || true
36-
mypy --version || true
37-
pip freeze | sed -n '1,200p'
38-
39-
- name: Run ruff (style/lint)
40-
run: |
41-
echo "==== Running ruff ===="
42-
ruff check . || { echo "Ruff failed"; exit 1; }
32+
- name: Run ruff
33+
run: ruff check .
4334

44-
- name: Run mypy (type checks)
45-
run: |
46-
echo "==== Running mypy ===="
47-
mypy user_scanner || { echo "Mypy found issues"; exit 1; }
35+
- name: Run mypy
36+
run: mypy user_scanner

.github/workflows/tests-pr.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Tests (pytest + coverage)
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
test:
11+
name: Test (pytest + coverage) — ${{ matrix.python-version }}
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: ["3.10"]
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Show Python info
27+
run: |
28+
python --version
29+
which python
30+
python -m pip --version
31+
32+
- name: Cache pip
33+
uses: actions/cache@v4
34+
with:
35+
path: ~/.cache/pip
36+
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml', '**/requirements*.txt') }}
37+
restore-keys: |
38+
${{ runner.os }}-pip-
39+
40+
- name: Install package & test deps
41+
run: |
42+
python -m pip install --upgrade pip
43+
pip install .
44+
pip install pytest pytest-cov
45+
46+
- name: Show installed packages
47+
run: pip freeze | sed -n '1,200p'
48+
49+
- name: Run pytest with coverage
50+
run: |
51+
mkdir -p test-output
52+
pytest -vv --maxfail=1 --disable-warnings \
53+
--junitxml=test-output/report.xml \
54+
--cov=user_scanner \
55+
--cov-report=xml:test-output/coverage.xml \
56+
--cov-report=term
57+
58+
- name: Upload test artifacts
59+
if: always()
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: test-reports-${{ matrix.python-version }}
63+
path: test-output
Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ name: Tests (pytest + coverage)
22

33
on:
44
push:
5-
pull_request:
65
branches: [ main ]
76

87
permissions:
@@ -15,6 +14,7 @@ jobs:
1514
strategy:
1615
matrix:
1716
python-version: ["3.10"]
17+
1818
steps:
1919
- name: Checkout
2020
uses: actions/checkout@v4
@@ -44,43 +44,21 @@ jobs:
4444
pip install .
4545
pip install pytest pytest-cov
4646
47-
- name: Show installed packages (top)
48-
run: |
49-
pip freeze | sed -n '1,200p'
47+
- name: Show installed packages
48+
run: pip freeze | sed -n '1,200p'
5049

51-
- name: Run pytest (verbose) with coverage and generate JUnit XML
50+
- name: Run pytest with coverage
5251
run: |
5352
mkdir -p test-output
5453
pytest -vv --maxfail=1 --disable-warnings \
5554
--junitxml=test-output/report.xml \
56-
--cov=user_scanner --cov-report=xml:test-output/coverage.xml --cov-report=term
57-
58-
- name: Show pytest junit xml (head)
59-
if: always()
60-
run: |
61-
echo "==== JUnit report (head) ===="
62-
sed -n '1,200p' test-output/report.xml || true
63-
echo "==== JUnit report (tail) ===="
64-
sed -n '200,400p' test-output/report.xml || true
55+
--cov=user_scanner \
56+
--cov-report=xml:test-output/coverage.xml \
57+
--cov-report=term
6558
66-
- name: Show coverage xml (head)
67-
if: always()
68-
run: |
69-
echo "==== Coverage XML (head) ===="
70-
sed -n '1,200p' test-output/coverage.xml || true
71-
72-
- name: Upload test & coverage artifacts
59+
- name: Upload test artifacts
7360
if: always()
7461
uses: actions/upload-artifact@v4
7562
with:
7663
name: test-reports-${{ matrix.python-version }}
7764
path: test-output
78-
79-
- name: (Optional) Upload coverage to Codecov
80-
if: success() && env.CODECOV_TOKEN != ''
81-
uses: codecov/codecov-action@v4
82-
with:
83-
files: test-output/coverage.xml
84-
token: ${{ secrets.CODECOV_TOKEN }}
85-
env:
86-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

user_scanner/cli/printer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def get_end(self, json_char: str = "]") -> str:
4747
return indentate(json_char, self.indent)
4848

4949
def get_result_output(self, result: Result) -> str:
50-
#In principle result should always have this
50+
# In principle result should always have this
5151
site_name = result.site_name
5252
username = result.username
5353

user_scanner/core/orchestrator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def load_modules(category_path: Path):
2727

2828

2929
def load_categories() -> Dict[str, Path]:
30-
root = Path(__file__).resolve().parent.parent # Should be user_scanner
30+
root = Path(__file__).resolve().parent.parent / "user_scan"
3131
categories = {}
3232

3333
for subfolder in root.iterdir():
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# community
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from user_scanner.core.orchestrator import status_validate
2+
3+
4+
def validate_coderlegion(user):
5+
url = f"https://coderlegion.com/user/{user}"
6+
7+
return status_validate(url, 404, 200, timeout=15.0)
8+
9+
10+
if __name__ == "__main__":
11+
user = input("Username?: ").strip()
12+
result = validate_coderlegion(user)
13+
14+
if result == 1:
15+
print("Available!")
16+
elif result == 0:
17+
print("Unavailable!")
18+
else:
19+
print("Error occured!")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import re
2+
from user_scanner.core.orchestrator import generic_validate, Result
3+
4+
5+
def validate_hackernews(user: str) -> Result:
6+
if not (2 <= len(user) <= 15):
7+
return Result.error("Length must be 2-15 characters")
8+
9+
if not re.match(r'^[a-zA-Z0-9_-]+$', user):
10+
return Result.error("Only use letters, numbers, underscores, and hyphens")
11+
12+
url = f"https://news.ycombinator.com/user?id={user}"
13+
14+
def process(response):
15+
if "No such user." in response.text:
16+
return Result.available()
17+
if f"profile: {user}" in response.text.lower() or "created:" in response.text:
18+
return Result.taken()
19+
return Result.error("Unexpected response structure")
20+
21+
return generic_validate(url, process, timeout=3.0, follow_redirects=True)
22+
23+
24+
if __name__ == "__main__":
25+
user = input("Username?: ").strip()
26+
result = validate_hackernews(user)
27+
28+
if result == 1:
29+
print("Available!")
30+
elif result == 0:
31+
print("Unavailable!")
32+
else:
33+
print(f"Error occurred! Reason: {result.get_reason()}")
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from user_scanner.core.orchestrator import generic_validate
2+
from user_scanner.core.result import Result
3+
4+
def validate_stackoverflow(user: str) -> Result:
5+
url = f"https://stackoverflow.com/users/filter?search={user}"
6+
7+
def process(response):
8+
if response.status_code == 200:
9+
text = response.text
10+
11+
if "No users matched your search." in text:
12+
return Result.available()
13+
14+
pattern = f'>{user}<'
15+
if pattern in text:
16+
return Result.taken()
17+
18+
return Result.available()
19+
20+
return Result.error("Unexpected status code from Stack Overflow")
21+
22+
return generic_validate(url, process)
23+
24+
25+
if __name__ == "__main__":
26+
user = input("Username?: ").strip()
27+
result = validate_stackoverflow(user)
28+
29+
if result == Result.available():
30+
print("Available!")
31+
elif result == Result.taken():
32+
print("Unavailable!")
33+
else:
34+
msg = result.get_reason()
35+
print("Error occurred!" + msg)

0 commit comments

Comments
 (0)