Skip to content
This repository has been archived by the owner on Dec 2, 2024. It is now read-only.

Commit

Permalink
Refactor to merge go into branch
Browse files Browse the repository at this point in the history
  • Loading branch information
showierdata9978 committed Aug 15, 2024
1 parent fad80e5 commit 49ffc26
Show file tree
Hide file tree
Showing 29 changed files with 276 additions and 106 deletions.
File renamed without changes.
File renamed without changes.
150 changes: 150 additions & 0 deletions python/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
This module connects the API to the websocket server.
"""

from typing import Any

import msgpack

from database import rdb, db
from supporter import Supporter

OpCreateUser = 0
OpUpdateUser = 1
OpDeleteUser = 2
OpUpdateUserSettings = 3

OpRevokeSession = 4

OpUpdateRelationship = 5

OpCreateChat = 6
OpUpdateChat = 7
OpDeleteChat = 8

OpCreateChatMember = 9
OpUpdateChatMember = 10
OpDeleteChatMember = 11

OpCreateChatEmote = 12
OpUpdateChatEmote = 13
OpDeleteChatEmote = 14

OpTyping = 15

OpCreatePost = 16
OpUpdatePost = 17
OpDeletePost = 18
OpBulkDeletePosts = 19

OpPostReactionAdd = 20
OpPostReactionRemove = 21

class Events:
def __init__(self):
# noinspection PyTypeChecker
self.supporter: Supporter = None

def add_supporter(self, supporter: Supporter):
self.supporter = supporter

def parse_post_meowid(self, post: dict[str, Any], include_replies: bool = True):
post = list(self.supporter.parse_posts_v0([post], include_replies=include_replies, include_revisions=False))[0]

match post["post_origin"]:
case "home":
chat_id = 0
case "livechat":
chat_id = 1
case "inbox":
chat_id = 2
case _:
chat_id = db.get_collection("chats").find_one({"_id": post["post_origin"]}, projection={"meowid": 1})[
"meowid"]

replys = []
if include_replies:
replys = [reply["meowid"] for reply in post["reply_to"]]

return {
"id": post["meowid"],
"chat_id": chat_id,
"author_id": post["author"]["meowid"],
"reply_to_ids": replys,
"emoji_ids": [emoji["id"] for emoji in post["emojis"]],
"sticker_ids": post["stickers"],
"attachments": post["attachments"],
"content": post["p"],
"reactions": [{
"emoji": reaction["emoji"],
"count": reaction["count"]
} for reaction in post["reactions"]],
"last_edited": post.get("edited_at", 0),
"pinned": post["pinned"]
}

@staticmethod
def parse_user_meowid(partial_user: dict[str, Any]):
quote = db.get_collection("usersv0").find_one({"_id": partial_user["_id"]}, projection={"quote": 1})["quote"]
return {
"id": partial_user["meowid"],
"username": partial_user["_id"],
"flags": partial_user["flags"],
"avatar": partial_user["avatar"],
"legacy_avatar": partial_user["pfp_data"],
"color": partial_user["avatar_color"],
"quote": quote
}

def send_post_event(self, original_post: dict[str, Any]):
post = self.parse_post_meowid(original_post, include_replies=True)

users = [self.parse_user_meowid(post["author"])]

replies = {}
for reply in post["reply_to_ids"]:
replies[reply] = self.parse_post_meowid(db.get_collection("posts").find_one({"meowid": reply}),
include_replies=False)
users.append(self.parse_user_meowid(replies[reply]["author"]))

emotes = {}
for emoji in post["emoji_ids"]:
emotes[emoji["_id"]] = {
"id": emoji["_id"],
"chat_id": db.get_collection("chats").find_one({"_id": emoji["chat_id"]}, projection={"meowid": 1})[
"meowid"],
"name": emoji["name"],
"animated": emoji["animated"],
}

data = {
"post": post,
"reply_to": replies,
"emotes": emotes,
"attachments": original_post["attachments"],
"author": users,
}

is_dm = db.get_collection("chats").find_one({"_id": original_post["post_origin"], "owner": None},
projection={"meowid": 1})
if is_dm:
data["dm_to"] = db.get_collection("users") \
.find_one({"_id": original_post["author"]["_id"]}, projection={"meowid": 1}) \
["meowid"]

data["dm_chat"] = None # unspecifed

if "nonce" in original_post:
data["nonce"] = original_post["nonce"]

self.send_event(OpCreatePost, data)

@staticmethod
def send_event(event: int, data: dict[str, any]):
payload = bytearray(msgpack.packb(data))
payload.insert(0, event)

rdb.publish("events", payload)


events = Events()
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions main.py → python/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Load .env file
from dotenv import load_dotenv


load_dotenv()

import asyncio
Expand All @@ -13,6 +15,7 @@
from security import background_tasks_loop
from grpc_auth import service as grpc_auth
from rest_api import app as rest_api
from events import events


if __name__ == "__main__":
Expand All @@ -23,6 +26,8 @@
supporter = Supporter(cl)
cl.supporter = supporter

events.add_supporter(supporter)

# Start background tasks loop
Thread(target=background_tasks_loop, daemon=True).start()

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
121 changes: 121 additions & 0 deletions python/rest_api/v0/bots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import os
import uuid
from typing import TYPE_CHECKING

import requests
from quart import Blueprint, current_app as app, request, abort
from quart_schema import validate_request
from pydantic import BaseModel

import security
import secrets

from database import db

if TYPE_CHECKING:
class Reqest:
user: str
flags: int
permissions: int

request: Reqest

from cloudlink import CloudlinkServer
from supporter import Supporter
class App:
supporter: "Supporter"
cl: "CloudlinkServer"


app: App

bots_bp = Blueprint("bots", __name__, url_prefix="/bots")

class CreateBot(BaseModel):
name: str
description: str
captcha: str

class SecureRequests(BaseModel):
mfa_code: int

@bots_bp.post("/")
@validate_request(CreateBot)
async def create_bot(data: CreateBot):
if not request.user:
abort(401)

if os.getenv("CAPTCHA_SECRET") and not (hasattr(request, "bypass_captcha") and request.bypass_captcha):
if not requests.post("https://api.hcaptcha.com/siteverify", data={
"secret": os.getenv("CAPTCHA_SECRET"),
"response": data.captcha,
}).json()["success"]:
return {"error": True, "type": "invalidCaptcha"}, 403

if not (security.ratelimited(f"bot:create:{request.user}")):
abort(429)


if any([
db.users.find_one({"lower_username": data.name.lower()}),
db.bots.find_one({"name": data.name})
]):
return {"error": True, "type": "nameTaken"}, 409

token = secrets.token_urlsafe(32)

bot = {
"_id": str(uuid.uuid4()),
"token": security.hash_password(token),
"name": data.name,
"description": data.description,
"owner": request.user,
"avatar": {
"default": 1,
"custom": None
}
}

db.bots.insert_one(bot)
security.ratelimit(f"bot:create:{request.user}", 1, 60)

bot["token"] = token
bot["error"] = False

return bot, 200


@bots_bp.get("/")
async def get_bots():
if not request.user:
abort(401)

bots = list(db.bots.find({"owner": request.user}, projection={"_id": 1, "name": 1, "avatar": 1}))
return {"error": False, "bots": bots}, 200

@bots_bp.get("/<bot_id>")
async def get_bot(bot_id: str):
if not request.user:
abort(401)

bot = db.bots.find_one({"_id": bot_id, "owner": request.user}, projection={"token": 0})
if not bot:
abort(404)

return {"error": False, "bot": bot}, 200

@bots_bp.delete("/<bot_id>")
@validate_request(SecureRequests)
async def delete_bot(bot_id: str, data: SecureRequests):
if not request.user:
abort(401)

bot = db.bots.find_one({"_id": bot_id, "owner": request.user})
if not bot:
abort(404)

if not security.check_mfa(request.user, data.mfa_code):
return {"error": True, "type": "invalidMfa"}, 403

db.bots.delete_one({"_id": bot_id})
return {"error": False}, 200
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 49ffc26

Please sign in to comment.