def main(): helper.testnet = args.testnet helper.password = args.password if helper.testnet: helper.ports.ports_to_testnet() try: while True: for i in sorted( os.listdir("wallets/" + ("testnet/" if args.testnet else "mainnet/"))): if not "." in i: start = int(round(time.time() * 1000)) print("Opening " + i + "'s wallet") with HiddenPrints(): get_info(wallet_name=i, private_info=False, password=args.password, port=helper.ports.wallet_sync_port, timeout=sync_time) if int(round(time.time() * 1000)) - start > sync_time * 1000 - 10000: print("Warn: " + i + "'s wallet is likely unsynced") print("Taking a break...") time.sleep(sync_time) # Poor CPU except Exception as e: tipper_logger.log("walletsyncer error: " + str(e)) traceback.print_exc() main()
def open_rpc(self, port, wallet_name, password=wallet_password, timeout=timeout, tries=5): if tries == 0: tipper_logger.log( f"WARNING: FAILED to open {wallet_name}'s wallet!!") return self.rpc = RPC(port=port, wallet_name=wallet_name, password=password, load_timeout=timeout) if not os.path.isfile("aborted-" + wallet_name ): # Check if wallet was emergency aborted self.wallet = Wallet( JSONRPCWallet(port=self.rpc.port, password=self.rpc.password, timeout=self.rpc.load_timeout)) else: tipper_logger.log( f"WARNING: {wallet_name} had their RPC aborted!!! Trying {tries} more times" ) os.remove("aborted-" + wallet_name) self.open_rpc(port=port, wallet_name=wallet_name, password=password, timeout=timeout, tries=tries - 1)
def handle_tip_request(author, body, comment): """ Handles the tipping interaction, called by a Redditor's comment Replies to the user if the response is not None Sends user a message if message is not None :param body: The contents of a comment that called the bot :param author: The username of the entity that created the comment :param comment: The comment itself that called the bot """ recipient = get_tip_recipient(comment) amount = helper.parse_amount(f'/u/{helper.botname.lower()} (tip )?', body) if recipient is None or amount is None: reply = "Nothing interesting happens.\n\n*In case you were trying to tip, I didn't understand you.*" elif Decimal(amount) < Decimal(0.0001): reply = helper.get_below_threshold_message() else: tipper_logger.log(f'{author} is sending {recipient} {amount} XMR.') generate_wallet_if_doesnt_exist(recipient.lower()) res = tip(sender=author, recipient=recipient, amount=amount) reply = f'{res["response"]}' tipper_logger.log("The response is: " + reply) if res["message"] is not None: helper.praw.redditor(author).message( subject="Your tip", message=f"Regarding your tip here: {comment.context}\n\n" + res["message"] + get_signature()) helper.praw.comment(str(comment)).reply(reply + get_signature())
def handle_anonymous_tip(author, subject, contents): """ Allows people to send anonymous tips :param author: Reddit account to withdraw from :param subject: Subject line of the message, telling who to tip and how much :param contents: Message body (ignored) """ recipient = parse_anon_tip_recipient(subject) amount = parse_anon_tip_amount(subject) if recipient is None or amount is None: helper.praw.redditor(author).message(subject="Your anonymous tip", message="Nothing interesting happens.\n\n*Your recipient or amount wasn't clear to me*" + get_signature()) return if Decimal(amount) < (0.0001): # Less than amount displayed in balance page helper.praw.redditor(author).message(subject="Your anonymous tip", message=helper.get_below_threshold_message() + get_signature()) return generate_wallet_if_doesnt_exist(recipient) tipper_logger.log(author + " is trying to send " + parse_anon_tip_amount(subject) + " XMR to " + parse_anon_tip_recipient(subject)) res = tip(sender=author, recipient=recipient, amount=amount) if res["message"] is not None: helper.praw.redditor(author).message(subject="Your anonymous tip", message=res["message"] + get_signature()) else: helper.praw.redditor(author).message(subject="Anonymous tip successful", message=res["response"] + get_signature()) helper.praw.redditor(recipient).message(f"You have received an anonymous tip of {amount} XMR! ({helper.get_dollar_val(amount)} USD)", message=(get_signature() if contents == helper.no_message_anon_tip_string else "The tipper attached the following message:\n\n" + contents + get_signature()))
def generate_wallet(name, password=None): """ Generates a new user wallet Stores the blockheight in a file named user_blockheight :param name: Name of user generating the wallet :param password: Password to give the new wallet :return True on successful wallet generation, False otherwise """ if password is None: password = helper.password name = str(name) rpc = RPC(port=helper.ports.generate_wallet_port) rpc_url = f"http://127.0.0.1:{helper.ports.generate_wallet_port}/json_rpc" function_url = "http://127.0.0.1:" + str(helper.ports.monerod_port) + "/get_height" headers = {'Content-Type': 'application/json'} payload = { "jsonrpc" : "2.0", "id" : "0", "method" : "create_wallet", "params": { "filename" : name, "password" : password, "language" : "English", } } tipper_logger.log(f"Generating wallet for {name}.") try: requests.post(rpc_url, data=json.dumps(payload), headers=headers).json() except Exception as e: tipper_logger.log(str(e)) try: blockheight_response = requests.post(function_url, headers=headers).json() print(blockheight_response["height"] - 10, file=open('wallets/' + ("testnet/" if helper.testnet else "mainnet/") + name + ".height", 'w')) # DON'T CHANGE THIS DUMDUM except Exception as e: tipper_logger.log(str(e)) rpc.kill() # Create .address.txt (probably a better way since we already had a wallet open) wallet = SafeWallet(port=helper.ports.create_address_txt_port, wallet_name=name, wallet_password=password) address = wallet.rpc.run_rpc_request( '{"jsonrpc":"2.0","id":"0","method":"get_address","params":{"account_index":0,"address_index":[0]}}').json()[ "result"]["address"] print(address, file=open('wallets/' + ("testnet/" if helper.testnet else "mainnet/") + name + ".address.txt", 'w')) wallet.kill_rpc() if wallet_exists(name): tipper_logger.log("Generated a wallet for " + name) return True tipper_logger.log("Failed to generate a wallet for " + name) return False
def wait_for_rpc_to_load(self): """ Waits for RPC to confirm it's ready for commands """ rpc_read_process = multiprocessing.Process( target=self.check_rpc_loaded) rpc_read_process.start() rpc_read_process.join(timeout=self.load_timeout) rpc_read_process.kill() tipper_logger.log("Got final status of rpc reader")
def main(): while True: tipper_logger.log("Searching for new messages") start_time = datetime.datetime.now().timestamp() author = None try: for message in helper.praw.inbox.stream(): if not message.author: helper.praw.inbox.mark_read( [message] ) # Gets rid of messages that otherwise crash service (i.e. sub bans) else: author = message.author.name if message.created_utc > start_time: process_message(author=author, comment=message, subject=message.subject, body=message.body) except Exception as e: try: if "read timeout" not in str(e).lower() \ and "reddit.com timed out" not in str(e) \ and "503" not in str(e): tipper_logger.log("Main error: " + str(e)) tipper_logger.log("Blame " + author) traceback.print_exc() helper.praw.redditor("OsrsNeedsF2P").message( subject=f"Something broke for /u/{author}!!", message=f"{str(e)}" + helper.get_signature()) except Exception as e: tipper_logger.log("Just wow." + str(e))
def get_tip_recipient(comment): """ Determines the recipient of the tip, based on the comment requesting the tip :param comment: The PRAW comment that notified the bot :return: String representing Username of the parent of the comment """ author = None try: author = comment.parent().author except Exception: tipper_logger.log("Somehow there's no parent at all?") return fix_automoderator_recipient(author.name)
def generate_transaction(sender_wallet, recipient_address, amount, split_size=6, timeout=50): """ Generates a transaction with multiple outputs instead of 2 This allows for the recipient to spend more easily. Each output is worth approx. amount/splitSize XMR :param sender_wallet: Wallet to send Monero from :param recipient_address: Address to receive Monero :param amount: The amount to send, in XMR :param split_size: The amount of outputs to generate :param timeout: Time (in seconds) to try and broadcast a tx before returning failure :return: TXID on success, the string "FAILURE" otherwise """ sum = 0 transactions = [] decimalamount = Decimal(amount) senderwalletbalance = sender_wallet.balance() if Decimal(amount) > sender_wallet.balance() - Decimal(0.001) and Decimal( amount ) < Decimal(0.1) + sender_wallet.balance( ): # If you're sending more than your balance, but not much more -- tipper_logger.log("Sending sweep_all transaction...") sweep_res = timeout_function(target=send_sweep_all, args=(sender_wallet, recipient_address), timeout=timeout) tipper_logger.log("Sweep res is: " + str(sweep_res)) if not is_txid(sweep_res): raise ValueError( sweep_res ) #It'll get caught by the calling function which will handle it return sweep_res # Make multiple of the same output, but in smaller chunks for i in range(0, split_size - 1): sum += float(amount) / split_size transactions.append( (recipient_address, Decimal(float(amount) / split_size))) # Add the remainder transactions.append((recipient_address, Decimal(float(amount) - sum))) tipper_logger.log("About to broadcast transaction..") broadcast_res = timeout_function(target=broadcast_transaction, args=(sender_wallet, transactions), timeout=timeout) tipper_logger.log("Broadcast res is: " + str(broadcast_res)) if not is_txid(broadcast_res): raise ValueError(broadcast_res) return broadcast_res
def handle_info_request(author, private_info=False): """ Allows Reddit users to see their wallet address, balance, and optionally their private key. :param author: Username of the entity requesting their info :param private_info: Whether or not to send the private key (mnemonic) along with the message :return: """ helper.praw.redditor(author).message( subject="Your " + ("private address and info" if private_info else "public address and balance"), message=get_info_as_string(wallet_name=author.lower(), private_info=private_info) + get_signature()) tipper_logger.log( f'Told {author} their {("private" if private_info else "public")} info.' )
def kill_existing_rpc(self, port): for proc in psutil.process_iter(): try: for conns in proc.connections(kind='inet'): if conns.laddr.port == port: proc.send_signal(SIGTERM) print("MURDERING THE SIGNAL") # TODO: Sleep until it's dead, timeout 20 seconds? except psutil.AccessDenied: # This is fine, because this issue was not incurred when trying to kill the signal. pass except Exception as e: # This could be bad, so let's log it just in case. tipper_logger.log( "RPC BAD: Something bad happened with trying to kill the RPC?" ) tipper_logger.log(e)
def handle_donation(author, subject): """ Allows Reddit users to donate a portion of their balance directly to the CCS CCS can be seen at: https://ccs.getmonero.org/ :param author: Reddit account to withdraw from :param subject: Subject line of the message, telling how much to withdraw """ sender_rpc_n_wallet = SafeWallet(port=helper.ports.donation_sender_port, wallet_name=author.lower(), wallet_password=helper.password) amount = Decimal( helper.parse_amount('donate ', subject, balance=sender_rpc_n_wallet.wallet.balance())) try: generate_transaction( sender_wallet=sender_rpc_n_wallet.wallet, recipient_address=helper.get_general_fund_address(), amount=amount, split_size=1) helper.praw.redditor(author).message( subject="Your donation to the General Dev Fund", message= f'Thank you for donating {format_decimal(amount)} of your XMR balance to the CCS!\n\nYou will soon have your total donations broadcasted to the wiki :) {get_signature()}' ) helper.praw.redditor("OsrsNeedsF2P").message( subject=f'{author} donated {amount} to the CCS!', message= f"Update table here: https://old.reddit.com/r/{helper.botname}/wiki/index#wiki_donating_to_the_ccs" ) tipper_logger.log( f'{author} donated {format_decimal(amount)} to the CCS.') except Exception as e: helper.praw.redditor(author).message( subject="Your donation to the CCS failed", message=f'Please send the following to /u/OsrsNeedsF2P:\n\n' + str(e) + get_signature()) tipper_logger.log("Caught an error during a donation to CCS: " + str(e)) sender_rpc_n_wallet.kill_rpc()
def handle_withdraw(sender_wallet, sender_name, recipient_address, amount): """ Withdraws Monero from sender_name's wallet :param sender_wallet: sender_name's wallet :param sender_name: User who wishes to withdraw :param recipient_address: Address to send funds to :param amount: Amount to send in XMR :return: Response message regarding status of send """ tipper_logger.log(f'{sender_name} is trying to send {recipient_address} {amount} XMR') try: res = "Withdrawal success! [Txid](" \ f"{helper.get_xmrchain(generate_transaction(sender_wallet=sender_wallet, recipient_address=recipient_address, amount=Decimal(amount)))})" except Exception as e: tipper_logger.log(e) res = get_error_response(e) return res
def check_rpc_loaded(self): """ Loops through RPC output until it detects it's ready/failed """ rpc_output = self.parse_rpc_output() while rpc_output == "LOADING": time.sleep(0.01) rpc_output = self.parse_rpc_output() if rpc_output == "FAIL": tipper_logger.log("RPC Failed!!! Aborting!") self.kill() self.kill_existing_rpc( self.port ) # Any wallet attempted to be created with this RPC will now fail open("aborted-" + self.wallet_name, "w").close() time.sleep(0.01) # Just in case - time to write if rpc_output == "SUCCESS": tipper_logger.log("Wallet loaded in time")
def __init__(self, port, wallet_name=None, rpc_location="monero_tools/extras/monero-wallet-rpc", password=None, disable_rpc_login=True, load_timeout=300): if password is None: password = helper.password self.port = port self.wallet_name = wallet_name self.rpc_location = rpc_location self.password = password self.disable_rpc_login = disable_rpc_login self.load_timeout = load_timeout if wallet_name is not None: # Open wallet rpc_command = f'{rpc_location} --wallet-file ./wallets/{"testnet/" if helper.testnet else "mainnet/"}{wallet_name} --password {password} --rpc-bind-port {port} {"--testnet" if helper.testnet else ""} {"--disable-rpc-login" if disable_rpc_login else ""}' else: # Create new wallet rpc_command = f'{rpc_location} --wallet-dir ./wallets/{"testnet/" if helper.testnet else "mainnet/"} --rpc-bind-port {port}{" --testnet" if helper.testnet else ""}{" --disable-rpc-login" if disable_rpc_login else ""}' tipper_logger.log(rpc_command) rpc_command_shelled = shlex.split(rpc_command) self.kill_existing_rpc( port) # Prevents an old RPC from accidentally being reused self.rpc_process = subprocess.Popen(rpc_command_shelled, stdout=subprocess.PIPE) self.wait_for_rpc_to_load() if os.path.isfile( "locked" ): # Check if we were syncing it with walletsyncer.py in another program print("Wallet locked - waiting 90 sec and trying again") os.remove("locked") self.kill() time.sleep(90) self.rpc_process = subprocess.Popen(rpc_command_shelled, stdout=subprocess.PIPE) self.wait_for_rpc_to_load()
def handle_withdraw_request(author, subject, contents): """ Handles the withdrawal request, setting up RPC and calling the withdraw function :param author: Wallet to withdraw from :param subject: The withdrawl request string :param contents: The address to withdraw to :return: Response message about withdrawl request """ amount = helper.parse_amount("withdraw ", subject) if amount is None: helper.praw.redditor(author).message(subject="I didn't understand your withdrawal!", message=f'You sent: "{subject}", but I couldn\'t figure out how much you wanted to send. See [this](https://www.reddit.com/r/{helper.botname}/wiki/index#wiki_withdrawing) guide if you need help, or click "Report a Bug" under "Get Started" if you think there\'s a bug!' + get_signature()) return None sender_rpc_n_wallet = SafeWallet(port=helper.ports.withdraw_sender_port, wallet_name=author.lower(), wallet_password=helper.password) res = str(handle_withdraw(sender_rpc_n_wallet.wallet, author, contents, amount)) sender_rpc_n_wallet.kill_rpc() helper.praw.redditor(author).message(subject="Your withdrawl", message=res + get_signature()) tipper_logger.log("Told " + author + " their withdrawl status (" + res + ")")
def process_message(author, comment, subject, body): """ Handles the comment command a user tried to execute :param subject: Subject line of private message :param body: Body of private message :param author: Username of author :param comment: comment to parse for the command """ tipper_logger.log("Got message " + body) tipper_logger.log(f'Received message: {subject} from {author}: {body}') generate_wallet_if_doesnt_exist(name=author.lower(), password=helper.password) if comment_requests_tip(body): handle_tip_request(author=author, body=body, comment=comment) return if subject_requests_info(subject): handle_info_request(author=author, private_info=False) return if subject_requests_private_info(subject): handle_info_request(author=author, private_info=True) return if subject_requests_withdraw(subject): handle_withdraw_request(author=author, subject=subject, contents=body) return if subject_requests_donate(subject): handle_donation(author=author, subject=subject) return if subject_requests_anonymous_tip(subject): handle_anonymous_tip(author=author, subject=subject, contents=body) return helper.praw.redditor(author).message(subject="I didn't understand your command", message=f'I didn\'t understand what you meant last time you tagged me. You said: \n\n{body}\n\nIf you didn\'t mean to summon me, you\'re all good! If you\'re confused, please let my owner know by clicking Report a Bug!{helper.get_signature()}')
def parse_rpc_output(self): """ Reads 1 line from RPC output :return: Status of RPC - LOADING if still unknown, FAIL if an error occurred, SUCCESS otherwise """ rpc_out = self.rpc_process.stdout.readline() tipper_logger.log("RPC:" + str(rpc_out)) if "error" in str(rpc_out).lower() or "failed to initialize" in str( rpc_out).lower(): if "locking fd" in str(rpc_out.lower()): tipper_logger.log( "The wallet is already open (that's likely fine..)") open("locked", "w").close() time.sleep(0.01) # Just in case else: tipper_logger.log("Found out the RPC has an error (FAIL)") return "FAIL" if "starting wallet rpc server" in str(rpc_out.lower()): tipper_logger.log("Found out the RPC has started (SUCCESS)") return "SUCCESS" return "LOADING"
def tip(sender, recipient, amount): """ Sends Monero from sender to recipient If the sender and the recipient are the same, it creates only 1 rpc Always closes RPCs, even on failure :param sender: name of wallet sending Monero :param recipient: name of wallet receiving :param amount: amount to send in XMR :return info: dictionary containing txid, a private message and a public response """ info = { "txid": "None", "response": "None", #Comment reply "message": None #Error message } tipper_logger.log(sender + " is trying to send " + recipient + " " + amount + " XMR") sender = str(sender) recipient = str(recipient) sender_rpc_n_wallet = None try: sender_rpc_n_wallet = SafeWallet(port=helper.ports.tip_sender_port, wallet_password=helper.password, wallet_name=sender.lower()) tipper_logger.log("Sender wallet loaded!!") except Exception as e: sender_rpc_n_wallet.kill_rpc() tipper_logger.log("Failed to open wallets for " + sender + " and " + recipient + ". Message: ") tipper_logger.log(e) info[ "response"] = "Could not open wallets properly! Perhaps my node is out of sync? (Try again shortly).\n\n^/u/OsrsNeedsF2P!!" info["message"] = str(e) return info tipper_logger.log("Successfully initialized wallets..") try: recipient_address = helper.get_address_txt(recipient.lower()) txs = generate_transaction(sender_wallet=sender_rpc_n_wallet.wallet, recipient_address=recipient_address, amount=amount) info["txid"] = str(txs) info[ "response"] = f"Successfully tipped /u/{recipient} {amount} XMR! [^(txid)]({helper.get_xmrchain(txs)})" tipper_logger.log("Successfully sent tip") except Exception as e: tipper_logger.log(e) traceback.print_exc() info["message"] = get_error_response(e) info[ "response"] = "Didn't tip - Check your private message to see why :)" sender_rpc_n_wallet.kill_rpc() tipper_logger.log("Tip function completed without crashing") return info
def fix_automoderator_recipient(recipient): if recipient.lower() == "automoderator": tipper_logger.log( f"Changing recipient to {helper.botname} to prevent abuse") return helper.botname return recipient