diff --git a/api/runtimeconfig.json b/api/runtimeconfig.json index 4da3b05..e244908 100644 --- a/api/runtimeconfig.json +++ b/api/runtimeconfig.json @@ -11,7 +11,8 @@ "guild_log_whitelist": {}, "ping_server_override": false, "debug_mode": false, - "show_ping_on_startup": true + "show_ping_on_startup": true, + "isocard_server_enabled": true }, "replit": false, "other_keys": { diff --git a/cogs/isocard.py b/cogs/isocard.py index 89e6f4b..4088c78 100644 --- a/cogs/isocard.py +++ b/cogs/isocard.py @@ -3,6 +3,7 @@ # Imports import discord import random +import json from framework.isobot.db.isocard import IsoCard from discord import option, ApplicationContext, SlashCommandGroup from discord.ext import commands @@ -74,15 +75,16 @@ async def info(self, ctx: ApplicationContext, card_number: int): description="View a list of all your cards." ) async def my_card(self, ctx: ApplicationContext): - all_card_numbers = isocard_db.keys() + all_card_numbers = isocard_db.fetch_all_cards() + isocard_database = isocard_db.raw() your_cards = list() for card in all_card_numbers: - if isocard_db[str(card)]["cardholder_user_id"] == ctx.author.id: your_cards.append(str(card)) + if isocard_database[str(card)]["cardholder_user_id"] == ctx.author.id: your_cards.append(str(card)) embed_desc = str() sr = 1 for card in your_cards: - if isocard_db[str(card)]["config"]["card_label"] != None: - embed_desc += f"{sr}. **{card}**: {isocard_db[str(card)]['config']['card_label']}\n" + if isocard_database[str(card)]["config"]["card_label"] != None: + embed_desc += f"{sr}. **{card}**: {isocard_database[str(card)]['config']['card_label']}\n" else: embed_desc += f"{sr}. **{card}**\n" sr += 1 embed_desc += "\n*Nothing more here*" @@ -108,5 +110,24 @@ async def options_label(self, ctx: ApplicationContext, card_number: int, new_lab if new_label == None: return await ctx.respond(embed=discord.Embed(description=":white_check_mark: Your card label has been reset.", color=discord.Color.green()), ephemeral=True) else: return await ctx.respond(embed=discord.Embed(description=":white_check_mark: Your card label has been edited.", color=discord.Color.green()), ephemeral=True) + @isocard.command( + name="verify_transaction", + description="Verify an ongoing transaction." + ) + @option(name="verification_code", description="The 6-digit verification code for your transaction", type=int) + async def verify_transaction(self, ctx: ApplicationContext, verification_code: int): + try: + with open("database/isocard_transactions.json", 'r') as f: transactions_db = json.load(f) + if transactions_db[str(verification_code)]["payer_id"] == ctx.author.id: + transactions_db[str(verification_code)]["status"] = "complete" + with open("database/isocard_transactions.json", 'w+') as f: json.dump(transactions_db, f, indent=4) + localembed = discord.Embed( + title="Transaction successfully verified.", + description="Please wait patiently until the merchant has verified the transaction.", + color=discord.Color.green() + ) + await ctx.respond(embed=localembed, ephemeral=True) + except KeyError: return await ctx.respond("This transaction verification code is invalid.") + # Initialization def setup(bot): bot.add_cog(IsoCard(bot)) diff --git a/framework/isobot/db/isocard.py b/framework/isobot/db/isocard.py index 2261192..43b7a7c 100644 --- a/framework/isobot/db/isocard.py +++ b/framework/isobot/db/isocard.py @@ -20,6 +20,16 @@ def save(self, data: dict) -> int: with open("database/isocard.json", 'w+', encoding="utf-8") as f: json.dump(data, f, indent=4) return 0 + def raw(self) -> dict: + """Returns all of the raw data from the IsoCard database.\n\n***WARNING:*** This function must **ONLY** be used for validation of IsoCard ownership, and nothing else.""" + isocard_db = self.load() + return isocard_db + + def fetch_all_cards(self) -> list: + """Fetches a `list` of all registered IsoCard numbers in the IsoCard database.\n\n***WARNING:*** This function must **ONLY** be used for validation of IsoCard ownership, and nothing else.""" + isocard_db = self.load() + return list(isocard_db.keys()) + def fetch_card_data(self, card_id: int) -> dict: """Fetches the raw `dict` data related to the given IsoCard id.\n\nReturns data as `dict` if successful, returns `KeyError` if card id does not exist.""" isocard_db = self.load() diff --git a/framework/isobot/isocard.py b/framework/isobot/isocard.py new file mode 100644 index 0000000..60bc2b4 --- /dev/null +++ b/framework/isobot/isocard.py @@ -0,0 +1,161 @@ +"""The IsoCard payments web server.""" +# Imports +import json +import random +import logging +from api import auth +from flask import Flask +from flask import request +from framework.isobot import currency +from threading import Thread + +# Configuration +log = logging.getLogger('werkzeug') +log.setLevel(logging.ERROR) +app = Flask('') +currency = currency.CurrencyAPI("database/currency.json", "logs/currency.log") + +def call_isocards_database() -> dict: + """Calls all of the latest information from the IsoCards database.""" + with open("database/isocard.json", 'r') as f: isocards = json.load(f) + return isocards + +def save(data): + """Dumps all cached databases to the local machine.""" + with open("database/isocard_transactions.json", 'w+') as f: json.dump(data, f, indent=4) + +# Functions +def generate_verification_code() -> int: + """Generates a random 6 digit verification code.""" + int_1 = str(random.randint(1, 9)) + int_2 = str(random.randint(0, 9)) + int_3 = str(random.randint(0, 9)) + int_4 = str(random.randint(0, 9)) + int_5 = str(random.randint(0, 9)) + int_6 = str(random.randint(0, 9)) + code: str = int_1 + int_2 + int_3 + int_4 + int_5 + int_6 + return int(code) + +# API Commands +@app.route('/', methods=["GET"]) +def main(): + return "Server is online." + +@app.route('/requestpayment', methods=["GET"]) +def requestpayment(): + try: + isocards = call_isocards_database() + with open("database/isocard_transactions.json", 'r') as f: transactions_db = json.load(f) + args = request.args + card_number = args.get("cardnumber") + ssc = args.get("ssc") + amount = args.get("amount") + merchant_id = args.get("merchantid") + if str(isocards[str(card_number)]["ssc"]) == ssc: + verification_code = generate_verification_code() + user_id = isocards[str(card_number)]["cardholder_user_id"] + transactions_db[str(verification_code)] = { + "payer_id": user_id, + "merchant_id": merchant_id, + "card_number": card_number, + "user_id": user_id, + "amount": int(amount), + "status": "in_progress" + } + save(transactions_db) + request_data = { + "code": 200, + "message": f"Payment requested to IsoCard number: {card_number}. Payment will be complete once user accepts this.", + "verification_code": verification_code + } + return request_data, 200 + else: return { + "code": 401, + "message": "Unable to authorize transaction." + }, 401 + except Exception as e: return { + "code": 500, + "message": f"Failed to process payment: {e}", + "exception": type(e).__name__ + }, 500 + +@app.route('/checkpayment', methods=["GET"]) +def checkpayment(): + try: + with open("database/isocard_transactions.json", 'r') as f: transactions_db = json.load(f) + args = request.args + verification_code = args.get("verificationcode") + if transactions_db[str(verification_code)]["status"] == "complete": + if currency.get_bank(transactions_db[str(verification_code)]["payer_id"]) < transactions_db[str(verification_code)]["amount"]: + del transactions_db[str(verification_code)] + return { + "code": 403, + "message": "Transaction terminated: Insufficient payer balance.", + "exception": "InsufficientFunds" + }, 403 + currency.bank_remove(transactions_db[str(verification_code)]["payer_id"], transactions_db[str(verification_code)]["amount"]) + currency.bank_add(transactions_db[str(verification_code)]["merchant_id"], transactions_db[str(verification_code)]["amount"]) + del transactions_db[str(verification_code)] + save(transactions_db) + return { + "code": 200, + "message": "Transaction complete." + }, 200 + else: return { + "code": 202, + "message": "Transaction still not approved." + }, 202 + except KeyError: return { + "code": 404, + "message": "Verification code does not point to an active transaction.", + "exception": "TransactionNotFound" + }, 404 + except Exception as e: return { + "code": 500, + "message": f"Failed to process payment: {e}", + "exception": type(e).__name__ + }, 500 + +@app.route('/account', methods=["GET"]) +def account(): + try: + isocards = call_isocards_database() + args = request.args + isocard_number = args.get("cardnumber") + ssc = args.get("ssc") + if isocards[str(isocard_number)]["ssc"] == ssc: + card_data = isocards[str(isocard_number)] + del card_data["config"] + del card_data["ssc"] + card_data["account_balance"] = currency.get_wallet(card_data["user_id"]) + return { + "code": 200, + "card_info": card_data + }, 200 + else: return { + "code": 403, + "message": "Incorrect IsoCard SSC.", + "exception": "InvalidSSC" + } + except KeyError: return { + "code": 404, + "message": "Card number is invalid.", + "exception": "InvalidIsoCard" + }, 404 + except Exception as e: return { + "code": 500, + "message": f"Failed to fetch account info: {e}", + "exception": type(e).__name__ + } + +# Initialization +def run(): app.run(host="0.0.0.0", port=4800) + +if auth.get_runtime_options()["isocard_server_enabled"]: # Run server ONLY if its runtime option is enabled + print("[isocard/server] Starting IsoCard payments server...") + t = Thread(target=run) + t.daemon = True + t.start() + + +#btw i use arch diff --git a/main.py b/main.py index b0f8d79..a62f574 100644 --- a/main.py +++ b/main.py @@ -11,7 +11,7 @@ from utils import logger, ping from math import floor from random import randint -from framework.isobot import currency, colors, settings, commands as _commands +from framework.isobot import currency, colors, settings, commands as _commands, isocard from framework.isobot.shop import ShopData from framework.isobot.db import levelling, items, userdata, automod, weather, warnings, presence as _presence, serverconfig, embeds from discord import ApplicationContext, option @@ -54,6 +54,7 @@ def initial_setup(): "user_data", "weather", "embeds", + "isocard_transactions", "isobank/accounts", "isobank/auth" )