Skip to content

Commit

Permalink
feat: scan inventory for badges
Browse files Browse the repository at this point in the history
closes #2
  • Loading branch information
exurd committed Feb 14, 2025
1 parent 0f5888b commit 93ef096
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 12 deletions.
27 changes: 20 additions & 7 deletions src/dbr/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from importlib import metadata

from dotenv import load_dotenv

from .modules import data_save

__prog__ = "Dumb Badge(s) Remover"
__prg__ = "DBR"
Expand Down Expand Up @@ -84,6 +84,11 @@ def get_parser() -> argparse.ArgumentParser:
parser.add_argument("--download-badge-spam-lists", action="store_true",
help="Download text files from exurd/badge-spam-lists; a bunch of text files containing place IDs from various Roblox badge chains.")

# related to inventory scanning
parser.add_argument("--check-inventory", "-c", type=int, default=None, metavar="USER_ID",
help="Checks a user's inventory for spam badges. DOES NOT DELETE BADGES. "
"Requires a list of place IDs to check (use download arguments above).")

# misc.
parser.add_argument("--cache-directory", "-cd", default=os.path.join(base_cache_path, "dbr_cache"),
help="The directory where cache data is kept.")
Expand Down Expand Up @@ -121,6 +126,9 @@ def main(args=None):

print(f"{__prog__} {__version__}\n{__copyright__}\n")

data_save.init(root_folder=args.cache_directory)

# if requested, download spam lists
if args.download_mgs_invalid_list:
from .modules.metagamerscore import download_mgs_invalid_games
download_mgs_invalid_games()
Expand All @@ -130,6 +138,17 @@ def main(args=None):
download_spam_lists()
sys.exit(0)

if args.check_inventory:
from .modules.spam_scanner import create_spam_list, convert_to_frozensets, create_folder, scan_inventory
if create_spam_list():
create_folder(args.check_inventory)
convert_to_frozensets()

badges, places = scan_inventory(args.check_inventory)
data_save.save_data(list(badges), f"spambadges_{str(args.check_inventory)}.json")
data_save.save_data(list(places), f"spamplaces_{str(args.check_inventory)}.json")
sys.exit(0)

user_agent = args.user_agent
rbx_token = args.rbx_token

Expand All @@ -140,26 +159,20 @@ def main(args=None):
if rbx_token == parser.get_default("RBX_TOKEN"):
if "RBX_TOKEN" in data and data["USER_AGENT"] != "":
rbx_token = data["RBX_TOKEN"]

if user_agent == parser.get_default("USER_AGENT"):
if "USER_AGENT" in data and data["USER_AGENT"] != "":
user_agent = data["USER_AGENT"]

from .modules import data_save

if all(val is None for val in [args.rbx_token, args.env_file]):
parser.error("the following arguments are required to continue: --rbx-token or --env-file containing `RBX_TOKEN=`")

if all(val is None for val in [args.file, args.user, args.group, args.place, args.badge, args.mgs_id]):
parser.error("the following arguments are required to continue: --file, --user, --group, --place, --badge or --mgs-id")

# if requested, download game list from mgs
from .modules import remover
if remover.init(user_agent, rbx_token) is False:
sys.exit(1)

data_save.init(root_folder=args.cache_directory)

if args.badge is not None:
remover.delete_badge(args.badge)
if args.mgs_id is not None:
Expand Down
14 changes: 10 additions & 4 deletions src/dbr/modules/data_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ def load_data(filename):
"""
Load data from a JSON file.
"""
data_file_path = os.path.join(DATA_FOLDER, filename)
print(data_file_path)
if DATA_FOLDER is None:
data_file_path = os.path.join(filename)
else:
data_file_path = os.path.join(DATA_FOLDER, filename)
if os.path.exists(data_file_path) and os.path.getsize(data_file_path) > 0:
with open(data_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
Expand All @@ -35,9 +37,13 @@ def save_data(data, filename):
"""
Saves data to a JSON file.
"""
data_file_path = os.path.join(DATA_FOLDER, filename)
if DATA_FOLDER is None:
data_file_path = os.path.join(filename)
else:
data_file_path = os.path.join(DATA_FOLDER, filename)

with open(data_file_path, "w", encoding="utf-8") as f:
json.dump(data, f)
json.dump(data, f, ensure_ascii=False, indent=4, sort_keys=True)
f.close()


Expand Down
2 changes: 1 addition & 1 deletion src/dbr/modules/get_request_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def get_request_url(url, requestSession=None, retry_amount=8, accept_forbidden=F
Internal function to request urls.
"""
if not isinstance(url, str):
print("getRequestURL: url was not string type, sending None")
print(f"get_request_url: url was not string type (instead got {type(url)}), sending None")
return None

if requestSession is None:
Expand Down
167 changes: 167 additions & 0 deletions src/dbr/modules/spam_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import time
import os
import re
import logging

from . import data_save
from .get_request_url import get_request_url
from .badge_spam_list import zstd_extract_lines

# i made a mistake while creating the spam lists...
# some lists don't have `www.` so pattern `(?:www\.)?`
# is needed to avoid issues
roblox_pattern = re.compile(r"https:\/\/(?:www\.)?roblox\.com\/games\/([0-9]+)")

PLACE_SPAM = {}
BADGE_SPAM = {}
FOUND_BADGES = set()
FOUND_PLACES = set()


def create_folder(user_id):
"""Creates folder for use in spam_scanner"""
folder = os.path.join(
data_save.DATA_FOLDER,
"scanresults_"
f"{str(user_id)}_"
f"{str(round(time.time()))}"
)
data_save.init(root_folder=folder)
os.makedirs((folder + "/badge_inventory"), exist_ok=True)
logging.basicConfig(filename=os.path.join(folder, "results.log"),
filemode="w",
format="%(asctime)s,%(msecs)d %(levelname)s %(message)s",
datefmt="%H:%M:%S",
level=logging.INFO
)



def create_spam_list(folder=os.getcwd()):
"""
Creates spam place ID list.
Uses the following sources (if they exist locally):
- mgs_invalid_games.json
- exurd/badge-spam-lists
Both are combined into one list of place IDs using
a dictionary and frozensets.
"""
global PLACE_SPAM
global BADGE_SPAM

# TODO: allow scan to be tracked back to it's roots (or multiple roots)...
print("Creating list of spam place IDs...")
try:
mgs_invalid_games_json = os.path.join(folder, "mgs_invalid_games.json")
if json := data_save.load_data(mgs_invalid_games_json):
l = []
[l.append(int(place_id)) for place_id in json["invalid_games"]]
PLACE_SPAM["mgs_invalid_games.json"] = l

badge_spam_list_folder = os.path.join(folder, "badge-spam-lists-main/")
for filename in os.listdir(badge_spam_list_folder):
if ".txt.zst" in filename:
path = os.path.join(badge_spam_list_folder, filename)
if lines := zstd_extract_lines(path):
l = []
[l.append(int(roblox_pattern.findall(line)[0])) for line in lines]
PLACE_SPAM[filename] = l

data_save.save_data(PLACE_SPAM, "spam_places_list.json")
except Exception as e:
print(f"Failed to create spam place list: {e}")
return False

print("Creating list of spam badge IDs...")
sb_txt = os.path.join(folder, "spam_badges.txt")
if os.path.exists(sb_txt):
try:
with open(sb_txt, mode="r") as f:
lines = f.readlines()
f.close()
l = []
[l.append(int(line.strip())) for line in lines]
BADGE_SPAM["spam_badges.txt"] = l

data_save.save_data(list(BADGE_SPAM), "spam_badges_list.json")
except Exception as e:
print(f"Failed to create spam place list: {e}")
return False

return True



def convert_to_frozensets():
"""
Converts lists in a dictonary to frozensets.
This allows for quicker lookup times.
"""
global PLACE_SPAM
global BADGE_SPAM

try:
for entry in PLACE_SPAM:
PLACE_SPAM[entry] = frozenset(PLACE_SPAM[entry])
return True
except Exception as e:
print(e)
return False


def scan_inventory(user_id:int, page_cursor=None, page_count=1) -> tuple[set, set]:
"""
Checks a user's inventory for spam badges. DOES NOT DELETE BADGES.
Returns a tuple of two sets: `FOUND_BADGES` and `FOUND_PLACES`.
"""
global FOUND_BADGES
global FOUND_PLACES


pageNum = page_count
if page_cursor == None:
url_request = f"https://badges.roblox.com/v1/users/{user_id}/badges?limit=100&sortOrder=Asc"
else:
url_request = f"https://badges.roblox.com/v1/users/{user_id}/badges?limit=100&cursor={page_cursor}&sortOrder=Asc"
while True:
print("\n---")
print(f"Next page ({str(pageNum)})...")
print("---\n")
req = get_request_url(url_request)

if req.ok:
request_json = req.json()
if 'errors' in request_json:
print("Error in uni_json! [", request_json, "]")
continue

for badge_entry in request_json['data']:
place_id = badge_entry['awarder']['id']
badge_id = badge_entry['id']
k = [key for key, s in PLACE_SPAM.items() if place_id in s]
if k:
logging.info(f"Badge: {badge_id} | Place: {place_id} (from: {', '.join(k)})")
FOUND_PLACES.add(place_id)

k = [key for key, s in BADGE_SPAM.items() if badge_id in s]
if k:
logging.info(f"Badge: {badge_id} | Place: {place_id} (from: {', '.join(k)})")
FOUND_BADGES.add(badge_id)

print(f"Found {len(FOUND_PLACES)} place(s), {len(FOUND_BADGES)} badge(s).")

# save badge inventory data
data_save.save_data(request_json, f"badge_inventory/{user_id}_{pageNum}.json")

if request_json['nextPageCursor'] is None:
print("Searched all badges.")
return (FOUND_BADGES, FOUND_PLACES)
else:
pageNum += 1
print("Checking next page of badges...")
url_request = f"https://badges.roblox.com/v1/users/{user_id}/badges?limit=100&cursor={request_json['nextPageCursor']}&sortOrder=Asc"
else:
print("Trying page", str(pageNum), "again...")
time.sleep(2)

0 comments on commit 93ef096

Please sign in to comment.