From 786e88a56f1445b31f194920d832dd037bd2a2d1 Mon Sep 17 00:00:00 2001 From: radhitya Date: Mon, 22 Jun 2026 22:18:56 +0700 Subject: block control helper --- misc/linum-block-control.py | 328 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 misc/linum-block-control.py (limited to 'misc') diff --git a/misc/linum-block-control.py b/misc/linum-block-control.py new file mode 100644 index 0000000..8891f76 --- /dev/null +++ b/misc/linum-block-control.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 + +import sys +import urllib.request +import os + +categories = { + "ads": [ + ("EasyList", "https://easylist.to/easylist/easylist.txt"), + ( + "uBlock", + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt", + ), + ( + "AdGuard Base", + "https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_2_Base/filter.txt", + ), + ("Blocklist Project Ads", "https://blocklistproject.github.io/Lists/ads.txt"), + ( + "GoodbyeAds", + "https://raw.githubusercontent.com/jerryn70/GoodbyeAds/master/Formats/GoodbyeAds-AdBlock-Filter.txt", + ), + ], + "trackers": [ + ("EasyPrivacy", "https://easylist.to/easylist/easyprivacy.txt"), + ( + "AdGuard DNS", + "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", + ), + ( + "Blocklist Project Tracking", + "https://blocklistproject.github.io/Lists/tracking.txt", + ), + ("OISD big", "https://raw.githubusercontent.com/sjhgvr/oisd/main/oisd_big.txt"), + ], + "porn": [ + ( + "StevenBlack porn", + "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn-only/hosts", + ), + ("Blocklist Project Porn", "https://blocklistproject.github.io/Lists/porn.txt"), + ( + "OISD nsfw", + "https://raw.githubusercontent.com/sjhgvr/oisd/main/abp_nsfw.txt", + ), + ], + "gambling": [ + ( + "StevenBlack gambling", + "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-only/hosts", + ), + ( + "HaGeZi Gambling", + "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/gambling.txt", + ), + ( + "Blocklist Project Gambling", + "https://blocklistproject.github.io/Lists/gambling.txt", + ), + ], + "malware": [ + ( + "HaGeZi TIF", + "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/tif.txt", + ), + ( + "Blocklist Project Malware", + "https://blocklistproject.github.io/Lists/malware.txt", + ), + ( + "Blocklist Project Phishing", + "https://blocklistproject.github.io/Lists/phishing.txt", + ), + ], +} + +BLOCKLIST_DIR = "/etc/linum/blocklist" +CUSTOM_FILE = os.path.join(BLOCKLIST_DIR, "custom.txt") +WHITELIST_FILE = os.path.join(BLOCKLIST_DIR, "whitelist.txt") + + +def eprint(*a, **kw): + print(*a, file=sys.stderr, **kw) + + +def confirm(prompt): + return input(f"{prompt} [Y/n] ").strip().lower() in ("", "y", "yes") + + +def download_text(url): + try: + with urllib.request.urlopen(url, timeout=30) as resp: + text = resp.read().decode("utf-8", errors="replace") + return text.splitlines() + except Exception as e: + eprint(f"download failed {url}: {e}") + return None + + +def cmd_list(args): + if not args: + for k in categories: + print(k) + return + + key = args[0] + if key == "all": + for k, items in categories.items(): + print(f"\n[{k}]") + for name, url in items: + print(f"-> {name}") + print(f"-> {url}") + return + items = categories.get(key) + if not items: + eprint(f"'{key}' category not found") + return + for name, url in items: + print(f">> {name}") + print(f">> {url}") + + +def cmd_download(_args): + cat_keys = list(categories) + print("available:") + for i, k in enumerate(cat_keys, 1): + print(f">> {i}, {k}") + raw = input("choose one category: ") + sel_cats = [] + for part in raw.split(): + try: + idx = int(part) - 1 + if 0 <= idx < len(cat_keys): + sel_cats.append(cat_keys[idx]) + except ValueError: + eprint(f"skip '{part}' because it is not a number") + if not sel_cats: + eprint("there is no valid category") + return + + selected = [] + for cat in sel_cats: + items = categories[cat] + print(f"\nitem in [{cat}]:") + for i, (name, url) in enumerate(items, 1): + print(f">> {i}. {name}") + raw2 = input(f"choose one from '{cat}' (space or '*' for all): ") + if raw2.strip() == "*": + selected.extend((name, url, f"{cat}.txt") for name, url in items) + else: + for part in raw2.split(): + try: + idx = int(part) - 1 + if 0 <= idx < len(items): + name, url = items[idx] + selected.append((name, url, f"{cat}.txt")) + except ValueError: + pass + + if not selected: + eprint("nothing selected") + return + + print(f"\nit will download {len(selected)} item and merge to the blocklist") + if not confirm("continue?"): + print("cancel") + return + + file_buffers: dict[str, list[str]] = {} + for name, url, fname in selected: + eprint(f"download {name}...") + lines = download_text(url) + if lines is None: + continue + fpath = os.path.join(BLOCKLIST_DIR, fname) + existing = [] + if os.path.exists(fpath): + with open(fpath, "r") as f: + existing = f.read().splitlines() + all_lines = existing + lines + seen = set() + unique = [] + for ln in all_lines: + stripped = ln.strip() + if stripped and stripped not in seen: + seen.add(stripped) + unique.append(stripped) + file_buffers[fname] = unique + + for fname, lines in file_buffers.items(): + fpath = os.path.join(BLOCKLIST_DIR, fname) + old_count = len(open(fpath).read().splitlines()) if os.path.exists(fpath) else 0 + new_count = len(lines) + delta = new_count - old_count + eprint(f" {fname}: {old_count} to {new_count} lines ({delta:+d})") + + if not confirm("write the file?"): + print("cancel") + return + + for fname, lines in file_buffers.items(): + fpath = os.path.join(BLOCKLIST_DIR, fname) + with open(fpath, "w") as f: + f.write("\n".join(lines) + "\n") + print(f"{fname} — {len(lines)} lines") + + +def cmd_custom(args): + if not os.path.exists(CUSTOM_FILE): + open(CUSTOM_FILE, "w").close() + + with open(CUSTOM_FILE, "r") as f: + lines = f.read().splitlines() + + if not args or args[0] == "list": + if not lines: + print("(custom.txt is empty)") + for ln in lines: + print(ln) + return + + cmd = args[0] + if cmd == "add" and len(args) >= 3: + domain, ip = args[1], args[2] + lines.append(f"{ip} {domain}") + elif cmd == "del" and len(args) >= 2: + domain = args[1] + lines = [ln for ln in lines if not ln.endswith(f" {domain}")] + elif cmd == "set" and len(args) >= 3: + domain, new_ip = args[1], args[2] + for i, ln in enumerate(lines): + parts = ln.split() + if len(parts) == 2 and parts[1] == domain: + lines[i] = f"{new_ip} {domain}" + break + else: + eprint(f"domain '{domain}' not found in custom.txt") + return + else: + eprint("Usage: custom list|add |del |set ") + return + + with open(CUSTOM_FILE, "w") as f: + f.write("\n".join(lines) + "\n") + print(f"custom.txt — {len(lines)} lines") + + +def cmd_whitelist(args): + if not os.path.exists(WHITELIST_FILE): + open(WHITELIST_FILE, "w").close() + + with open(WHITELIST_FILE, "r") as f: + domains = f.read().splitlines() + + if not args or args[0] == "list": + if not domains: + print("(whitelist.txt is empty)") + for d in domains: + print(d) + return + + cmd = args[0] + if cmd == "add" and len(args) >= 2: + domain = args[1].strip() + if domain not in domains: + domains.append(domain) + elif cmd == "del" and len(args) >= 2: + domain = args[1].strip() + domains = [d for d in domains if d != domain] + else: + eprint("Usage: whitelist list|add |del ") + return + + with open(WHITELIST_FILE, "w") as f: + f.write("\n".join(domains) + "\n") + print(f"whitelist.txt — {len(domains)} domains") + + +def cmd_help(_args): + print("""Commands: + list [category|all] — list categories / items + download — pick and download blocklists + custom list|add|del|set — manage custom overrides + whitelist list|add|del — manage whitelist + help — this + exit — quit""") + + +def main(): + if not os.path.isdir(BLOCKLIST_DIR): + eprint(f"ERROR: {BLOCKLIST_DIR} does not exist. Run from repo root.") + sys.exit(1) + + cmds = { + "list": cmd_list, + "download": cmd_download, + "custom": cmd_custom, + "whitelist": cmd_whitelist, + "help": cmd_help, + } + + import subprocess + + subprocess.run(["clear"]) + + print("linum block control — type 'help' for commands") + + while True: + try: + raw = input("linum> ").strip() + except (EOFError, KeyboardInterrupt): + print() + break + if not raw: + continue + parts = raw.split() + cmd = parts[0] + if cmd == "exit": + break + fn = cmds.get(cmd) + if fn: + fn(parts[1:]) + else: + eprint(f"unknown command '{cmd}'. try: help") + + +if __name__ == "__main__": + main() -- cgit v1.2.3