From f05dda95c3f4ee0f5feea7f49b14c9e469bdb59c Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 4 Oct 2025 20:08:48 +0200 Subject: [PATCH] +Upload --- .gitignore | 4 + client/main.py | 15 +++ client/requirements.txt | 2 + pycord.db | Bin 0 -> 28672 bytes server/__pycache__/crud.cpython-313.pyc | Bin 0 -> 1542 bytes server/__pycache__/database.cpython-313.pyc | Bin 0 -> 876 bytes server/__pycache__/models.cpython-313.pyc | Bin 0 -> 1326 bytes server/__pycache__/schemas.cpython-313.pyc | Bin 0 -> 1148 bytes server/__pycache__/security.cpython-313.pyc | Bin 0 -> 709 bytes server/config.py | 6 + server/crud.py | 45 ++++++++ server/database.py | 13 +++ server/main.py | 120 ++++++++++++++++++++ server/models.py | 27 +++++ server/requirements.txt | 7 ++ server/schemas.py | 41 +++++++ server/security.py | 50 ++++++++ 17 files changed, 330 insertions(+) create mode 100644 .gitignore create mode 100644 client/main.py create mode 100644 client/requirements.txt create mode 100644 pycord.db create mode 100644 server/__pycache__/crud.cpython-313.pyc create mode 100644 server/__pycache__/database.cpython-313.pyc create mode 100644 server/__pycache__/models.cpython-313.pyc create mode 100644 server/__pycache__/schemas.cpython-313.pyc create mode 100644 server/__pycache__/security.cpython-313.pyc create mode 100644 server/config.py create mode 100644 server/crud.py create mode 100644 server/database.py create mode 100644 server/main.py create mode 100644 server/models.py create mode 100644 server/requirements.txt create mode 100644 server/schemas.py create mode 100644 server/security.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c469d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + + +#Ignore vscode AI rules +.github\instructions\codacy.instructions.md diff --git a/client/main.py b/client/main.py new file mode 100644 index 0000000..85448c7 --- /dev/null +++ b/client/main.py @@ -0,0 +1,15 @@ +import customtkinter + +class App(customtkinter.CTk): + def __init__(self): + super().__init__() + + self.title("Pycord Client") + self.geometry("800x600") + + self.label = customtkinter.CTkLabel(self, text="Willkommen bei Pycord!", font=("Roboto", 24)) + self.label.pack(pady=20) + +if __name__ == "__main__": + app = App() + app.mainloop() diff --git a/client/requirements.txt b/client/requirements.txt new file mode 100644 index 0000000..eaa008b --- /dev/null +++ b/client/requirements.txt @@ -0,0 +1,2 @@ +customtkinter +websockets diff --git a/pycord.db b/pycord.db new file mode 100644 index 0000000000000000000000000000000000000000..9cca31b72f74cdf7e4fe99b301f8aef6d8774fb3 GIT binary patch literal 28672 zcmeI(L64I_6u@!VZE2RIyWTi(nZy`NH15H8m#wqAXr;SsYiv9;7%)rXwg#%(+om4$ zBO1Sr3_?nq^b3?f35A(?yw3YgFB8(6>p`-J#g}<{7cRt0?UAPI+G`;+O*8Vdlb_Y0 z+O4XQU-jDku3JNU@yqGvqel0)cCtLNkN+G$>Hf_d90(wQ00IagfB*srAb>zK1+EOk zdS>fMGK=D6vYcjd`Xf%W=`6g9%ll40knTtd&wnF7imezi^hFsbdg>wPZQHP(+IqO1 zF^S4JV^_W;+Q^nf&g!()v20ua`Dzh<{#s3_%+oIOjNG$sNM6W5_`{L# z$AiIhVIHa3i4ShjzjOoDp5M>nbh-{!eOH0^-VG+=ot%hXJ^|HtJ`ALH;j6x0^^79| zc`gI#_vMZFo=+$%!cFJw(6Am``lPD97|Xw;xVE+{uSQkf>nxuR4C~a=A2xHSaaBSQ zcUWc```-1qwlx0}R-v`DRhP4B>mAFo^i@)j77r932OFG4m+0-ptn$@CZ^P_1j;7zj zEW4klQGICJ)hzbr`KF%(0R#|0009ILKmY**5I_I{1ol|KRJF1H-{TdgGz1Vp009IL zKmY**5I_I{1T>BPKfeM55I_I{1Q0*~0R#|0009K{UqJmgpZEX!zs^*M00IagfB*sr zAb&2Jk;6rb50?{56nIw9su32tpsZJ>sRmO@m8SgiuswTo;FhpdP;>ve2od(F(c z)HhR*AmUIsR1|>(2M+BCAtBKt{tseV!e(g>2yv*y!BybU8*kQGyF?Wu?Y!T-_vX#a zd%w5)p^zWJ5a0Q|(iRZ64#2(*;5sUx0?wc`rZ~p2kP*@%6W z-_Mbmq1SZE9ACLgnXQVHKWc1~sL5J2oADIHVlk&+tY|#jOv_ta)rOX8HZSP5u3fYs zhV}-vwy9ocTDEFXi&oiJrJgz7O3B^`kF#F5L;w-nj5h_RH+;S9;-0=hY#RK3@M|{gbmFT^jm>*VG;L z(`N=`ewWPi<p7iX&KfqgE6fK^5Q-XN$;M;5p`j(mZd$aHJH`7ZdR3#;Oa#3jTckLY9)^oS;Dk-WfVT|!IN1x?nJ zyvP+TstZ>HEjAAKBp$8AGl`QReMa!lTAZ35HB7hZSO#?}wnKfj5Lg;@U7I->;;U{5 z>*jsRAEw*~HQS??O`C;iK9|d7bB(6Ocqv=j-oyxrT2*!v^UGGvbX~(Ap?eLRQ-e8% z#p-q2^JPxG9qw3zIr~b%T@(O%T?7{(jf3AsVnUf6XadVbgQ^IyOFCjPqJjfkFemLo zhxCa-a%M3}Aj`u4o@uB+6pG`xl=Cnh{+CWOML~s2VIY*SZW)PAVHZKvRWSu+GAEWv zh7@taWXRZn89Y!1&C-?dXL@W6AVPeT`XWO<(7%!p*v?-?A;v zTvqndx3%;lD^We<(XeXFp*kkmrCcvz)=S zXgzMdm^vC7-B11&VI+0X3cvl#)HoLBfN$lLg2~3H@Z+~rJWkR#z)Z%*zC9cLnMTJg{3br^=){j ttsciFy85B?F*e(lKaETrN%1EOhZlc}!Wp5R{1Fw%Sa;zUpyKB+#b4Y}-F^T7 literal 0 HcmV?d00001 diff --git a/server/__pycache__/models.cpython-313.pyc b/server/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..126339885729c04da8c86bd4b527db9ee0499812 GIT binary patch literal 1326 zcmb7DOK%%h6uvW_mtQe0PMWqrO(a0>A}d_cs;Wp*5I7{DPMXz7SL5-GJ?ePIId|OF zU0JGxx{QP>m0gx(*9{W?fmM_Zy*rClc1RBDmUCvDQUn{u@;B!-=YID*X4Ptm;q&Xp zwEwft*zcsAKjvJR?V<3Txy;ozSz80Gt%Ke!K%s4b(IC6NX|^q}+BVn?=GiN(HUAWx zWwyfH!WHHkp3^GiW+-M>6Rp-;)>6-II;RxUG?cZGM595h?62cg^aLo&O(BeXs`O<7 zBJ9PRV%*eK1w`bhVG_%JI8^U+MG*ObhKGW8d?|uCkL+d5mDw%|&zZ+Sa~bHa1_f6K z!!uWyXSoIZ88-`HXS$Q=ChEmpZ)tfJD0yYPndRE9b5nu!MQ4NK1jNwh~x_A`y)S^dup8Tg&@-?j76NFyUKa$cOsGdfw}+& zovDXipe`$in37x7`o8RoE+6_*K1!eqHu?A5DN41-xlFOj0rykvN@s-pIEO_H zo#CIU`@t=b>L9&)NhgfGwc+rtpZeZ6N#IA`caZeJA4qR23?PxoUfS4o8yiVic*Ajk zhw>mwV&Ub-d3a3`$<^T)XjU_z^2*N;zGuJHuTHk6#oB>=xbw1pT)Z(^e^pyN*vquq zjmcN9mfm}@`$IhWc3N*7F8}h$aee*i7BtRN{r8A;(EUF~#KaNE2gLG9G#2M$(!!;p z2g+Ur?KmuM5o4=v2^M#E)!fKc++OMN@~_;gL(|K2+GOkjALTzFkB?KMnH-4@_a}V5zK%{$~y>D z^Y{N%^YVp*j}MLG@<)@qRR7+OPky?7a;9albo`bD0avTozHl;nQo zM?qf<#%~cOt~twvzx}HT405gxx|Okx`<)jhqxetS%DJ6y#~rwi(X_PLy!=tnH0`x+ tYWj>Jysj|q*3`aqVqcyZXNIk9XwN=9WAr||XlrZQvz8&6lf~_EaG)TAhG%`#!+jL>G6W?qqxd}oo zc=6IdqJNKrc=cuP8~(r2X1v@p zVYG$95%ZbPpRuPLc%9YQE#_NG%(n$!vkO14a;<|}S6Js-JJ*&_TmD1qbi77oNP!KW zRo0aWH+4IVgCbRdXL;GEj{74f7;v9~<#VuCm~e#aTes2T+feE_4L9>%nh+?}kRmg$ zkK06b!(-MwE@z&Uw2?BaO4-%zeoXSTl&||LKC{e9xf_8=<0uIejb_`VkfBy3iB44- z>BOLRQA$I*K&mu_sMSwHRD*}CBm7vl6WkM2j1gP96(!r05^CT^ffg)hx+(`O~Y8+J18m%Mz zyPu+r-F2wD`SiFrJ*72^f{EWRLxtuibEw`S6_r_0dpoN{Myeo<_QMHcp2s0^4TVW! z-vS2*$jt|6iqfra=wvtQ3}?`HyEt~E1oTQ^ieQ?6jG3pQOOq7BWztPvOI9lKz%ts% z-B7y@*vZ1x4_P*gzPO?o-KL0cs_&_IJ#j9K2+Jm+5r^X?a~=K-vsGu^GZ mgYezvoPTD^rw-@yhXZ8c^QR?*kHko_%WH>se=_{#?7soEyzxE& literal 0 HcmV?d00001 diff --git a/server/__pycache__/security.cpython-313.pyc b/server/__pycache__/security.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a7af5829f187d10062533359820d6b3a811d968 GIT binary patch literal 709 zcma)3L2DC16rR~^(zI(6i&CMbut;0nBJHUMi$qF16tzXVR0UVUW_Fq^Y<8A66YJjk zCxjk*?LY8mH1r@0dMKW}NdumJv)u&oBnKHBif)_+ zyIJ-;RAKN+urLW;gXlpxkimY;KuB?{+&8}477-7!oFQ(9B_ibk0w6PPYzVQ+H?v$f zZ>3BLp^1d1+wvZ|Q}WGO{OYWXoRzcm%4uENtuclP!}(0j{Qti@b40ew+EoIxSm{|{ z#;$Gia~xBB(hF(wTxgibg&tQ^i5-&(_c30_^|#LDk+XcZcjc^I+G}tZ9}m2$ZW_QO z?rxUd2cxy%EdE*mU63OEmaP7CAfjQyTkrrgjSA_a`&FToekYHv8%yVV=R054M~&T6 P=f AsyncSession: + async with async_session() as session: + yield session \ No newline at end of file diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..ac8b439 --- /dev/null +++ b/server/main.py @@ -0,0 +1,120 @@ +import uvicorn +from contextlib import asynccontextmanager +from datetime import timedelta +from typing import List +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.ext.asyncio import AsyncSession + +from database import engine, get_session +from models import Base, User, Server +import crud +import schemas +import security +from config import ACCESS_TOKEN_EXPIRE_MINUTES + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # on startup + async with engine.begin() as conn: + # await conn.run_sync(Base.metadata.drop_all) + await conn.run_sync(Base.metadata.create_all) + yield + # on shutdown + # (nothing to do here for now) + + +app = FastAPI(lifespan=lifespan) + +# --- Authentication --- + +@app.post("/token", response_model=schemas.Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_session)): + user = await crud.authenticate_user(db, username=form_data.username, password=form_data.password) + if not user: + raise HTTPException( + status_code=401, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = security.create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + +# --- Users --- + +@app.post("/users/", response_model=schemas.User) +async def create_user(user: schemas.UserCreate, db: AsyncSession = Depends(get_session)): + db_user = await crud.get_user_by_username(db, username=user.username) + if db_user: + raise HTTPException(status_code=400, detail="Username already registered") + return await crud.create_user(db=db, user=user) + +@app.get("/users/me/", response_model=schemas.User) +async def read_users_me(current_user: User = Depends(security.get_current_user)): + return current_user + +# --- Servers --- + +@app.post("/servers/", response_model=schemas.Server) +async def create_server( + server: schemas.ServerCreate, + db: AsyncSession = Depends(get_session), + current_user: User = Depends(security.get_current_user) +): + return await crud.create_server(db=db, server=server, owner_id=current_user.id) + +@app.get("/servers/", response_model=List[schemas.Server]) +async def read_servers(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_session)): + servers = await crud.get_servers(db, skip=skip, limit=limit) + return servers + +@app.get("/servers/{server_id}", response_model=schemas.Server) +async def read_server(server_id: int, db: AsyncSession = Depends(get_session)): + db_server = await crud.get_server(db, server_id=server_id) + if db_server is None: + raise HTTPException(status_code=404, detail="Server not found") + return db_server + +# --- General --- + +@app.get("/") +async def read_root(): + return {"message": "Pycord server is running"} + +# --- WebSocket --- + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + +manager = ConnectionManager() + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + await manager.broadcast(f"Client #{client_id} says: {data}") + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.broadcast(f"Client #{client_id} left the chat") + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/server/models.py b/server/models.py new file mode 100644 index 0000000..c247dd5 --- /dev/null +++ b/server/models.py @@ -0,0 +1,27 @@ +from sqlalchemy import ( + Column, + Integer, + String, + ForeignKey, +) +from sqlalchemy.orm import relationship, declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + hashed_password = Column(String) + + owned_servers = relationship("Server", back_populates="owner") + +class Server(Base): + __tablename__ = "servers" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="owned_servers") \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..76d3608 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn[standard] +SQLAlchemy +aiosqlite +passlib[bcrypt] +python-jose[cryptography] +python-multipart \ No newline at end of file diff --git a/server/schemas.py b/server/schemas.py new file mode 100644 index 0000000..f4d7a72 --- /dev/null +++ b/server/schemas.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from typing import List + +# --- Token Schemas --- + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + username: str | None = None + +# --- Server Schemas --- + +class ServerBase(BaseModel): + name: str + +class ServerCreate(ServerBase): + pass + +class Server(ServerBase): + id: int + owner_id: int + + class Config: + orm_mode = True + +# --- User Schemas --- + +class UserBase(BaseModel): + username: str + +class UserCreate(UserBase): + password: str + +class User(UserBase): + id: int + owned_servers: List[Server] = [] + + class Config: + orm_mode = True diff --git a/server/security.py b/server/security.py new file mode 100644 index 0000000..1fa182a --- /dev/null +++ b/server/security.py @@ -0,0 +1,50 @@ +from datetime import datetime, timedelta +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.ext.asyncio import AsyncSession + +from config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES +import crud +from database import get_session +import schemas + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_session)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = schemas.TokenData(username=username) + except JWTError: + raise credentials_exception + user = await crud.get_user_by_username(db, username=token_data.username) + if user is None: + raise credentials_exception + return user