├── main.py ├── README.md ├── errors.py ├── models ├── items.py ├── config.py └── request.py ├── config.json ├── authenticator.py ├── sniper.py └── helpers.py /main.py: -------------------------------------------------------------------------------- 1 | from models import config 2 | 3 | import sniper 4 | import helpers 5 | import asyncio 6 | 7 | user_config = config.__init__() 8 | rolimon_limiteds = helpers.RolimonsDataScraper() 9 | 10 | asyncio.run(sniper.WatchLimiteds(user_config, rolimon_limiteds)()) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roblox-limited-sniper 2 | wow a free fast limited sniper? 3 | 4 | i know i have done this more than once whoops... but i get better each time so 😛 5 | 6 | did not really test this but i know it can buy limiteds 7 | 8 | no tutorial cus this is free 9 | 10 | tuto at hmmm 20 stars 11 | -------------------------------------------------------------------------------- /errors.py: -------------------------------------------------------------------------------- 1 | class InvalidCookie(Exception): pass 2 | 3 | class InvalidOtp(Exception): pass 4 | 5 | class InvalidChallangeType(Exception): pass 6 | 7 | class Request: 8 | class Failed(Exception): pass 9 | 10 | class InvalidStatus(Exception): pass 11 | 12 | class Config: 13 | class InvalidFormat(Exception): pass 14 | 15 | class MissingValues(Exception): pass 16 | 17 | class CantAccess(Exception): pass 18 | 19 | 20 | # hi xolo here 21 | # wanted to add a lil comment here °c° 22 | 23 | # class x: ... 24 | # looks much cooler but chatgpt say not good practise :angry: -------------------------------------------------------------------------------- /models/items.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Literal 3 | 4 | import uuid 5 | 6 | @dataclass 7 | class RolimonsData: 8 | rap: int 9 | value: int 10 | 11 | @dataclass 12 | class BuyData: 13 | collectible_item_id: str 14 | collectible_item_instance_id: str 15 | collectible_product_id: str 16 | 17 | expected_price: int 18 | expected_purchaser_id: str 19 | 20 | expected_seller_id: int = 1 21 | expected_purchaser_type: str = "User" 22 | expected_currency: int = 1 23 | expected_seller_type: Literal[None] = None 24 | 25 | idempotency_key: str = field(default_factory=lambda: str(uuid.uuid4())) 26 | 27 | @dataclass 28 | class Data: 29 | item_id: int 30 | product_id: int 31 | collectible_item_id: str 32 | 33 | lowest_resale_price: int 34 | 35 | @dataclass 36 | class Generic: 37 | item_id: int 38 | collectible_item_id: str -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "webhook": "https://discord.com/api/webhooks/1399803350039658618/", 3 | 4 | "account": { 5 | "otp_token": "", 6 | "cookie": "_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_CAEaAhAB." 7 | }, 8 | "buy_settings": { 9 | "generic_settings": { 10 | "min_percentage_off": 15, 11 | "min_robux_off": 100, 12 | "max_robux_cost": 1000, 13 | "price_measurer": "rap" 14 | }, 15 | "custom_settings": { 16 | "4771632715": { 17 | "min_percentage_off": 50, 18 | "min_robux_off": 0, 19 | "max_robux_cost": 10000, 20 | "price_measurer": "rap" 21 | } 22 | } 23 | }, 24 | "limiteds": [ 25 | [4771632715, "de8c9733-9f5b-42a9-b9f3-b75f581e2fbd"] 26 | ], 27 | "proxies": [ 28 | null 29 | ] 30 | } -------------------------------------------------------------------------------- /authenticator.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import time 3 | import json 4 | import base64 5 | import errors 6 | import base64 7 | import hashlib 8 | 9 | from models import request 10 | from typing import Union, Literal 11 | from dataclasses import dataclass 12 | 13 | @dataclass 14 | class ChallangeData: 15 | rblx_challange_id: str 16 | rblx_challange_metadata: str 17 | rblx_challange_type: Union[Literal["twostepverification"], str] 18 | 19 | class AutoPass: 20 | def __init__(self, secret: str): 21 | self.secret = secret 22 | 23 | @staticmethod 24 | def totp(secret: str) -> Union[str, errors.InvalidOtp]: 25 | try: 26 | # boy this took me long time googling 27 | interval = 30 28 | digits = 6 29 | digest = hashlib.sha1 30 | 31 | missing_padding = len(secret) % 8 32 | if missing_padding: 33 | secret += '=' * (8 - missing_padding) 34 | 35 | key = base64.b32decode(secret, casefold = True) 36 | counter = int(time.time() // interval) 37 | counter_bytes = counter.to_bytes(8, byteorder="big") 38 | hmac_hash = hmac.new(key, counter_bytes, digest).digest() 39 | 40 | offset = hmac_hash[-1] & 0x0F 41 | code = ( 42 | (hmac_hash[offset] & 0x7F) << 24 | 43 | (hmac_hash[offset + 1] & 0xFF) << 16 | 44 | (hmac_hash[offset + 2] & 0xFF) << 8 | 45 | (hmac_hash[offset + 3] & 0xFF) 46 | ) 47 | 48 | return str(code % (10 ** digits)).zfill(digits) 49 | except: 50 | raise errors.InvalidOtp() 51 | 52 | async def __call__(self, previous_request: "request.Request", challenge_data: ChallangeData) -> Union["request.Request", errors.InvalidOtp]: 53 | if challenge_data.rblx_challange_type != "twostepverification": 54 | raise errors.InvalidChallangeType("Change your security settings to use authenticator only") 55 | 56 | meta_data: dict 57 | meta_data = json.loads(base64.b64decode(challenge_data.rblx_challange_metadata).decode("utf-8")) 58 | 59 | response = request.Request( 60 | url = f"https://twostepverification.roblox.com/v1/users/{previous_request.user_id}/challenges/authenticator/verify", 61 | method = "post", 62 | headers = previous_request.headers, 63 | 64 | proxy = previous_request.proxy, 65 | session = previous_request.session, 66 | close_session = previous_request.close_session, 67 | json_data = { 68 | "challengeId": meta_data.get("challengeId"), 69 | "actionType": meta_data.get("actionType"), 70 | "code": self.totp(self.secret) 71 | } 72 | ) 73 | try: 74 | response = await response.send() 75 | except: 76 | raise errors.InvalidOtp() 77 | 78 | try: 79 | dumped_challange_meta_data = str(json.dumps({ 80 | "verificationToken": response.response_json.verificationToken, 81 | "rememberDevice": False, 82 | "challengeId": meta_data.get("challengeId"), 83 | "actionType": meta_data.get("actionType") 84 | })) 85 | response = await request.Request( 86 | url = "https://apis.roblox.com/challenge/v1/continue", 87 | method = "post", 88 | headers = previous_request.headers, 89 | 90 | proxy = previous_request.proxy, 91 | session = previous_request.session, 92 | close_session = previous_request.close_session, 93 | 94 | json_data = { 95 | "challengeId": challenge_data.rblx_challange_id, 96 | "challengeMetadata": dumped_challange_meta_data, 97 | "challengeType": challenge_data.rblx_challange_type 98 | } 99 | ).send() 100 | except: 101 | raise errors.InvalidOtp() 102 | 103 | if not previous_request.headers.raw_headers: 104 | previous_request.headers.raw_headers = {} 105 | 106 | previous_request.headers.raw_headers["x-csrf-token"] = previous_request.headers.x_csrf_token 107 | previous_request.headers.raw_headers["rblx-challenge-id"] = challenge_data.rblx_challange_id 108 | previous_request.headers.raw_headers["rblx-challenge-metadata"] = base64.b64encode(dumped_challange_meta_data.replace(" ", "").encode('utf-8')).decode('utf-8') 109 | previous_request.headers.raw_headers["rblx-challenge-type"] = challenge_data.rblx_challange_type 110 | 111 | return previous_request -------------------------------------------------------------------------------- /models/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import errors 3 | import helpers 4 | import asyncio 5 | import authenticator 6 | 7 | from dataclasses import dataclass, field 8 | from typing import Optional, Dict, List, Union, Literal 9 | from models import request, items 10 | 11 | @dataclass 12 | class Account: 13 | cookie: str 14 | otp_token: str 15 | 16 | x_csrf_token: helpers.XCsrfTokenWaiter 17 | 18 | user_id: str = field(init = None) 19 | user_name: str = field(init = None) 20 | 21 | 22 | def __post_init__(self) -> Union[None, errors.InvalidCookie]: 23 | try: 24 | headers = request.Headers(cookies = {".ROBLOSECURITY": self.cookie}) 25 | 26 | response = asyncio.run( 27 | request.Request( 28 | url = "https://users.roblox.com/v1/users/authenticated", 29 | method = "get", 30 | headers = headers 31 | ).send() 32 | ) 33 | 34 | self.cookie = asyncio.run(helpers.UnlockCookie(self.cookie)()) 35 | self.user_id = response.response_json.user_id 36 | self.user_name = response.response_json.user_name 37 | self.x_csrf_token = helpers.XCsrfTokenWaiter(cookie = self.cookie, on_start = True) 38 | except errors.Request.Failed as reason: 39 | raise errors.InvalidCookie(reason) 40 | 41 | @dataclass 42 | class ItemSettings: 43 | min_percentage_off: int 44 | min_robux_off: Optional[int] = None 45 | max_robux_cost: Optional[int] = None 46 | price_measurer: Optional[Literal["value", "rap", "value_rap"]] = "rap" 47 | 48 | @dataclass 49 | class BuySettings: 50 | generic_settings: ItemSettings 51 | custom_settings: Optional[Dict[str, ItemSettings]] = None 52 | 53 | @dataclass 54 | class Settings: 55 | webhook: Union[None, str] 56 | account: Account 57 | buy_settings: BuySettings 58 | limiteds: helpers.Iterator 59 | proxies: List[str] 60 | 61 | 62 | def create_item_settings(data): 63 | return ItemSettings( 64 | min_percentage_off=data["min_percentage_off"], 65 | min_robux_off=data.get("min_robux_off"), 66 | max_robux_cost=data.get("max_robux_cost"), 67 | price_measurer=data.get("price_measurer") 68 | ) 69 | 70 | def __init__() -> Union[Settings, errors.Config.CantAccess, errors.Config.InvalidFormat, errors.Config.MissingValues]: 71 | try: 72 | config = open("config.json", "r") 73 | try: 74 | file_json = json.loads(config.read()) 75 | except Exception as reason: 76 | raise errors.InvalidConfigFormat(reason) 77 | except Exception as reason: 78 | raise errors.CantAccessConfig(reason) 79 | 80 | try: 81 | account = Account( 82 | cookie = file_json["account"]["cookie"], 83 | otp_token = authenticator.AutoPass(file_json["account"]["otp_token"]), 84 | x_csrf_token = helpers.XCsrfTokenWaiter(cookie = file_json["account"]["cookie"], proxy = None, on_start = True) 85 | ) 86 | 87 | buy_settings_data = file_json["buy_settings"] 88 | buy_settings_generic_data = buy_settings_data["generic_settings"] 89 | 90 | if buy_settings_generic_data.get("price_measurer") not in ("rap", "value", "value_rap", None): 91 | raise errors.Config.InvalidFormat(f"Accepted price_measurers (value, rap, value_rap, None). Received: {buy_settings_generic_data['price_measurer']}") 92 | 93 | generic_settings = create_item_settings(buy_settings_generic_data) 94 | 95 | buy_settings_custom_data = buy_settings_data.get("custom_settings") 96 | 97 | if buy_settings_custom_data: 98 | custom_settings = {} 99 | 100 | for item_id, data in buy_settings_custom_data.items(): 101 | if data.get("price_measurer") not in ("rap", "value", "value_rap", None): 102 | raise errors.Config.InvalidFormat(f"Accepted price_measurers (value, rap, value_rap, None). Received: {data['price_measurer']}") 103 | 104 | custom_item_settings = create_item_settings(data) 105 | 106 | custom_settings[item_id] = custom_item_settings 107 | 108 | 109 | buy_settings = BuySettings( 110 | generic_settings = generic_settings, 111 | custom_settings = custom_settings 112 | 113 | ) 114 | 115 | limiteds = file_json["limiteds"] 116 | if not limiteds: 117 | raise errors.Config.MissingValues("Limiteds list can not be empty") 118 | for limited in limiteds: 119 | if len(limited) != 2: 120 | raise errors.Config.MissingValues("Limited needs both item id and collectible item id") 121 | 122 | proxies = file_json["proxies"] 123 | if not proxies: 124 | raise errors.Config.MissingValues("Proxy list can not be empty") 125 | 126 | settings = Settings( 127 | webhook = file_json.get("webhook"), 128 | account = account, 129 | buy_settings = buy_settings, 130 | limiteds = helpers.Iterator(data = [items.Generic(item_id = limited[0], collectible_item_id = limited[1]) 131 | for limited in limiteds]), 132 | proxies = proxies 133 | ) 134 | 135 | return settings 136 | 137 | except KeyError as reason: 138 | raise errors.Config.MissingValues(reason) 139 | -------------------------------------------------------------------------------- /sniper.py: -------------------------------------------------------------------------------- 1 | from models import items, config, request 2 | from typing import Union, Tuple, Optional, List, Dict 3 | 4 | import errors 5 | import helpers 6 | import asyncio 7 | 8 | class BuyLimited: 9 | def __init__(self, user_data: config.Account, buy_data: items.BuyData) -> None: 10 | self.user_data = user_data 11 | self.buy_data = buy_data 12 | 13 | 14 | async def __call__(self) -> Union[bool, Tuple[bool, request.ResponseJsons.BuyResponse]]: 15 | try: 16 | url = f"https://apis.roblox.com/marketplace-sales/v1/item/{self.buy_data.collectible_item_id}/purchase-resale" 17 | response: request.Response 18 | response = await request.Request( 19 | url = url, 20 | method = "post", 21 | headers = request.Headers( 22 | x_csrf_token = await self.user_data.x_csrf_token(), 23 | cookies = {".ROBLOSECURITY": self.user_data.cookie} 24 | ), 25 | json_data = request.RequestJsons.jsonify_api_broad(url, self.buy_data), 26 | otp_token = self.user_data.otp_token, 27 | close_session = False, 28 | user_id = self.user_data.user_id 29 | ).send() 30 | 31 | if response.response_json.purchased: 32 | return True, response.response_json 33 | else: 34 | return False, response.response_json 35 | 36 | except errors.Request.Failed: 37 | return False 38 | 39 | class WatchLimiteds: 40 | def __init__(self, config: config.Settings, rolimon_limiteds: helpers.RolimonsDataScraper) -> None: 41 | self.webhook = config.webhook 42 | 43 | self.account = config.account 44 | 45 | self.generic_settings = config.buy_settings.generic_settings 46 | self.custom_settings = config.buy_settings.custom_settings 47 | self.limiteds = config.limiteds 48 | self.rolimon_limiteds = rolimon_limiteds 49 | self.proxies = config.proxies 50 | self.ui_manager = helpers.UIManager(total_proxies = len(config.proxies)) 51 | self.requests = 0 52 | 53 | async def __call__(self): 54 | threads = [ 55 | ProxyThread(self, proxy).watch() # self is the own obj for shared vars 56 | for proxy in self.proxies 57 | ] 58 | 59 | await asyncio.gather(*threads, helpers.run_ui(ui_manager = self.ui_manager)) 60 | 61 | class ProxyThread(helpers.CombinedAttribute): 62 | webhook: str 63 | account: config.Account 64 | generic_settings: config.ItemSettings 65 | custom_settings: Union[None, Dict[str, config.ItemSettings]] 66 | limiteds: helpers.Iterator 67 | rolimon_limiteds: helpers.RolimonsDataScraper 68 | ui_manager: helpers.UIManager 69 | requests: int 70 | 71 | def __init__(self, watch_limiteds: WatchLimiteds, proxy: str): 72 | super().__init__(watch_limiteds) 73 | 74 | self._proxy = proxy 75 | 76 | def check_if_item_elligable(self, item_data: items.Data, item_value_rap: items.RolimonsData) -> bool: 77 | # checking if custom config is avaible. Using custom config if avaible else generic 78 | item_buy_config: config.ItemSettings 79 | item_buy_config = self.generic_settings if not str(item_data.item_id) in self.custom_settings else self.custom_settings[str(item_data.item_id)] 80 | if item_buy_config.price_measurer == "value": 81 | if item_value_rap.value: 82 | base_value_item = item_value_rap.value 83 | else: 84 | return False 85 | elif item_buy_config.price_measurer == "rap": 86 | base_value_item = item_value_rap.rap 87 | elif item_buy_config.price_measurer == "value_rap": 88 | # use value if avaible else rap 89 | base_value_item = item_value_rap.value if item_value_rap.value else item_value_rap.rap 90 | 91 | if item_buy_config.min_percentage_off and not (base_value_item * (item_buy_config.min_percentage_off / 100)) > item_data.lowest_resale_price: 92 | return False 93 | if item_buy_config.min_robux_off and not base_value_item - item_data.lowest_resale_price > item_buy_config.min_robux_off: 94 | return False 95 | if item_buy_config.max_robux_cost and not item_data.lowest_resale_price >= item_buy_config.max_robux_cost: 96 | return False 97 | 98 | return True 99 | 100 | @staticmethod 101 | async def get_resale_data(item: items.Data) -> Union[request.ResponseJsons.ResaleResponse, errors.Request.Failed]: 102 | response = await request.Request( 103 | url = f"https://apis.roblox.com/marketplace-sales/v1/item/{item.collectible_item_id}/resellers?limit=1", 104 | method = "get", 105 | retries = 5 106 | ).send() 107 | return response.response_json 108 | 109 | async def handle_response(self, item_list: request.ResponseJsons.ItemDetails): 110 | rolimons_limited = self.rolimon_limiteds 111 | for item in item_list.items: 112 | if str(item.item_id) in await rolimons_limited(): 113 | if self.check_if_item_elligable(item, (await rolimons_limited())[str(item.item_id)]): 114 | resale_data: request.ResponseJsons.ResaleResponse 115 | resale_data = await self.get_resale_data(item) 116 | item.lowest_resale_price = resale_data.price 117 | if self.check_if_item_elligable(item, (await rolimons_limited())[str(item.item_id)]): # check again just incase price has changed 118 | buy_data = items.BuyData( 119 | collectible_item_id = item.collectible_item_id, 120 | collectible_item_instance_id = resale_data.collectible_item_instance_id, 121 | collectible_product_id = resale_data.collectible_product_id, 122 | expected_price = resale_data.price, 123 | expected_purchaser_id = str(self.account.user_id) 124 | ) 125 | 126 | buy_response = await BuyLimited(self.account, buy_data)() 127 | 128 | webhook = request.RequestJsons.WebhookMessage( 129 | content = f"{'✅' if buy_response and buy_response[0] else '❌'} Bought Item {item.item_id} for {buy_data.expected_price} R$ | ProductID: {buy_data.collectible_product_id} | InstanceID: {buy_data.collectible_item_instance_id} | Buyer: {buy_data.expected_purchaser_id}" 130 | ) 131 | await self.ui_manager.log_event(webhook.content) 132 | await self.ui_manager.add_items_bought() 133 | 134 | await request.Request( 135 | url = self.webhook, 136 | method = "post", 137 | json_data = request.RequestJsons.jsonify_api_broad(self.webhook, webhook), 138 | success_status_codes = [204] 139 | ).send() 140 | 141 | async def get_batch_item_data(self, url: str, items: List[items.Generic], proxy = str) -> Union[None, errors.Request.Failed]: 142 | response = await request.Request( 143 | url = url, 144 | method = "post", 145 | 146 | headers = request.Headers( 147 | cookies = {".ROBLOSECURITY": self.account.cookie}, 148 | x_csrf_token = await self.account.x_csrf_token() 149 | ), 150 | json_data = request.RequestJsons.jsonify_api_broad(url, items), 151 | proxy = proxy 152 | ).send() 153 | await self.ui_manager.add_requests(1) 154 | await self.ui_manager.add_items(len(items)) 155 | await self.handle_response(response.response_json) 156 | 157 | async def watch(self): 158 | while True: 159 | try: 160 | await asyncio.gather(*[ 161 | self.get_batch_item_data(url = "https://catalog.roblox.com/v1/catalog/items/details", items = self.limiteds(120), proxy = self._proxy), 162 | self.get_batch_item_data(url = "https://apis.roblox.com/marketplace-items/v1/items/details", items = self.limiteds(30), proxy = self._proxy) 163 | ]) 164 | except: 165 | continue 166 | finally: 167 | await asyncio.sleep(1) 168 | 169 | 170 | 171 | # ben mutlu alo efe 172 | # alo 173 | # knk sus köy bok ya -------------------------------------------------------------------------------- /models/request.py: -------------------------------------------------------------------------------- 1 | import re 2 | import errors 3 | import aiohttp 4 | import authenticator 5 | 6 | from dataclasses import dataclass, field, fields, is_dataclass 7 | from typing import List, Optional, Union, Callable, Awaitable 8 | from models import items 9 | 10 | class ResponseJsons: 11 | 12 | @dataclass 13 | class ItemDetails: 14 | items: List[items.Data] 15 | 16 | @dataclass 17 | class CookieInfo: 18 | user_id: int 19 | user_name: str 20 | display_name: str 21 | 22 | @dataclass 23 | class BuyResponse: 24 | purchased_result: str 25 | purchased: bool 26 | pending: bool 27 | error_message: Union[str, None] 28 | 29 | @dataclass 30 | class ResaleResponse: 31 | collectible_item_instance_id: str 32 | collectible_product_id: str 33 | seller_id: int 34 | price: int 35 | 36 | @dataclass 37 | class TwoStepVerification: 38 | verificationToken: str 39 | 40 | @staticmethod 41 | def validate_json(url, response_json: dict) -> Union[None, "ResponseJsons.ItemDetails", "ResponseJsons.CookieInfo", "ResponseJsons.ResaleResponse", "ResponseJsons.TwoStepVerification"]: 42 | if url == "https://catalog.roblox.com/v1/catalog/items/details": 43 | items_return = [] 44 | 45 | for item in response_json.get("data", []): 46 | items_return.append( 47 | items.Data( 48 | item_id = item["id"], 49 | product_id = item["productId"], 50 | collectible_item_id = item["collectibleItemId"], 51 | lowest_resale_price = item["lowestResalePrice"] 52 | ) 53 | ) 54 | 55 | return ResponseJsons.ItemDetails(items = items_return) 56 | 57 | elif url == "https://apis.roblox.com/marketplace-items/v1/items/details": 58 | items_return = [] 59 | 60 | for item in response_json: 61 | items_return.append( 62 | items.Data( 63 | item_id = item["itemTargetId"], 64 | product_id = item["productTargetId"], 65 | collectible_item_id = item["collectibleItemId"], 66 | lowest_resale_price = item["lowestResalePrice"] 67 | ) 68 | ) 69 | 70 | return ResponseJsons.ItemDetails(items = items_return) 71 | 72 | elif url == "https://users.roblox.com/v1/users/authenticated": 73 | 74 | return ResponseJsons.CookieInfo( 75 | user_id = response_json["id"], 76 | user_name = response_json["name"], 77 | display_name = response_json["displayName"] 78 | ) 79 | 80 | elif re.match(r"^https://apis.roblox.com/marketplace-sales/v1/item/.*/purchase-resale$", url): 81 | return ResponseJsons.BuyResponse( 82 | purchased_result = response_json["purchaseResult"], 83 | purchased = response_json["purchased"], 84 | pending = response_json["pending"], 85 | error_message = response_json["errorMessage"] 86 | ) 87 | 88 | elif re.match(r"^https://apis.roblox.com/marketplace-sales/v1/item/.*/resellers\?limit=1$", url): 89 | 90 | return ResponseJsons.ResaleResponse( 91 | collectible_item_instance_id = response_json["data"][0]["collectibleItemInstanceId"], 92 | collectible_product_id = response_json["data"][0]["collectibleProductId"], 93 | seller_id = response_json["data"][0]["seller"]["sellerId"], 94 | price = response_json["data"][0]["price"] 95 | ) 96 | 97 | elif url == "https://twostepverification.roblox.com/v1/users/3254298971/challenges/authenticator/verify": 98 | 99 | return ResponseJsons.TwoStepVerification( 100 | verificationToken = response_json["verificationToken"] 101 | ) 102 | 103 | class RequestJsons: 104 | 105 | @dataclass 106 | class WebhookMessage: # adding more fields later on 107 | content: str 108 | 109 | def jsonify_api_broad(url: str, data: Union["RequestJsons.WebhookMessage", items.BuyData, List[items.Generic]]) -> dict: 110 | if url == "https://apis.roblox.com/marketplace-items/v1/items/details": 111 | return {"itemIds": [item.collectible_item_id for item in data]} 112 | elif url == "https://catalog.roblox.com/v1/catalog/items/details": 113 | return {"items": [{"id": item.item_id} for item in data]} 114 | elif re.match(r"^https://apis.roblox.com/marketplace-sales/v1/item/.*/purchase-resale$", url): 115 | return { 116 | "collectibleItemId": data.collectible_item_id, 117 | "collectibleItemInstanceId": data.collectible_item_instance_id, # from resale data pls 118 | "collectibleProductId": data.collectible_product_id, 119 | "expectedCurrency": data.expected_currency, 120 | "expectedPrice": data.expected_price, 121 | "expectedPurchaserId": data.expected_purchaser_id, 122 | "expectedPurchaserType": data.expected_purchaser_type, 123 | "expectedSeller": data.expected_seller_id, 124 | "expectedSellerType": data.expected_seller_type, 125 | "idempotencyKey": data.idempotency_key 126 | } 127 | 128 | elif re.match(r"^https:\/\/(?:canary\.|ptb\.)?discord(app)?\.com\/api\/webhooks\/\d+\/[\w-]+$", url): 129 | 130 | return { 131 | "content": data.content 132 | } 133 | 134 | @dataclass 135 | class Headers: 136 | x_csrf_token: Optional[str] = "" 137 | cookies: Optional[dict] = field(default_factory=lambda: {}) 138 | raw_headers: Optional[dict] = None 139 | 140 | @dataclass 141 | class Response: 142 | status_code: int 143 | 144 | response_headers: Headers 145 | response_json: Union[None, ResponseJsons.ItemDetails, ResponseJsons.CookieInfo, ResponseJsons.BuyResponse] 146 | response_text: str 147 | 148 | @dataclass 149 | class Request: 150 | url: str 151 | method: str 152 | 153 | headers: Optional[Headers] = field(default_factory=lambda: Headers()) 154 | json_data: Optional[dict] = None 155 | 156 | proxy: Optional[str] = None 157 | 158 | session: Optional[aiohttp.ClientSession] = None 159 | close_session: Optional[bool] = True 160 | 161 | success_status_codes: Optional[List[int]] = field(default_factory=lambda: [200]) 162 | retries: Optional[int] = 1 163 | 164 | otp_token: Optional["authenticator.AutoPass"] = None 165 | user_id: Optional[int] = 0 166 | 167 | auth: bool = False 168 | 169 | async def send(self) -> Union[Response, errors.Request.Failed]: 170 | if not self.session: 171 | self.session = aiohttp.ClientSession() 172 | 173 | exceptions = [] 174 | 175 | for i in range(self.retries): 176 | try: 177 | method: Callable[..., Awaitable[aiohttp.ClientResponse]] 178 | method = getattr(self.session, self.method) 179 | headers = {"x-csrf-token": str(self.headers.x_csrf_token)} if not self.headers.raw_headers else self.headers.raw_headers 180 | response = await method(self.url, headers = headers, cookies = self.headers.cookies, json = self.json_data, proxy = self.proxy) 181 | if response.status in self.success_status_codes or (response.status == 403 and self.otp_token and self.user_id): 182 | if response.status == 403 and self.otp_token and self.user_id and "Challenge" in await response.text(): 183 | challange_data = authenticator.ChallangeData( 184 | rblx_challange_id = response.headers.get("rblx-challenge-id"), 185 | rblx_challange_metadata = response.headers.get("rblx-challenge-metadata"), 186 | rblx_challange_type = response.headers.get("rblx-challenge-type") 187 | ) 188 | request_formatted = await self.otp_token(self, challenge_data = challange_data) 189 | request_formatted.close_session = True 190 | return await request_formatted.send() 191 | 192 | response_cookies = {cookie.key: cookie.value for cookie in self.session.cookie_jar} 193 | response_headers = Headers(x_csrf_token = response.headers.get("x-csrf-token"), cookies = response_cookies, raw_headers = dict(response.headers)) 194 | 195 | try: 196 | response_json = ResponseJsons.validate_json(self.url, await response.json()) 197 | except: 198 | response_json = None 199 | if self.close_session: 200 | await self.session.close() 201 | 202 | return Response(status_code = response.status, response_headers = response_headers, response_json = response_json, response_text = await response.text()) 203 | else: 204 | print(await response.text()) 205 | raise errors.Request.InvalidStatus(response.status) 206 | except Exception as reason: 207 | exceptions.append(reason) 208 | 209 | if self.close_session: 210 | await self.session.close() 211 | 212 | raise errors.Request.Failed(exceptions) -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import time 4 | import json 5 | import errors 6 | import random 7 | import asyncio 8 | import aiohttp 9 | 10 | from models import request, items 11 | from typing import Optional, Union, List, Dict, TYPE_CHECKING 12 | 13 | from rich.console import Console 14 | from rich.live import Live 15 | from rich.table import Table 16 | from rich.panel import Panel 17 | from rich.text import Text 18 | from rich.align import Align 19 | from rich.layout import Layout 20 | from rich.box import HEAVY 21 | 22 | if TYPE_CHECKING: 23 | from sniper import WatchLimiteds 24 | 25 | class UIManager: 26 | def __init__(self, total_proxies: int): 27 | self.start_time = time.time() 28 | self.total_proxies = total_proxies 29 | self.total_requests = 0 30 | self.total_items_checked = 0 31 | self.total_items_bought = 0 32 | self.lock = asyncio.Lock() 33 | self.logs = [] 34 | 35 | async def log_event(self, message: str): 36 | async with self.lock: 37 | timestamp = time.strftime('%H:%M:%S') 38 | self.logs.append(f"[{timestamp}] {message}") 39 | if len(self.logs) > 20: 40 | self.logs.pop(0) 41 | 42 | async def add_requests(self, count: int): 43 | async with self.lock: 44 | self.total_requests += count 45 | 46 | async def add_items(self, count: int): 47 | async with self.lock: 48 | self.total_items_checked += count 49 | 50 | async def add_items_bought(self, count: int = 1): 51 | async with self.lock: 52 | self.total_items_bought += count 53 | 54 | def render(self): 55 | elapsed = int(time.time() - self.start_time) 56 | mins, secs = divmod(elapsed, 60) 57 | uptime = f"{mins}m {secs}s" 58 | 59 | stats = Table.grid(padding=1) 60 | stats.add_column(justify="right", style="bold cyan") 61 | stats.add_column(style="bold white") 62 | 63 | stats.add_row("Proxies", str(self.total_proxies)) 64 | stats.add_row("Total Requests", str(self.total_requests)) 65 | stats.add_row("Items Checked", str(self.total_items_checked)) 66 | stats.add_row("Items Bought", str(self.total_items_bought)) 67 | stats.add_row("Uptime", uptime) 68 | 69 | log_panel = Panel( 70 | "\n".join(self.logs) if self.logs else "[grey]No logs yet...", 71 | title="Recent Events", 72 | border_style="yellow", 73 | padding=(1, 2) 74 | ) 75 | 76 | layout = Layout() 77 | layout.split( 78 | Layout(Panel(stats, title="Global Stats", border_style="green", padding=(1, 2)), name="upper", size=14), 79 | Layout(log_panel, name="lower") 80 | ) 81 | 82 | return layout 83 | 84 | async def run_ui(ui_manager: UIManager): 85 | console = Console() 86 | with Live(ui_manager.render(), refresh_per_second=2, console=console) as live: 87 | while True: 88 | await asyncio.sleep(0.5) 89 | live.update(ui_manager.render()) 90 | 91 | class CombinedAttribute: 92 | def __init__(self, watch_limiteds: 'WatchLimiteds'): 93 | self.watch_limiteds = watch_limiteds 94 | 95 | def __getattr__(self, name): 96 | return getattr(self.watch_limiteds, name) 97 | 98 | def __setattr__(self, name, value): 99 | if name == "watch_limiteds" or name.startswith("_"): 100 | super().__setattr__(name, value) 101 | else: 102 | setattr(self.watch_limiteds, name, value) 103 | 104 | def __delattr__(self, name): 105 | if name == "watch_limiteds" or name.startswith("_"): 106 | super().__delattr__(name) 107 | else: 108 | delattr(self.watch_limiteds, name) 109 | 110 | class Iterator: 111 | def __init__(self, data: List[items.Generic]): 112 | self.original_data = data[:] 113 | self._reset_pool() 114 | 115 | def _reset_pool(self, ): 116 | self.pool = self.original_data[:] 117 | random.shuffle(self.pool) 118 | self.index = 0 119 | 120 | def __call__(self, batch_size: int) -> List[items.Generic]: 121 | if batch_size >= len(self.original_data): 122 | return self.original_data[:] 123 | 124 | batch = [] 125 | 126 | while len(batch) < batch_size: 127 | if self.index >= len(self.pool): 128 | self._reset_pool() 129 | 130 | needed = batch_size - len(batch) 131 | available = len(self.pool) - self.index 132 | take = min(needed, available) 133 | 134 | batch.extend(self.pool[self.index:self.index + take]) 135 | self.index += take 136 | 137 | return batch 138 | 139 | class XCsrfTokenWaiter: 140 | def __init__(self, cookie: Optional[str] = None, proxy: Optional[str] = None, on_start: bool = False): 141 | self.last_call_time = time.time() 142 | 143 | self.cookie = cookie 144 | self.proxy = proxy 145 | 146 | self.x_crsf_token = None if not on_start else asyncio.run(self.generate_x_csrf_token(self.cookie, self.proxy)) 147 | 148 | async def __call__(self) -> Union[None, str]: 149 | now = time.time() 150 | elapsed = now - self.last_call_time 151 | 152 | if elapsed > 120: 153 | self.x_crsf_token = await self.generate_x_csrf_token(self.cookie, self.proxy) 154 | if self.x_crsf_token: 155 | self.last_call_time = now 156 | 157 | return self.x_crsf_token 158 | 159 | @staticmethod 160 | async def generate_x_csrf_token(cookie: Union[str, None], proxy: Union[str, None]) -> Union[str, None]: 161 | response: request.Response 162 | response = await request.Request( 163 | url = "https://auth.roblox.com/v2/logout", 164 | method = "post", 165 | headers = request.Headers( 166 | cookies = {".ROBLOSECURITY": cookie} 167 | ), 168 | success_status_codes = [403], 169 | proxy = proxy 170 | ).send() 171 | return response.response_headers.x_csrf_token 172 | 173 | class RolimonsDataScraper: 174 | def __init__(self): 175 | self.last_call_time = time.time() 176 | self.item_data: Dict[str, items.RolimonsData] = None 177 | 178 | async def __call__(self) -> Union[None, Dict[str, items.RolimonsData]]: 179 | now = time.time() 180 | elapsed = now - self.last_call_time 181 | 182 | if elapsed > 600 or not self.item_data: 183 | self.item_data = await self.retrieve_item_data() 184 | if self.item_data: 185 | self.last_call_time = now 186 | 187 | return self.item_data 188 | 189 | @staticmethod 190 | def extract_variable(html: str) -> Union[None, Dict[str, List]]: 191 | item_pattern = re.compile(r'var\s+item_details\s*=\s*(\{.*?\});', re.DOTALL) 192 | 193 | match = item_pattern.search(html) 194 | if match: 195 | js_data = match.group(1) 196 | 197 | try: 198 | parsed_data = json.loads(js_data) 199 | return parsed_data 200 | except json.JSONDecodeError as e: 201 | return None 202 | else: 203 | return None 204 | 205 | @staticmethod 206 | async def retrieve_item_data() -> Dict[str, items.RolimonsData]: 207 | headers = request.Headers( raw_headers = { 208 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 209 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 210 | "Accept-Encoding": "gzip, deflate, br", 211 | "Accept-Language": "en-US,en;q=0.9", 212 | "Connection": "keep-alive", 213 | "Upgrade-Insecure-Requests": "1", 214 | "Cache-Control": "max-age=0" 215 | }) 216 | 217 | response: request.Response 218 | response = await request.Request( 219 | url = "https://www.rolimons.com/catalog", 220 | method = "get", 221 | headers = headers 222 | ).send() 223 | 224 | item_details = RolimonsDataScraper.extract_variable(response.response_text) 225 | if item_details: 226 | items_dataclass = {} 227 | for item_id, data in item_details.items(): 228 | items_dataclass[item_id] = items.RolimonsData( 229 | rap = data[8], 230 | value = data[16] 231 | ) 232 | 233 | return items_dataclass 234 | else: 235 | return None 236 | 237 | class UnlockCookie: 238 | def __init__(self, cookie: str) -> None: 239 | self.cookie = cookie 240 | 241 | async def __call__(self) -> Union[str, 'errors.InvalidCookie']: 242 | try: 243 | async with aiohttp.ClientSession() as session: 244 | csrf_token = await XCsrfTokenWaiter.generate_x_csrf_token(self.cookie, None) 245 | auth_ticket = await self.get_authentication_ticket(session, csrf_token) 246 | new_cookie = await self.redeem_authentication_ticket(session, csrf_token, auth_ticket) 247 | 248 | if new_cookie: 249 | return new_cookie 250 | else: 251 | raise Exception("No .ROBLOSECURITY cookie returned") 252 | 253 | except Exception as reason: 254 | raise errors.InvalidCookie(reason) 255 | 256 | async def get_authentication_ticket(self, session: aiohttp.ClientSession, x_csrf_token: str) -> str: 257 | req = request.Request( 258 | url="https://auth.roblox.com/v1/authentication-ticket", 259 | method="post", 260 | headers=request.Headers( 261 | x_csrf_token=x_csrf_token, 262 | cookies={".ROBLOSECURITY": self.cookie}, 263 | raw_headers={ 264 | "rbxauthenticationnegotiation": "1", 265 | "referer": "https://www.roblox.com/camel", 266 | "Content-Type": "application/json", 267 | "x-csrf-token": x_csrf_token, 268 | }, 269 | ), 270 | session=session, 271 | close_session=False, 272 | ) 273 | 274 | res = await req.send() 275 | 276 | ticket = res.response_headers.raw_headers.get("rbx-authentication-ticket") 277 | if not ticket: 278 | raise ValueError("Failed to retrieve authentication ticket") 279 | return ticket 280 | 281 | async def redeem_authentication_ticket(self, session: aiohttp.ClientSession, x_csrf_token: str, authentication_ticket: str) -> str: 282 | req = request.Request( 283 | url="https://auth.roblox.com/v1/authentication-ticket/redeem", 284 | method="post", 285 | headers=request.Headers( 286 | x_csrf_token=x_csrf_token, 287 | raw_headers={ 288 | "rbxauthenticationnegotiation": "1", 289 | "x-csrf-token": x_csrf_token, 290 | }, 291 | ), 292 | json_data={"authenticationTicket": authentication_ticket}, 293 | session=session, 294 | close_session=False, 295 | ) 296 | 297 | res = await req.send() 298 | 299 | roblosecurity = res.response_headers.cookies.get(".ROBLOSECURITY") 300 | if not roblosecurity: 301 | raise ValueError("Failed to retrieve .ROBLOSECURITY from response") 302 | return roblosecurity 303 | --------------------------------------------------------------------------------