First Commit. Sauce only

This commit is contained in:
2026-04-01 22:51:51 +02:00
commit e3a383c9f5
2 changed files with 414 additions and 0 deletions

78
README.md Normal file
View File

@@ -0,0 +1,78 @@
# WAMEX - The wa.me Extractor —
This was the project that made me understand why AI is a pretty big deal. My idiot ass would have tackled the precise problem this app solves in the dumbest way possible: manual brute force. I had to migrate a group to a community without losing 400+ pending requests. I was gonna get those numbers manually because spam and bot filters and just the whole bitch-ass catastrophuck that is WhatsApp. Instead, because I knew that at least part of the process could be automated (even if I didn't know how) then it might be worth figuring some of this out.
It's not fancy, by any stretch of the imagination, but it doesn't need to be. you give it a mangled mess of text with phone numbers in there somewhere, it finds the numbers and structures them as wa.me links. If the phone number ain't on whatsapp, or they have specifically blocked the feature, then you're shit out of luck sweetheart. This isn't for that.
Anyway, it has a very very niche set of uses, but for community managers on whatsapp, or anybody looking to make a list of direct message links, this is a tool that might be of use. I make no guarantees about the safety of this software. I probably will never update it, unless I need it for something other than its current purpose.
It would be better to leave WhatsApp entirely in my opinion, but I will usually keep at least some semblance of a connection to the places where most people are.
Soon, my pretties... Soon we will be free of the scourge of Meta and the other Madmen that hath wrought this nonsense upon us.
Thanks for reading my not-TED-talk.
okay bye.
PS: this tool saved me weeks, and I built it with a bot in under half an hour with only the vaguest understanding of what I needed to acheive, and a reasonable grasp of context management and agentic workflows. I would never have even considered this a viable solution 18 months ago. Shit is wild, people.
## Setup
## Requirements
Python 3.10+ (uses `match` syntax / union types in hints)
### Install dependencies
```bash
pip install beautifulsoup4
```
`tkinter` ships with most Python installations on Linux.
If it's missing:
```bash
# Debian/Ubuntu
sudo apt install python3-tk
# Arch
sudo pacman -S tk
# Fedora
sudo dnf install python3-tkinter
```
---
## Run
```bash
python wa_extractor.py
```
---
## Workflow
1. In WhatsApp Web, open the pending requests panel.
2. Right-click the outer div containing all requests → **Inspect**.
3. In DevTools, right-click the `<div>` element → **Copy****Outer HTML**.
4. Paste into a `.txt` file (or directly into the left pane of the app).
5. Hit **⚡ extract**.
6. Adjust batch size if needed (default 20 — good for spam-safe manual invites).
7. **Save .txt** or **Copy all** to share with your community admins.
---
## Output format
```
https://wa.me/+27821234567
https://wa.me/+27831234567
... (20 links)
────────────────────────────────────────
https://wa.me/+27841234567
...
```
Each block = one admin's batch for the day.

336
wa_extractor.py Normal file
View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
wa_extractor.py — WhatsApp Pending Requests → wa.me Link Generator
Paste raw HTML from the pending requests div, extract all phone numbers,
output batched wa.me links ready for community admin invites.
"""
import re
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
from pathlib import Path
# ── Optional: use BeautifulSoup if available, else fall back to raw regex ──
try:
from bs4 import BeautifulSoup
HAS_BS4 = True
except ImportError:
HAS_BS4 = False
# ─────────────────────────────────────────────
# CORE EXTRACTION LOGIC
# ─────────────────────────────────────────────
# Matches international phone numbers: optional +, country code, digits/spaces/hyphens
# Handles formats like: +27821234567 +27 82 123 4567 +1-800-555-0199 0027821234567
PHONE_REGEX = re.compile(
r'(?<!\d)' # not preceded by a digit
r'(\+|00)' # starts with + or 00
r'([1-9]\d{0,3})' # country code (14 digits)
r'[\s\-\.]?' # optional separator
r'(\d[\d\s\-\.]{6,13}\d)' # subscriber number (714 digits with separators)
r'(?!\d)', # not followed by a digit
re.MULTILINE
)
def strip_non_digits(s: str) -> str:
return re.sub(r'\D', '', s)
def extract_numbers_from_html(html: str) -> list[str]:
"""Extract and normalise phone numbers from raw HTML."""
candidates = []
if HAS_BS4:
soup = BeautifulSoup(html, 'html.parser')
text = soup.get_text(separator=' ')
# Also check all attribute values (title, aria-label, data-*)
for tag in soup.find_all(True):
for attr_val in tag.attrs.values():
if isinstance(attr_val, str):
text += ' ' + attr_val
elif isinstance(attr_val, list):
text += ' ' + ' '.join(attr_val)
else:
# Strip HTML tags with regex if bs4 not available
text = re.sub(r'<[^>]+>', ' ', html)
seen = set()
for match in PHONE_REGEX.finditer(text):
raw = match.group(0)
digits = strip_non_digits(raw)
# Normalise 00XX → +XX
if digits.startswith('00'):
digits = digits[2:]
# Sanity check: 715 digits (E.164 range)
if 7 <= len(digits) <= 15:
key = digits
if key not in seen:
seen.add(key)
candidates.append('+' + digits)
return candidates
def format_output(numbers: list[str], batch_size: int = 20) -> str:
"""Format numbers as wa.me links, with a separator every batch_size entries."""
lines = []
for i, num in enumerate(numbers):
lines.append(f"https://wa.me/{num}")
if (i + 1) % batch_size == 0 and i + 1 < len(numbers):
lines.append('')
lines.append('' * 40)
lines.append('')
return '\n'.join(lines)
# ─────────────────────────────────────────────
# TKINTER UI
# ─────────────────────────────────────────────
BG = '#0e0e0e'
BG_PANEL = '#161616'
BG_INPUT = '#1a1a1a'
ACCENT = '#25d366' # WhatsApp green
ACCENT2 = '#128c7e'
TEXT = '#e8e8e8'
TEXT_DIM = '#666666'
BORDER = '#2a2a2a'
FONT_MONO = ('JetBrains Mono', 10) if True else ('Courier New', 10)
FONT_UI = ('JetBrains Mono', 10)
FONT_TITLE = ('JetBrains Mono', 15, 'bold')
FONT_SMALL = ('JetBrains Mono', 8)
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title('wa.me Extractor')
self.configure(bg=BG)
self.resizable(True, True)
self.minsize(740, 600)
self._html_path: Path | None = None
self._numbers: list[str] = []
self._build_ui()
self._center()
def _center(self):
self.update_idletasks()
w, h = 860, 680
sw = self.winfo_screenwidth()
sh = self.winfo_screenheight()
self.geometry(f'{w}x{h}+{(sw-w)//2}+{(sh-h)//2}')
# ── Layout ──────────────────────────────
def _build_ui(self):
self.columnconfigure(0, weight=1)
self.rowconfigure(2, weight=1)
# ── Header ──
hdr = tk.Frame(self, bg=BG, pady=18)
hdr.grid(row=0, column=0, sticky='ew', padx=28)
tk.Label(hdr, text='⬡ wa.me extractor', font=FONT_TITLE,
fg=ACCENT, bg=BG).pack(side='left')
self._bs4_badge(hdr)
# ── Controls row ──
ctrl = tk.Frame(self, bg=BG_PANEL, pady=14, padx=20)
ctrl.grid(row=1, column=0, sticky='ew', padx=0)
ctrl.columnconfigure(1, weight=1)
tk.Label(ctrl, text='HTML source:', font=FONT_UI,
fg=TEXT_DIM, bg=BG_PANEL).grid(row=0, column=0, sticky='w', padx=(0,10))
self._path_var = tk.StringVar(value='no file selected')
tk.Label(ctrl, textvariable=self._path_var, font=FONT_SMALL,
fg=TEXT_DIM, bg=BG_PANEL, anchor='w').grid(row=0, column=1, sticky='ew')
btn_open = self._btn(ctrl, '📂 open .txt', self._open_file)
btn_open.grid(row=0, column=2, padx=(12,6))
btn_run = self._btn(ctrl, '⚡ extract', self._run, primary=True)
btn_run.grid(row=0, column=3, padx=(0,0))
# Batch size
tk.Label(ctrl, text=' batch size:', font=FONT_UI,
fg=TEXT_DIM, bg=BG_PANEL).grid(row=0, column=4, padx=(18,6))
self._batch_var = tk.StringVar(value='20')
batch_spin = tk.Spinbox(ctrl, from_=5, to=50, increment=5,
textvariable=self._batch_var,
width=4, font=FONT_UI,
bg=BG_INPUT, fg=ACCENT, insertbackground=ACCENT,
buttonbackground=BG_INPUT, relief='flat',
highlightthickness=1, highlightcolor=BORDER,
highlightbackground=BORDER)
batch_spin.grid(row=0, column=5, padx=(0,2))
# ── Main panes ──
panes = tk.Frame(self, bg=BG)
panes.grid(row=2, column=0, sticky='nsew', padx=20, pady=(0,12))
panes.columnconfigure(0, weight=1)
panes.columnconfigure(2, weight=1)
panes.rowconfigure(1, weight=1)
# Left — raw HTML input
self._label(panes, 'paste html / or open file above', 0, 0)
self._html_box = self._text_area(panes, 1, 0)
# Divider
div = tk.Frame(panes, bg=BORDER, width=1)
div.grid(row=0, column=1, rowspan=2, sticky='ns', padx=10)
# Right — output
self._out_label_var = tk.StringVar(value='wa.me links')
self._label_var(panes, self._out_label_var, 0, 2)
self._out_box = self._text_area(panes, 1, 2, editable=False)
# ── Footer ──
foot = tk.Frame(self, bg=BG, pady=8)
foot.grid(row=3, column=0, sticky='ew', padx=20)
foot.columnconfigure(0, weight=1)
self._status_var = tk.StringVar(value='ready — open an HTML dump or paste below')
tk.Label(foot, textvariable=self._status_var, font=FONT_SMALL,
fg=TEXT_DIM, bg=BG, anchor='w').grid(row=0, column=0, sticky='w')
btn_copy = self._btn(foot, '⎘ copy all', self._copy_output)
btn_copy.grid(row=0, column=1, padx=(6,4))
btn_save = self._btn(foot, '💾 save .txt', self._save_output, primary=True)
btn_save.grid(row=0, column=2)
# ── Helpers ─────────────────────────────
def _btn(self, parent, text, cmd, primary=False):
fg = BG if primary else TEXT
bg = ACCENT if primary else BG_INPUT
abg = ACCENT2 if primary else BORDER
b = tk.Button(parent, text=text, command=cmd,
font=FONT_UI, fg=fg, bg=bg,
activeforeground=fg, activebackground=abg,
relief='flat', padx=12, pady=5,
cursor='hand2', bd=0)
return b
def _label(self, parent, text, row, col):
tk.Label(parent, text=text.upper(), font=FONT_SMALL,
fg=TEXT_DIM, bg=BG, anchor='w'
).grid(row=row, column=col, sticky='w', pady=(0,4))
def _label_var(self, parent, var, row, col):
tk.Label(parent, textvariable=var, font=FONT_SMALL,
fg=TEXT_DIM, bg=BG, anchor='w'
).grid(row=row, column=col, sticky='w', pady=(0,4))
def _text_area(self, parent, row, col, editable=True):
frame = tk.Frame(parent, bg=BORDER, padx=1, pady=1)
frame.grid(row=row, column=col, sticky='nsew')
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
t = scrolledtext.ScrolledText(
frame,
font=FONT_MONO,
bg=BG_INPUT, fg=TEXT,
insertbackground=ACCENT,
selectbackground=ACCENT2,
relief='flat', bd=0,
padx=10, pady=10,
wrap='none',
state='normal' if editable else 'disabled'
)
t.grid(row=0, column=0, sticky='nsew')
return t
def _bs4_badge(self, parent):
colour = ACCENT if HAS_BS4 else '#e05252'
label = 'bs4 ✓' if HAS_BS4 else 'bs4 ✗ (regex fallback)'
tk.Label(parent, text=label, font=FONT_SMALL,
fg=colour, bg=BG).pack(side='right', padx=4)
# ── Actions ─────────────────────────────
def _open_file(self):
path = filedialog.askopenfilename(
title='Select HTML dump (.txt)',
filetypes=[('Text files', '*.txt'), ('All files', '*.*')]
)
if not path:
return
self._html_path = Path(path)
self._path_var.set(str(self._html_path))
content = self._html_path.read_text(encoding='utf-8', errors='replace')
self._html_box.delete('1.0', tk.END)
self._html_box.insert('1.0', content)
self._status_var.set(f'loaded {len(content):,} chars — hit ⚡ extract')
def _run(self):
html = self._html_box.get('1.0', tk.END).strip()
if not html:
messagebox.showwarning('Nothing to parse',
'Paste HTML or open a .txt file first.')
return
try:
batch = int(self._batch_var.get())
except ValueError:
batch = 20
self._numbers = extract_numbers_from_html(html)
count = len(self._numbers)
if count == 0:
self._status_var.set('⚠ no phone numbers found — check your HTML')
messagebox.showinfo('No numbers found',
'Could not find any international phone numbers.\n'
'Make sure the HTML includes the +XX prefixed numbers.')
return
output = format_output(self._numbers, batch_size=batch)
batches = (count + batch - 1) // batch
self._out_box.config(state='normal')
self._out_box.delete('1.0', tk.END)
self._out_box.insert('1.0', output)
self._out_box.config(state='disabled')
self._out_label_var.set(f'wa.me links [{count} numbers · {batches} batches]')
self._status_var.set(
f'✓ extracted {count} numbers → {batches} batches of ≤{batch}'
)
def _copy_output(self):
text = self._out_box.get('1.0', tk.END).strip()
if not text:
self._status_var.set('nothing to copy yet')
return
self.clipboard_clear()
self.clipboard_append(text)
self._status_var.set('✓ copied to clipboard')
def _save_output(self):
text = self._out_box.get('1.0', tk.END).strip()
if not text:
messagebox.showwarning('Nothing to save', 'Run extraction first.')
return
path = filedialog.asksaveasfilename(
title='Save wa.me links',
defaultextension='.txt',
initialfile='wame_links.txt',
filetypes=[('Text files', '*.txt'), ('All files', '*.*')]
)
if not path:
return
Path(path).write_text(text, encoding='utf-8')
self._status_var.set(f'✓ saved → {path}')
# ─────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────
if __name__ == '__main__':
app = App()
app.mainloop()