Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Paydisini Integration #309

Merged
merged 24 commits into from
Sep 14, 2024
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ FROM yasirarism/misskaty-docker:py3.12
ENV HOSTNAME=yasir-server
# Copy Files
COPY . .
# Instal pip package
# Instal pip package if you use free depedencies
# RUN pip3 install --no-cache-dir -r requirements.txt
# Set CMD Bot
CMD ["bash", "start.sh"]
16 changes: 16 additions & 0 deletions database/payment_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from database import dbname
from typing import Optional

autopay = dbname["autpay"]


async def delete_autopay(uniqueCode: str):
await autopay.delete_one({"_id": uniqueCode})

async def get_autopay(self, uniqueCode: str):
exists = await autopay.find_one({"_id": uniqueCode})
return exists
Comment on lines +11 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)

Suggested change
exists = await autopay.find_one({"_id": uniqueCode})
return exists
return await autopay.find_one({"_id": uniqueCode})


async def autopay_update(self, msg_id: Optional[int] = "", note: Optional[str] = "", user_id: Optional[int] = "", amount: Optional[int] = "", status: Optional[str] = "", uniqueCode: Optional[str] = "", createdAt: Optional[str] = ""):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Improve type hints and parameter handling

The type hints for integer fields (msg_id, user_id, amount) default to empty strings, which is inconsistent. Consider using None as the default value for these Optional fields. Also, validate and sanitize these inputs before using them in database operations to prevent potential injection attacks.

async def autopay_update(
    self,
    uniqueCode: str,
    msg_id: Optional[int] = None,
    note: Optional[str] = None,
    user_id: Optional[int] = None,
    amount: Optional[int] = None,
    status: Optional[str] = None,
    createdAt: Optional[str] = None
):
    data = {k: v for k, v in locals().items() if k != 'self' and v is not None}
    await autopay.update_one({"_id": uniqueCode}, {"$set": data}, upsert=True)

data = {"msg_id": msg_id, "note": note, "user_id": user_id, "amount": amount, "status": status, "createdAt": createdAt}
await autopay.update_one({"_id": uniqueCode}, {"$set": data}, upsert=True)
9 changes: 8 additions & 1 deletion misskaty/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@
from faulthandler import enable as faulthandler_enable
from logging import ERROR, INFO, StreamHandler, basicConfig, getLogger, handlers

import uvloop
import uvloop, uvicorn
from apscheduler.jobstores.mongodb import MongoDBJobStore
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from async_pymongo import AsyncClient
from pymongo import MongoClient
from pyrogram import Client
from web.webserver import api

from misskaty.vars import (
API_HASH,
API_ID,
BOT_TOKEN,
DATABASE_NAME,
DATABASE_URI,
PORT,
TZ,
USER_SESSION,
)
Expand Down Expand Up @@ -83,6 +85,11 @@
}
scheduler = AsyncIOScheduler(jobstores=jobstores, timezone=TZ)

async def run_wsgi():
config = uvicorn.Config(api, host="0.0.0.0", port=int(PORT))
server = uvicorn.Server(config)
await server.serve()

app.start()
BOT_ID = app.me.id
BOT_NAME = app.me.first_name
Expand Down
3 changes: 2 additions & 1 deletion misskaty/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
app,
get_event_loop,
scheduler,
run_wsgi
)
from misskaty.plugins import ALL_MODULES
from misskaty.plugins.web_scraper import web
Expand Down Expand Up @@ -56,7 +57,6 @@ async def start_bot():
LOGGER.info(bot_modules)
LOGGER.info("+===============+===============+===============+===============+")
LOGGER.info("[INFO]: BOT STARTED AS @%s!", BOT_USERNAME)

try:
LOGGER.info("[INFO]: SENDING ONLINE STATUS")
for i in SUDO:
Expand All @@ -73,6 +73,7 @@ async def start_bot():
except Exception as e:
LOGGER.error(str(e))
scheduler.start()
asyncio.create_task(run_wsgi())
if "web" not in await dbname.list_collection_names():
webdb = dbname["web"]
for key, value in web.items():
Expand Down
46 changes: 45 additions & 1 deletion misskaty/plugins/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import io
import json
import os
import hashlib
import pickle
import platform
import privatebinapi
import re
import secrets
import sys
import traceback
from datetime import datetime
Expand All @@ -22,6 +24,7 @@
import requests
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from bs4 import BeautifulSoup
from urllib.parse import quote
from PIL import Image, ImageDraw, ImageFont
from psutil import Process, boot_time, cpu_count, cpu_percent
from psutil import disk_usage as disk_usage_percent
Expand All @@ -44,6 +47,7 @@
LabeledPrice,
Message,
PreCheckoutQuery,
WebAppInfo,
)

from database.gban_db import add_gban_user, is_gbanned_user, remove_gban_user
Expand All @@ -55,7 +59,8 @@
from misskaty.helper.http import fetch
from misskaty.helper.human_read import get_readable_file_size, get_readable_time
from misskaty.helper.localization import use_chat_lang
from misskaty.vars import AUTO_RESTART, COMMAND_HANDLER, LOG_CHANNEL, SUDO
from database.payment_db import autopay_update
from misskaty.vars import AUTO_RESTART, COMMAND_HANDLER, LOG_CHANNEL, SUDO, PAYDISINI_CHANNEL_ID, PAYDISINI_KEY

__MODULE__ = "DevCommand"
__HELP__ = """
Expand Down Expand Up @@ -179,6 +184,45 @@ async def log_file(_, ctx: Message, strings):
else:
await msg.edit_msg("Unsupported parameter")

@app.on_message(filters.command(["payment"], COMMAND_HANDLER))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring the 'payment' function into smaller, focused helper functions.

The payment function introduces significant complexity by handling multiple responsibilities within a single function. To improve readability and maintainability, consider breaking it down into smaller, focused helper functions. Here's a suggested refactoring:

  1. API Call: Extract the API interaction into a separate function.
async def create_payment(api_url, api_key, unique_id, service_id, amount, valid_time):
    params = {
        'key': api_key,
        'request': 'new',
        'unique_code': unique_id,
        'service': service_id,
        'amount': amount,
        'note': 'MissKaty Support by YS Dev',
        'valid_time': valid_time,
        'type_fee': '1',
        'payment_guide': True,
        'signature': hashlib.md5((api_key + unique_id + service_id + amount + valid_time + 'NewTransaction').encode()).hexdigest(),
        'return_url': f'https://t.me/{client.me.username}?start'
    }
    return await fetch.post(api_url, data=params)
  1. QR Code Generation: Isolate the QR code generation logic.
def generate_qr_code(data):
    return f"https://api.qrserver.com/v1/create-qr-code/?size=300x300&data={quote(data)}"
  1. Database Update: Separate the database update logic.
async def update_payment_db(msg_id, note, user_id, amount, status, unique_code, created_at):
    await autopay_update(msg_id, note, user_id, amount, status, unique_code, created_at)

By organizing the code in this way, the main payment function will be more concise and focused, improving both readability and maintainability.

async def payment(client: Client, message: Message):
api_url = 'https://api.paydisini.co.id/v1/'
api_key = PAYDISINI_KEY
unique_id = f"VIP-{secrets.token_hex(5)}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Consider using a longer token for the unique ID

While using secrets.token_hex() is good for security, a 5-byte token might not provide sufficient uniqueness for a payment ID. Consider using a longer token, such as secrets.token_hex(16), to reduce the risk of collisions.

Suggested change
unique_id = f"VIP-{secrets.token_hex(5)}"
unique_id = f"VIP-{secrets.token_hex(16)}"

amount = "10000" if len(message.command) == 1 else str(message.command[1])
id_ = message.from_user.id if message.chat.id != message.from_user.id else message.chat.id
valid_time = str(5*60)
service_id = PAYDISINI_CHANNEL_ID

params = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Replace f-string with no interpolated values with string (remove-redundant-fstring)

'key': api_key,
'request': 'new',
'unique_code': unique_id,
'service': service_id,
'amount': amount,
'note': f'MissKaty Support by YS Dev',
'valid_time': valid_time,
'type_fee': '1',
'payment_guide': True,
'signature': hashlib.md5((api_key + unique_id + service_id + amount + valid_time + 'NewTransaction').encode()).hexdigest(),
'return_url': f'https://t.me/{client.me.username}?start'
}
# if id_ in user_data and user_data[id_].get("is_auth"):
# return await message.reply("Already Authorized!")
rget = await fetch.post(api_url, data=params)
if rget.status_code != 200:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Enhance error handling and logging

The current error handling is quite basic. Consider implementing more robust error handling and logging. For example, log the specific error message and status code, and provide more informative error messages to the user based on different error scenarios.

    if rget.status_code != 200:
        error_message = f"API Error: Status code {rget.status_code}"
        logging.error(f"{error_message}. Response: {rget.text}")
        user_message = "An error occurred while processing your request. Please try again later."
        return await message.reply(user_message)

return await message.reply("ERROR: Maybe your IP is not whitelisted or have another error from api.")
res = rget.json()
if not res.get("success"):
return await message.reply(res["msg"])
qr_photo = f"https://api.qrserver.com/v1/create-qr-code/?size=300x300&data={quote(res['data']['qr_content'])}"
capt = f"𝗠𝗲𝗻𝘂𝗻𝗴𝗴𝘂 𝗽𝗲𝗺𝗯𝗮𝘆𝗮𝗿𝗮𝗻\nKode: {res['data']['unique_code']}\nNote: {res['data']['note']}\nHarga: {res['data']['amount']}\nFee: {res['data']['fee']}\nExpired: {res['data']['expired']}\n\n"
payment_guide = f"<b>{res['payment_guide'][0]['title']}:</b>\n" + "\n".join(f"{i+1}. {step}" for i, step in enumerate(res["payment_guide"][0]['content']))
if message.chat.type.value != "private":
msg = await message.reply_photo(qr_photo, caption=capt+payment_guide, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="Payment Web", url=res["data"]["checkout_url_v2"])]]), quote=True)
else:
msg = await message.reply_photo(qr_photo, caption=capt+payment_guide, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="Payment Web", web_app=WebAppInfo(url=res["data"]["checkout_url_v2"]))]]), quote=True)
await autopay_update(msg.id, res["data"]["note"], id_, res['data']['amount'], res['data']['status'], res['data']['unique_code'], res['data']['created_at'])

@app.on_message(filters.command(["donate"], COMMAND_HANDLER))
async def donate(self: Client, ctx: Message):
Expand Down
3 changes: 3 additions & 0 deletions misskaty/vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
USER_SESSION = environ.get("USER_SESSION")
DATABASE_NAME = environ.get("DATABASE_NAME", "MissKatyDB")
TZ = environ.get("TZ", "Asia/Jakarta")
PORT = environ.get("PORT", 80)
COMMAND_HANDLER = environ.get("COMMAND_HANDLER", "! /").split()
SUDO = list(
{
Expand All @@ -64,6 +65,8 @@
AUTO_RESTART = environ.get("AUTO_RESTART", False)
OPENAI_KEY = environ.get("OPENAI_KEY")
GOOGLEAI_KEY = environ.get("GOOGLEAI_KEY")
PAYDISINI_KEY = environ.get("PAYDISINI_KEY")
PAYDISINI_CHANNEL_ID = environ.get("PAYDISINI_CHANNEL_ID", "17")

## Config For AUtoForwarder
# Forward From Chat ID
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ GitPython
aiofiles
uvloop==0.19.0
lxml_html_clean
fastapi
uvicorn
python-multipart
98 changes: 98 additions & 0 deletions web/webserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from logging import INFO, StreamHandler, basicConfig, getLogger, ERROR, handlers
from os import path
from time import time
from datetime import datetime, timedelta

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse
from starlette.exceptions import HTTPException
from psutil import boot_time, disk_usage, net_io_counters
from contextlib import suppress
from asyncio import to_thread, subprocess, create_subprocess_shell
from apscheduler.triggers.date import DateTrigger
import hashlib

api = FastAPI()

basicConfig(
level=INFO,
format="[%(levelname)s] - [%(asctime)s - %(name)s - %(message)s] -> [%(module)s:%(lineno)d]",
datefmt="%d-%b-%y %H:%M:%S",
handlers=[
handlers.RotatingFileHandler(
"MissKatyLogs.txt", mode="w+", maxBytes=5242880, backupCount=1
),
StreamHandler(),
],
)
botStartTime = time()

LOGGER = getLogger(__name__)
getLogger("fastapi").setLevel(ERROR)

@api.post("/callback")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Enhance security measures for payment callback

While you're verifying the IP and signature, consider adding additional security measures such as rate limiting, HTTPS enforcement, and possibly a webhook secret. Also, ensure that all sensitive data is properly sanitized before use.

@api.post("/callback", dependencies=[Depends(RateLimiter(times=10, seconds=60))])
@requires_https
async def autopay(request: Request, signature: str = Header(None)):
    if not verify_signature(signature, request):
        raise HTTPException(status_code=403, detail="Invalid signature")

async def autopay(request: Request):
from misskaty import app
from database.payment_db import delete_autopay, get_autopay
from misskaty.vars import PAYDISINI_KEY, OWNER_ID
data = await request.form()
client_ip = request.client.host
if PAYDISINI_KEY != data["key"] and client_ip != "84.247.150.90":
raise HTTPException(status_code=403, detail="Access forbidden")
signature_data = f"{PAYDISINI_KEY}{data['unique_code']}CallbackStatus"
gen_signature = hashlib.md5(signature_data.encode()).hexdigest()
if gen_signature != data["signature"]:
raise HTTPException(status_code=403, detail="Invalid Signature")
unique_code = data['unique_code']
status = data['status']
exp_date = (datetime.now(jkt) + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
r = await get_autopay(unique_code)
msg = f"╭────〔 <b>TRANSAKSI SUKSES🎉</b> 〕──\n│・ <b>Transaksi ID :</b> {unique_code}\n│・ <b>Product :</b> MissKaty Support by YS Dev\n│・ <b>Durasi :</b> 30 hari\n│・ <b>Total Dibayar :</b> {r.get('amount')}\n│・ Langganan Berakhir: {exp_date}\n╰─────────"
if not r:
return JSONResponse({"status": false, "data": "Data not found on DB"}, 404)
if status == "Success":
with suppress(Exception):
await bot.send_message(r.get("user_id"), f"{msg}\n\nJika ada pertanyaan silahkan hubungi pemilik bot ini.")
await bot.delete_messages(r.get("user_id"), r.get("msg_id"))
await bot.send_message(OWNER_ID, msg)
await delete_autopay(unique_code)
return JSONResponse({"status": status, "msg": "Pesanan berhasil dibayar oleh customer."}, 200)
else:
with suppress(Exception):
await bot.send_message(r.get("user_id"), "QRIS Telah Expired, Silahkan Buat Transaksi Baru.")
await bot.delete_messages(r.get("user_id"), r.get("msg_id"))
await delete_autopay(unique_code)
return JSONResponse({"status": status, "msg": "Pesanan telah dibatalkan/gagal dibayar."}, 403)

@api.get("/status")
async def status():
from misskaty.helper.human_read import get_readable_file_size, get_readable_time
bot_uptime = get_readable_time(time() - botStartTime)
uptime = get_readable_time(time() - boot_time())
sent = get_readable_file_size(net_io_counters().bytes_sent)
recv = get_readable_file_size(net_io_counters().bytes_recv)
if path.exists(".git"):
commit_date = (await (await create_subprocess_shell("git log -1 --date=format:'%y/%m/%d %H:%M' --pretty=format:'%cd'", stdout=subprocess.PIPE, stderr=subprocess.STDOUT)).communicate())[0].decode()
else:
commit_date = "No UPSTREAM_REPO"
return {
"commit_date": commit_date,
"uptime": uptime,
"on_time": bot_uptime,
"free_disk": get_readable_file_size(disk_usage(".").free),
"total_disk": get_readable_file_size(disk_usage(".").total),
"network": {
"sent": sent,
"recv": recv,
},
}


@api.api_route("/")
async def homepage():
return "Hello World"


@api.exception_handler(HTTPException)
async def page_not_found(request: Request, exc: HTTPException):
return HTMLResponse(content=f"<h1>Error: {exc}</h1>", status_code=exc.status_code)
Loading