This commit is contained in:
KäseToatz
2024-04-26 13:05:31 +02:00
parent 30dc609b30
commit 9d902b2ce9
14 changed files with 353 additions and 99 deletions

4
.gitignore vendored
View File

@ -1,2 +1,2 @@
/out
/.vs /__pycache__

9
.gitmodules vendored
View File

@ -1,9 +0,0 @@
[submodule "lib/websocketpp"]
path = lib/websocketpp
url = https://github.com/zaphoyd/websocketpp
[submodule "lib/zlib"]
path = lib/zlib
url = https://github.com/madler/zlib
[submodule "lib/uWebSockets"]
path = lib/uWebSockets
url = https://github.com/uNetworking/uWebSockets

View File

@ -1,11 +0,0 @@
cmake_minimum_required(VERSION 3.12)
project("Kahoot")
set(CMAKE_CXX_STANDARD 20)
add_executable(Kahoot "src/main.cpp")
include_directories("lib/uWebSockets/src")
include_directories("lib/uWebSockets/uSockets/src")
include_directories("lib/zlib")

View File

@ -1,46 +0,0 @@
{
"version": 3,
"configurePresets": [
{
"name": "x64-release",
"displayName": "x64 Release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"installDir": "${sourceDir}/out/install/${presetName}",
"architecture": {
"value": "x64",
"strategy": "external"
},
"cacheVariables": {
"CMAKE_C_COMPILER": "cl.exe",
"CMAKE_CXX_COMPILER": "cl.exe",
"CMAKE_BUILD_TYPE": "Release"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "linux-release",
"displayName": "Linux Release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"installDir": "${sourceDir}/out/install/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"vendor": {
"microsoft.com/VisualStudioRemoteSettings/CMake/1.0": {
"sourceDir": "$env{HOME}/.vs/$ms{projectDirName}"
}
}
}
]
}

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 KaseToatz1337
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,2 +0,0 @@
# Kahoot
A kahoot flooder because I was bored very fast very gud

127
app.py Normal file
View File

@ -0,0 +1,127 @@
import re
import aiohttp
import time
import py_mini_racer
import base64
import uvicorn
import websockets
import json
import asyncio
import random
import itertools
from fastapi import FastAPI, Form
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
class App(FastAPI):
def __init__(self) -> None:
super().__init__(title="Kahoot API", openapi_url=None)
self.add_middleware(CORSMiddleware, allow_origins=["*"])
self.semaphore = asyncio.Semaphore(10)
async def connect(self, pin: int, name: str, number: int, namerator: bool = False) -> None:
try:
async with self.semaphore:
async with aiohttp.ClientSession() as session:
if namerator:
async with session.get("https://apis.kahoot.it/") as response:
name = (await response.json(content_type=None))["name"]
async with session.get(f"https://play.kahoot.it/reserve/session/{pin}/?{int(time.time() * 1000)}") as response:
sessionToken = response.headers["x-kahoot-session-token"]
challenge = (await response.json())["challenge"]
text = re.split("[{};]", challenge.replace("\t", "").encode("ascii", "ignore").decode())
solChars = [ord(s) for s in py_mini_racer.MiniRacer().eval("".join([text[1] + "{", text[2] + ";", "return message.replace(/./g, function(char, position) {", text[7] + ";})};", text[0]]))]
sessChars = [ord(s) for s in base64.b64decode(sessionToken).decode()]
sessionID = "".join([chr(sessChars[i] ^ solChars[i % len(solChars)]) for i in range(len(sessChars))])
ws = await websockets.connect(f"wss://play.kahoot.it/cometd/{pin}/{sessionID}", open_timeout=None, ping_timeout=None, close_timeout=None)
await ws.send(json.dumps([{"channel": "/meta/handshake"}]))
clientID = json.loads(await ws.recv())[0]["clientId"]
await ws.send(json.dumps([
{
"channel": "/service/controller",
"data": {
"type": "login",
"gameid": pin,
"name": name,
},
"clientId": clientID,
}
]))
await ws.send(json.dumps([
{
"channel": "/service/controller",
"data": {
"type": "message",
"gameid": pin,
"id": 16,
},
"clientId": clientID,
}
]))
while True:
message = json.loads(await ws.recv())[0]
if message.get("data", {}).get("id", None) == 2:
question = json.loads(message["data"]["content"])
if question["type"] == "quiz":
await asyncio.sleep(number / 100)
await ws.send(json.dumps([
{
"channel": "/service/controller",
"data": {
"type": "message",
"gameid": pin,
"id": 45,
"content": json.dumps({"type": "quiz", "choice": random.randint(0, question["numberOfChoices"] - 1), "questionIndex": question["gameBlockIndex"]})
},
"clientId": clientID,
}
]))
elif question["type"] == "multiple_select_quiz":
await asyncio.sleep(number / 100)
await ws.send(json.dumps([
{
"channel": "/service/controller",
"data": {
"type": "message",
"gameid": pin,
"id": 45,
"content": json.dumps({"type": "multiple_select_quiz", "choice": [random.randint(0, question["numberOfChoices"] - 1)], "questionIndex": question["gameBlockIndex"]})
},
"clientId": clientID,
}
]))
elif message.get("data", {}).get("id", None) in [10, 13] or message.get("data", {}).get("reason", None) == "disconnect":
await ws.close()
except websockets.ConnectionClosed:
return
except:
if "ws" in locals().keys():
await ws.close()
app = App()
@app.post("/flood", response_class=JSONResponse)
async def flood(pin: int = Form(0), naming: str = Form(""), name: str = Form(""), amount: int = Form(0)) -> JSONResponse:
async with aiohttp.ClientSession() as session:
async with session.get(f"https://play.kahoot.it/reserve/session/{pin}/?{int(time.time() * 1000)}") as response:
if "x-kahoot-session-token" not in response.headers:
return JSONResponse({"message": "Invalid PIN specified.", "type": "error"}, 400)
if len(name) < 2 or len(name) > 15:
return JSONResponse({"message": "Invalid name specified.", "type": "error"}, 400)
if amount < 1 or amount > 2000:
return JSONResponse({"message": "Invalid amount specified.", "type": "error"}, 400)
if naming == "enumerated":
await asyncio.gather(*[app.connect(pin, f"{name[:15-len(str(i))]}{i}", i) for i in range(amount)])
elif naming == "capitalized":
names = list(map(''.join, itertools.product(*zip(name.upper(), name.lower()))))
await asyncio.gather(*[app.connect(pin, names[i], i) for i in range(amount)])
elif naming == "random":
await asyncio.gather(*[app.connect(pin, str(i), i, True) for i in range(amount)])
else:
return JSONResponse({"message": "Invalid naming method specified.", "type": "error"}, 400)
return {"message": "Game finished.", "type": "success"}
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=80)

14
html/assets/logo.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 440 150" enable-background="new 0 0 440 150" xml:space="preserve">
<g>
<path fill="#FFFFFF" d="M226.8,52.5c-18.7,0.3-34.8,16.1-36,35.1c-1.1,19.1,14.1,36.1,33.9,38.1c19.9,2,36-13.8,36-35.1 C260.7,69.2,245.5,52.2,226.8,52.5z M240.2,92c-0.8,9.9-7,15.3-11.1,15.7c-10.3,1.2-19.1-8.1-18.5-20s5.4-19.3,15.1-19.5 C235.4,68,241.3,79.1,240.2,92z"/>
<path fill="#FFFFFF" d="M300.3,54.4c-18.7-0.3-33.9,16.7-33.9,38.1s16.1,37.1,36,35.1c19.9-2,35.1-19,33.9-38.1 C335.2,70.5,319.1,54.8,300.3,54.4z M298,109.7c-4.1-0.5-10.2-5.9-11.1-15.7c-1.1-12.9,4.8-24,14.5-23.8s14.5,7.6,15.1,19.5 C317.1,101.5,308.3,110.9,298,109.7z"/>
<polygon fill="#FFFFFF" points="82.5,36.6 62.4,28.9 23.2,66.3 23.2,19.6 0,25.5 0,135.2 23.2,136 23,97.6 37.3,83.8 52.4,136 72.9,136 54,67.9 "/>
<path fill="#FFFFFF" d="M146.2,49.2L145.8,0L126,5.2l3.2,121.6l19.5,0.7l-1.1-62.8c4.3-1.9,18.4-5.8,18.9,9.7l1.9,22.1l1.3,31.1 h20.4l-7.7-53.7C179.3,42.5,167.8,42.4,146.2,49.2z"/>
<polygon fill="#FFFFFF" points="415.2,128.2 409.5,141.6 421.7,150 433.7,142.9 428.6,128.2 "/>
<path fill="#FFFFFF" d="M363.4,19.6l-15.5-6.4v32.1l-17.3-0.6l3.3,22.8h14l1,54.3c0,0-2.2,19.5,27.6,15.1c0,0,9.3-2.8,9-9.3v-21.4 c0,0-6.3,3.7-13.1,3.7c-6.8,0-6.9-3.3-6.9-3.3l-1.8-42l21.7-1.2V49.5L363.1,48L363.4,19.6z"/>
<polygon fill="#FFFFFF" points="440,19.8 399,12 424.2,120.5 "/>
<path fill="#FFFFFF" d="M77.6,49.9L83,64.1c13.5-8.1,21.1,0,21.1,0l-0.1,9.1c-40.2,7.1-30,44.3-30,44.3 c4.1,14.6,17.3,14.1,17.3,14.1h30.1l0.6-67.7C117.3,32.4,77.6,49.9,77.6,49.9z M104,118.6c0,0-14.6,3.5-16.2-10.7 c0,0,0.3-22,16.7-19.6L104,118.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

61
html/assets/script.js Normal file
View File

@ -0,0 +1,61 @@
window.onload = () => {
var pin = document.getElementById("pin");
var naming = document.getElementById("naming");
var name = document.getElementById("name");
var amount = document.getElementById("amount");
var flood = document.getElementById("flood");
var text = document.getElementById("text");
function checkMaxCaps()
{
if (naming.value == "capitalized")
{
var maxCaps = 1 << name.value.length;
if (amount.value > maxCaps)
{
text.innerText = `Amount will be capped at ${maxCaps}.`;
text.style.color = "red";
}
else
{
text.innerText = "";
text.style.color = "black";
}
}
}
naming.onchange = () => {
if (naming.value == "random")
{
name.disabled = true;
}
else
{
name.disabled = false;
checkMaxCaps();
}
}
amount.oninput = () => {checkMaxCaps()};
name.oninput = () => {checkMaxCaps()};
flood.onclick = async () => {
var formData = new FormData();
formData.append("pin", parseInt(pin.value));
formData.append("naming", naming.value)
formData.append("name", name.value);
formData.append("amount", parseInt(amount.value));
text.value = "Flooding in progress...";
text.style.color = "green";
var request = await fetch("http://127.0.0.1/flood", {
method: "POST",
body: formData
})
response = await request.json();
text.value = response.message;
if (response.type == "error")
{
text.style.color = "red";
}
}
}

117
html/assets/style.css Normal file
View File

@ -0,0 +1,117 @@
body
{
margin: 0px;
display: flex;
position: absolute;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
background-color: rgb(56, 18, 114);
}
#container
{
display: flex;
flex-direction: column;
}
input
{
outline: none;
}
#rectangle
{
position: fixed;
background-color: black;
transform: rotate(45deg);
opacity: 0.1;
top: -15vmin;
left: -15vmin;
min-width: 75vmin;
min-height: 75vmin;
z-index: -1;
}
#circle
{
position: fixed;
border-radius: 50%;
background-color: black;
opacity: 0.1;
bottom: -15vmin;
right: -15vmin;
min-width: 75vmin;
min-height: 75vmin;
z-index: -1;
}
#logo
{
background-image: url("/html/assets/logo.svg");
height: 80px;
background-repeat: no-repeat;
background-position: center;
margin-bottom: 30px;
}
#form
{
display: flex;
flex-direction: column;
gap: 5px;
padding: 20px;
background-color: white;
border-radius: 5px;
}
input, select
{
outline: none;
height: 40px;
text-align: center;
font-size: 1rem;
font-weight: 700;
color: rgb(51, 51, 51);
border: 0.125rem solid rgb(204, 204, 204);
border-radius: 5px;
background-color: white;
transition: border-color 0.25s;
}
select
{
color: #9191A0;
height: 46px;
}
input:focus:hover, select:focus:hover
{
border-color: rgb(51, 51, 51);
}
input:focus, select:focus
{
border-color: #0060DF;
}
#flood
{
background-color: rgb(51, 51, 51);
color: white;
border: none;
box-shadow: rgba(0, 0, 0, 0.25) 0px -4px inset;
padding: 0px 16px 4px;
min-height: 46px;
cursor: pointer;
}
#flood:hover
{
background-color: rgb(47, 47, 47);
box-shadow: rgba(0, 0, 0, 0.25) 0px -2px inset;
margin-top: 2px;
padding-bottom: 2px;
min-height: 44px;
}

32
html/index.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="EN">
<head>
<title>Kahoot Flooder</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
<link rel="stylesheet" href="assets/style.css">
<script type="text/javascript" src="assets/script.js"></script>
</head>
<body>
<div id="rectangle"></div>
<div id="circle"></div>
<div id="container">
<div id="logo"></div>
<div id="form">
<input id="pin" type="text" maxlength="7" placeholder="PIN">
<div id="names">
<select id="naming">
<option value="enumerated">Enumerated</option>
<option value="capitalized">Randomly capitalized</option>
<option value="random">Fully random</option>
</select>
<input id="name" type="text" maxlength="15" placeholder="Name">
</div>
<input id="amount" type="text" placeholder="Amount">
<input id="flood" type="button" value="Flood">
</div>
<p id="text"></p>
</div>
</body>
</html>

Submodule lib/uWebSockets deleted from 84941b99bf

Submodule lib/zlib deleted from 0f51fb4933

View File

@ -1,6 +0,0 @@
#include "ClientApp.h"
int main()
{
return 0;
}