Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added README.md
Empty file.
87 changes: 81 additions & 6 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# ruff : noqa
import socket
from contextlib import asynccontextmanager

Expand All @@ -11,13 +10,33 @@
from rich.console import Console
from rich.panel import Panel

# The port on which the FastAPI server will listen
PORT = 8000

# The host address for the server
# "0.0.0.0" makes the server accessible from any network interface
HOST = "0.0.0.0"

# Whether to enable auto-reload of the server on code changes
# Useful in development; should be False in production
RELOAD = False

# Rich console object for styled terminal output
# Used to print QR codes, instructions, and panels in a readable format
console = Console()


def get_wifi_ip():
"""
Retrieve the local IP address of the machine on the current Wi-Fi/network.

Returns:
str: The local IP address (e.g., '192.168.1.10').

Notes:
- Uses a temporary UDP socket to determine the outbound interface.
- No actual network traffic is sent to the remote host (8.8.8.8).
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80))
Expand All @@ -28,16 +47,39 @@ def get_wifi_ip():


def get_server_url():
"""
Construct the full HTTP URL of the server using the local Wi-Fi IP and port.

Returns:
str: URL in the format "http://<local_ip>:<port>".

Notes:
- Relies on get_wifi_ip() to obtain the host IP.
"""
ip = get_wifi_ip()
return f"http://{ip}:{PORT}"


def generate_qr_ascii(url: str) -> str:
"""
Generate an ASCII representation of a QR code for a given URL.

Parameters:
url (str): The URL or text to encode in the QR code.

Returns:
str: QR code rendered as ASCII characters.

Notes:
- Uses the qrcode library.
- The QR code is inverted for better visibility in terminal output.
"""
qr = qrcode.QRCode(border=1)
qr.add_data(url)
qr.make()
# Capture ASCII QR into a string
import io, sys
import io
import sys

buf = io.StringIO()
sys_stdout = sys.stdout
Expand All @@ -49,6 +91,20 @@ def generate_qr_ascii(url: str) -> str:

@asynccontextmanager
async def lifespan(app: FastAPI):
"""
FastAPI lifespan context manager for displaying connection instructions.

Parameters:
app (FastAPI): The FastAPI application instance.

Yields:
None

Effects:
- Prints an ASCII QR code to the terminal for connecting a mobile device.
- Prints step-by-step instructions in the terminal using rich panels.
- Runs once when the application starts and cleans up after shutdown.
"""
url = get_server_url()
mobile_page_url = f"{url}/mobile_page"
qr_ascii = generate_qr_ascii(mobile_page_url)
Expand All @@ -57,10 +113,10 @@ async def lifespan(app: FastAPI):
steps_panel = Panel.fit(
"\n".join(
[
f"[bold cyan]1.[/bold cyan] Connect to the same Wi-Fi network",
f"[bold cyan]2.[/bold cyan] Scan the QR code",
"[bold cyan]1.[/bold cyan] Connect to the same Wi-Fi network",
"[bold cyan]2.[/bold cyan] Scan the QR code",
f"[bold cyan] [/bold cyan] Or [yellow]{mobile_page_url}[/yellow]",
f"[bold cyan]3.[/bold cyan] That's it, Now do your Misclick!",
"[bold cyan]3.[/bold cyan] That's it, Now do your Misclick!",
]
),
title="Steps",
Expand All @@ -74,19 +130,38 @@ async def lifespan(app: FastAPI):
app = FastAPI(lifespan=lifespan)
app.mount("/resource", StaticFiles(directory="mobile_page/"), name="resource")


# Store connected clients
connected_clients: list[WebSocket] = []


@app.get("/mobile_page")
async def get_mobile_page():
"""
Serve the main HTML page for mobile clients.

Returns:
HTMLResponse: The contents of "mobile_page/index.html" as an HTML response.

Notes:
- The HTML page allows the mobile device to interact with the WebSocket server.
"""
with open("mobile_page/index.html") as f:
return HTMLResponse(f.read())


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
WebSocket endpoint to broadcast messages between connected clients.

Parameters:
websocket (WebSocket): The incoming WebSocket connection.

Effects:
- Accepts the WebSocket connection and adds it to the connected clients list.
- Receives messages from one client and forwards them to all other connected clients.
- Removes the client from the list when it disconnects.
"""
await websocket.accept()
connected_clients.append(websocket)
try:
Expand Down
145 changes: 89 additions & 56 deletions browser_extension/content.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,100 @@
function injectWhenReady() {
if (!document.head || !document.body) {
// Retry until DOM is ready
requestAnimationFrame(injectWhenReady);
return;
}
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = chrome.runtime.getURL('runtime/pyscript.css');
document.head.appendChild(css);

const pyscriptJs = document.createElement('script');
pyscriptJs.src = chrome.runtime.getURL('runtime/pyscript.js');
pyscriptJs.defer = true;
document.head.appendChild(pyscriptJs);

pyscriptJs.onload = () => {
fetch(chrome.runtime.getURL('main.py'))
.then(res => res.text())
.then(code => {
const pyTag = document.createElement('py-script');
pyTag.textContent = code;
document.body.appendChild(pyTag);
});
}

fetch(chrome.runtime.getURL('static/easter_eggs.json'))
.then(res => res.json())
.then(videoList => {
// Shuffle the list (Fisher–Yates)
for (let i = videoList.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[videoList[i], videoList[j]] = [videoList[j], videoList[i]];
// Ensure the DOM is ready before injecting anything
if (!document.head || !document.body) {
requestAnimationFrame(injectWhenReady);
return;
}

const spacing = Math.min(window.innerWidth, window.innerHeight) / videoList.length;
const diagonalPositions = [];
// --- Load PyScript runtime CSS ---
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = chrome.runtime.getURL('runtime/pyscript.css');
document.head.appendChild(css);

videoList.forEach((video, i) => {
const a = document.createElement('a');
a.href = video.url;
a.target = "_blank";
a.id = "pyscript-hidden-easter-eggs";
a.style.position = "absolute";
a.style.opacity = "0";
a.style.pointerEvents = "auto";
a.style.zIndex = "9999";
// --- Load PyScript runtime JS ---
const pyscriptJs = document.createElement('script');
pyscriptJs.src = chrome.runtime.getURL('runtime/pyscript.js');
pyscriptJs.defer = true;
document.head.appendChild(pyscriptJs);

const x = Math.floor(i * spacing);
const y = Math.floor(i * spacing);
// Wait until PyScript runtime is loaded before injecting Python code
pyscriptJs.onload = async () => {
// --- Load main.py ---
const mainCode = await fetch(chrome.runtime.getURL('main.py')).then(r => r.text());

a.style.left = `${x}px`;
a.style.top = `${y}px`;
// --- Load all utils/*.py files (helper modules) ---
const utilsFiles = [
"__init__.py",
"easter_eggs.py",
"fake_cursor.py",
"make_highlights.py",
"move_and_click.py",
"toast.py"
// Add more utils/*.py files here if needed
];

document.body.appendChild(a);
// Create Python code to build utils/ directory at runtime
let utilsLoader = `
import os
os.makedirs("utils", exist_ok=True)
`;

// Save for PyScript wandering logic
diagonalPositions.push([x, y]);
});
// For each Python util file, write its contents into the in-browser FS
for (const f of utilsFiles) {
const code = await fetch(chrome.runtime.getURL(`utils/${f}`)).then(r => r.text());
utilsLoader += `with open("utils/${f}", "w") as fp:\n fp.write("""${code.replace(/"""/g, '\\"""')}""")\n\n`;
}

// Expose to PyScript
window.diagonalPositions = diagonalPositions;
});
}
// --- Inject everything into <py-script> ---
// (PyScript tag runs Python code directly in the browser)
const pyTag = document.createElement('py-script');
pyTag.textContent = utilsLoader + "\n\n" + mainCode;
document.body.appendChild(pyTag);
};

// --- Easter eggs loader ---
// Fetch JSON file with video links and create invisible <a> elements on screen
fetch(chrome.runtime.getURL('static/easter_eggs.json'))
.then(res => res.json())
.then(videoList => {
// Randomize video order using Fisher–Yates shuffle
for (let i = videoList.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[videoList[i], videoList[j]] = [videoList[j], videoList[i]];
}

// Distribute easter eggs along diagonal of the screen
const spacing = Math.min(window.innerWidth, window.innerHeight) / videoList.length;
const diagonalPositions = [];

videoList.forEach((video, i) => {
const a = document.createElement('a');
a.href = video.url;
a.target = "_blank"; // open in new tab
a.id = "pyscript-hidden-easter-eggs";

// Make link invisible but still clickable by fake cursor
a.style.position = "absolute";
a.style.opacity = "0";
a.style.pointerEvents = "auto";
a.style.zIndex = "9999";

// Position each link on the diagonal
const x = Math.floor(i * spacing);
const y = Math.floor(i * spacing);

a.style.left = `${x}px`;
a.style.top = `${y}px`;

// Add to DOM and keep track of coordinates
document.body.appendChild(a);
diagonalPositions.push([x, y]);
});

// Expose diagonal positions globally for PyScript wandering mode
window.diagonalPositions = diagonalPositions;
});
}

injectWhenReady()
// Kick off the injection once the page is ready
injectWhenReady();
Loading