✨ Migrated to deta from imgur for image-storing
This commit is contained in:
Generated
+15
-15
@@ -291,16 +291,16 @@
|
||||
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"version": "==2.0.12"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6",
|
||||
"sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2"
|
||||
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
|
||||
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.0"
|
||||
"version": "==8.1.2"
|
||||
},
|
||||
"databases": {
|
||||
"hashes": [
|
||||
@@ -536,7 +536,7 @@
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"jinja2": {
|
||||
@@ -914,11 +914,11 @@
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:3cbe235cea80b9c9991b397567aa2d65eb4e6fb09787f61d227ae82eb4eb50b4",
|
||||
"sha256:6758d01dec81af191b98a35cce3402675d115456584c39b500ab485a5e386bbb"
|
||||
"sha256:69d05fac17bf3f43937afbb775c536eb516bd21355a4f17d59a966f4a531ce71",
|
||||
"sha256:fe45513881229dbee610620b9e0817b1f48c47ba635870320fd44a712204bbdd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.0"
|
||||
"version": "==4.2.1"
|
||||
},
|
||||
"rsa": {
|
||||
"hashes": [
|
||||
@@ -938,11 +938,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:8f4813dd6a4d6cc17bde85fb2e635fe19763f96efbb0ddf5575562e5ee0bc47a",
|
||||
"sha256:c3d4e2ab578fbf83775755cd76dae73627915a22832cf4ea5de895978767833b"
|
||||
"sha256:425ec0e0014c5bcc1104dd1099de6c8f0584854fc9a4f512575f5ed5ee399fb9",
|
||||
"sha256:6d59c30ce22dd583b42cacf51eebe4c6ea72febaa648aa8b30e5015d23a191fe"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==61.2.0"
|
||||
"version": "==61.3.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -957,7 +957,7 @@
|
||||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
@@ -1060,10 +1060,10 @@
|
||||
},
|
||||
"watchgod": {
|
||||
"hashes": [
|
||||
"sha256:4ba20c2fa3e63df706ab50e694b9453b05395fadb7cbbfd984d71fb1547d485d",
|
||||
"sha256:c12d15f3df7d11e740704e45398277f75f1d78f46ad59ca9d7505bfd8b8d3086"
|
||||
"sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce",
|
||||
"sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
"version": "==0.8.2"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
|
||||
@@ -4,7 +4,7 @@ from socketio import ASGIApp
|
||||
|
||||
from classquiz.db import database
|
||||
from classquiz.config import settings
|
||||
from classquiz.routers import users, quiz, utils, stats
|
||||
from classquiz.routers import users, quiz, utils, stats, storage
|
||||
from classquiz.socket_server import sio
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
|
||||
@@ -50,4 +50,5 @@ app.include_router(users.router, tags=["users"], prefix="/api/v1/users")
|
||||
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz")
|
||||
app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils")
|
||||
app.include_router(stats.router, tags=["stats"], prefix="/api/v1/stats")
|
||||
app.include_router(storage.router, tags=["storage"], prefix="/api/v1/storage")
|
||||
app.mount("/", ASGIApp(sio))
|
||||
|
||||
+7
-1
@@ -18,11 +18,17 @@ class Settings(BaseSettings):
|
||||
mail_server: str
|
||||
mail_port: int
|
||||
secret_key: str
|
||||
imgur_client_id: str = "b13fefc8bb1db87"
|
||||
imgur_client_id: str = "27aa1687703ddb9"
|
||||
access_token_expire_minutes: int = 30
|
||||
cache_expiry: int = 86400
|
||||
sentry_dsn: str | None = "https://4981de1f72f24fd7b5e21b8913b93a02@o661934.ingest.sentry.io/6254641"
|
||||
|
||||
# storage_backend
|
||||
storage_backend: str | None = "deta"
|
||||
# if storage_backend == "deta":
|
||||
deta_project_key: str
|
||||
deta_project_id: str
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = 'utf-8'
|
||||
|
||||
@@ -7,44 +7,51 @@ import html
|
||||
from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User
|
||||
from classquiz.kahoot_importer.get import get as get_quiz
|
||||
from classquiz.config import settings
|
||||
from classquiz.storage import Storage
|
||||
|
||||
settings = settings()
|
||||
|
||||
|
||||
async def _download_image(url: str) -> bytes:
|
||||
async with ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
return await resp.read()
|
||||
|
||||
|
||||
async def _upload_to_imgur(image: bytes, album_id: str | None) -> str:
|
||||
"""
|
||||
Uploads an image to Imgur.
|
||||
:param image: The image to upload.
|
||||
:return: The URL of the image on Imgur.
|
||||
"""
|
||||
|
||||
formdata = FormData()
|
||||
formdata.add_field('image', image, filename='image.png')
|
||||
if album_id:
|
||||
formdata.add_field('album', album_id)
|
||||
async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session:
|
||||
async with session.post("https://api.imgur.com/3/image", data=image) as resp:
|
||||
data = await resp.json()
|
||||
return data["data"]["link"]
|
||||
|
||||
|
||||
async def _generate_imgur_album(quiz_id: str) -> str:
|
||||
"""
|
||||
Generates an album on Imgur.
|
||||
:param quiz_id: The ID of the quiz.
|
||||
:return: The URL of the album on Imgur.
|
||||
"""
|
||||
formdata = FormData()
|
||||
formdata.add_field('title', quiz_id)
|
||||
async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session:
|
||||
async with session.post("https://api.imgur.com/3/album", data={"title": "Quiz: " + quiz_id}, ) as resp:
|
||||
data = await resp.json()
|
||||
return data["data"]["id"]
|
||||
|
||||
# async def _upload_to_imgur(image: bytes, album_id: str | None) -> str:
|
||||
# """
|
||||
# Uploads an image to Imgur.
|
||||
# :param image: The image to upload.
|
||||
# :return: The URL of the image on Imgur.
|
||||
# """
|
||||
#
|
||||
# formdata = FormData()
|
||||
# formdata.add_field('image', image, filename='image.png')
|
||||
# if album_id:
|
||||
# formdata.add_field('album', album_id)
|
||||
# async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session:
|
||||
# print(settings.imgur_client_id)
|
||||
# async with session.post("https://api.imgur.com/3/image", data=image) as resp:
|
||||
# data = await resp.json()
|
||||
# print(resp.headers)
|
||||
# try:
|
||||
# return data["data"]["link"]
|
||||
# except KeyError:
|
||||
# raise Exception(data)
|
||||
#
|
||||
#
|
||||
# async def _generate_imgur_album(quiz_id: str) -> str:
|
||||
# # """
|
||||
# # Generates an album on Imgur.
|
||||
# # :param quiz_id: The ID of the quiz.
|
||||
# # :return: The URL of the album on Imgur.
|
||||
# # """
|
||||
# formdata = FormData()
|
||||
# formdata.add_field('title', quiz_id)
|
||||
# async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session:
|
||||
# async with session.post("https://api.imgur.com/3/album", data={"title": "Quiz: " + quiz_id}, ) as resp:
|
||||
# data = await resp.json()
|
||||
# return data["data"]["id"]
|
||||
|
||||
async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
||||
"""
|
||||
@@ -57,20 +64,24 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
||||
if quiz is None:
|
||||
return "quiz not found"
|
||||
quiz_questions: list[dict] = []
|
||||
imgur_album_id = await _generate_imgur_album(quiz_id)
|
||||
quiz_id = uuid.uuid4()
|
||||
storage = Storage(backend=settings.storage_backend, deta_key=settings.deta_project_key,
|
||||
deta_id=settings.deta_project_id)
|
||||
|
||||
for q in quiz.kahoot.questions:
|
||||
answers: list[QuizAnswer] = []
|
||||
image = None
|
||||
if q.image is not None and q.image != "":
|
||||
image_bytes = await _download_image(q.image)
|
||||
image = await _upload_to_imgur(image_bytes, imgur_album_id)
|
||||
image_name= f"{quiz_id}--{uuid.uuid4()}"
|
||||
image = await storage.upload(file_name=image_name, file_data=image_bytes)
|
||||
image = f"{settings.root_address}/api/v1/storage/download/{image_name}"
|
||||
# image = q.image
|
||||
for a in q.choices:
|
||||
answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer))))
|
||||
quiz_questions.append(
|
||||
QuizQuestion(question=q.question, answers=answers, time=str(q.time / 1000), image=image).dict())
|
||||
quiz_data = Quiz(id=uuid.uuid4(), public=False, title=quiz.kahoot.title, description=quiz.kahoot.description,
|
||||
quiz_data = Quiz(id=quiz_id, public=False, title=quiz.kahoot.title, description=quiz.kahoot.description,
|
||||
created_at=datetime.now(), updated_at=datetime.now(), user_id=user.id,
|
||||
questions=json.dumps(quiz_questions))
|
||||
return await quiz_data.save()
|
||||
|
||||
@@ -9,8 +9,9 @@ from fastapi.responses import JSONResponse
|
||||
import re
|
||||
|
||||
from classquiz.auth import get_current_user, get_current_user_optional
|
||||
from classquiz.config import redis
|
||||
from classquiz.config import redis, settings
|
||||
from classquiz.db.models import Quiz, QuizInput, User, PlayGame
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -18,9 +19,10 @@ router = APIRouter()
|
||||
@router.post("/create")
|
||||
async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_current_user)):
|
||||
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
||||
server_regex = rf"^{settings.root_address}/api/v1/storage/download/.{36}--.{36}$"
|
||||
for question in quiz_input.questions:
|
||||
if question.image is not None:
|
||||
if not re.match(imgur_regex, question.image):
|
||||
if not re.match(imgur_regex, question.image) or not re.match(server_regex, question.image):
|
||||
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||
quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4())
|
||||
await redis.delete("global_quiz_count")
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import StreamingResponse
|
||||
from classquiz.config import settings
|
||||
import io
|
||||
from classquiz.storage import Storage
|
||||
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/download/{file_name}')
|
||||
async def download_file(file_name: str):
|
||||
storage = Storage(backend=settings.storage_backend, deta_key=settings.deta_project_key,
|
||||
deta_id=settings.deta_project_id)
|
||||
download = await storage.download(file_name)
|
||||
|
||||
def iter_file():
|
||||
yield from download
|
||||
|
||||
return StreamingResponse(iter_file(), media_type='image/*')
|
||||
@@ -13,12 +13,16 @@ settings = settings()
|
||||
@sio.event
|
||||
async def join_game(sid, data):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post("https://hcaptcha.com/siteverify",
|
||||
data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp:
|
||||
resp_data = await resp.json()
|
||||
if not resp_data["success"]:
|
||||
print("CAPTCHA FAILED")
|
||||
return
|
||||
try:
|
||||
async with session.post("https://hcaptcha.com/siteverify",
|
||||
data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp:
|
||||
resp_data = await resp.json()
|
||||
if not resp_data["success"]:
|
||||
print("CAPTCHA FAILED")
|
||||
return
|
||||
except KeyError as e:
|
||||
print("CAPTCHA FAILED")
|
||||
return
|
||||
redis_res = await redis.get(f"game:{data['game_pin']}")
|
||||
if redis_res is None:
|
||||
await sio.emit("game_not_found", room=sid)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from .deta_storage import DetaStorage
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class Storage:
|
||||
def __init__(self, backend: str, deta_key: str | None, deta_id: str | None):
|
||||
self.backend = backend
|
||||
self.deta_key = deta_key
|
||||
self.deta_id = deta_id
|
||||
self.deta_base_url = f"https://drive.deta.sh/v1/{deta_id}/classquiz"
|
||||
self.deta_instance = DetaStorage(deta_base_url=self.deta_base_url, deta_key=self.deta_key, deta_id=self.deta_id)
|
||||
if backend == "deta":
|
||||
if deta_key is None or deta_id is None:
|
||||
raise ValueError("deta_key and deta_id must be provided")
|
||||
else:
|
||||
raise NotImplementedError(f"Backend {backend} not implemented")
|
||||
|
||||
async def download(self, file_name: str) -> BytesIO | None:
|
||||
if self.backend == "deta":
|
||||
return await self.deta_instance.download(
|
||||
file_name)
|
||||
|
||||
async def upload(self, file_name: str, file_data: bytes) -> None:
|
||||
if self.backend == "deta":
|
||||
return await self.deta_instance.upload(file=file_data, file_name=file_name)
|
||||
@@ -0,0 +1,43 @@
|
||||
from classquiz.config import settings
|
||||
from aiohttp import ClientSession
|
||||
from io import BytesIO
|
||||
|
||||
settings = settings()
|
||||
|
||||
|
||||
class DetaStorage:
|
||||
def __init__(self, deta_base_url: str, deta_id: str, deta_key: str):
|
||||
self.deta_url = deta_base_url
|
||||
self.deta_id = deta_id
|
||||
self.deta_key = deta_key
|
||||
self.headers = {
|
||||
"X-Api-Key": self.deta_key,
|
||||
}
|
||||
|
||||
async def download(self, file_name: str) -> BytesIO | None:
|
||||
"""
|
||||
|
||||
:param file_name: The name of the file to be downloaded
|
||||
:return: Either bytes f successfull download or None if failed
|
||||
"""
|
||||
async with ClientSession(headers=self.headers) as session:
|
||||
async with session.get(f"{self.deta_url}/files/download?name={file_name}") as response:
|
||||
if response.status == 200:
|
||||
return BytesIO(await response.read())
|
||||
elif response.status == 404:
|
||||
return None
|
||||
else:
|
||||
raise Exception("Download failed")
|
||||
|
||||
async def upload(self, file: bytes, file_name: str) -> None:
|
||||
"""
|
||||
:param file: The file in bytes
|
||||
:param file_name: The name of the file
|
||||
:return:
|
||||
"""
|
||||
async with ClientSession(headers=self.headers) as session:
|
||||
async with session.post(f"{self.deta_url}/files?name={file_name}", data=file) as response:
|
||||
if response.status == 201:
|
||||
return None
|
||||
else:
|
||||
raise Exception("Upload failed")
|
||||
@@ -40,11 +40,14 @@
|
||||
|
||||
const checkIfAllQuestionImagesComplyWithRegex = (questions: Array<Question>) => {
|
||||
let NoteverythingValid = false;
|
||||
const regex = /^https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif)$/;
|
||||
// const regex = /^https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif)$/;
|
||||
// const local_regex = /^http(|s):\/\/\w*(|:)\d*\/api\/v1\/storage\/download\/.{36}--.{36}$/g;
|
||||
const main_regex =
|
||||
/^(http(|s):\/\/\w*(|:)\d*\/api\/v1\/storage\/download\/.{36}--.{36}|https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif))$/;
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const question = questions[i];
|
||||
if (question.image && !regex.test(question.image)) {
|
||||
console.log('not valid');
|
||||
|
||||
if (question.image && !main_regex.test(question.image)) {
|
||||
NoteverythingValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
</div>
|
||||
{#if question.image !== null}
|
||||
<div>
|
||||
<img src={question.image} class="w-full h-full object-cover" alt="Question image" />
|
||||
<img
|
||||
src={question.image}
|
||||
class="h-2/5 object-cover mx-auto mb-8"
|
||||
alt="Content for Question"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if timer_res !== '0'}
|
||||
|
||||
Reference in New Issue
Block a user