From fa06cb66ab0d6a797b0de613060adbc8c26a31c1 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 6 Aug 2025 11:01:04 +0900 Subject: [PATCH 001/117] Initial commit --- .github/workflows/lint.yaml | 35 +++++++ .gitignore | 31 ++++++ .pre-commit-config.yaml | 18 ++++ LICENSE.txt | 7 ++ README.md | 186 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 63 ++++++++++++ samples/Pipfile | 15 +++ samples/pyproject.toml | 19 ++++ 8 files changed, 374 insertions(+) create mode 100644 .github/workflows/lint.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 samples/Pipfile create mode 100644 samples/pyproject.toml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..7f67e803 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,35 @@ +# GitHub Action workflow enforcing our code style. + +name: Lint + +# Trigger the workflow on both push (to the main repository, on the main branch) +# and pull requests (against the main repository, but from any repo, from any branch). +on: + push: + branches: + - main + pull_request: + +# Brand new concurrency setting! This ensures that not more than one run can be triggered for the same commit. +# It is useful for pull requests coming from the main repository since both triggers will match. +concurrency: lint-${{ github.sha }} + +jobs: + lint: + runs-on: ubuntu-latest + + env: + # The Python version your project uses. Feel free to change this if required. + PYTHON_VERSION: "3.12" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..233eb87e --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Files generated by the interpreter +__pycache__/ +*.py[cod] + +# Environment specific +.venv +venv +.env +env + +# Unittest reports +.coverage* + +# Logs +*.log + +# PyEnv version selector +.python-version + +# Built objects +*.so +dist/ +build/ + +# IDEs +# PyCharm +.idea/ +# VSCode +.vscode/ +# MacOS +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..c0a8de23 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# Pre-commit configuration. +# See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.2 + hooks: + - id: ruff-check + - id: ruff-format diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..5a04926b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2021 Python Discord + +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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3bf4bfba --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ +# Python Discord Code Jam Repository Template + +## A primer + +Hello code jam participants! We've put together this repository template for you to use in [our code jams](https://pythondiscord.com/events/) or even other Python events! + +This document contains the following information: + +1. [What does this template contain?](#what-does-this-template-contain) +2. [How do I use this template?](#how-do-i-use-this-template) +3. [How do I adapt this template to my project?](#how-do-i-adapt-this-template-to-my-project) + +> [!TIP] +> You can also look at [our style guide](https://pythondiscord.com/events/code-jams/code-style-guide/) to get more information about what we consider a maintainable code style. + +## What does this template contain? + +Here is a quick rundown of what each file in this repository contains: + +- [`LICENSE.txt`](LICENSE.txt): [The MIT License](https://opensource.org/licenses/MIT), an OSS approved license which grants rights to everyone to use and modify your project, and limits your liability. We highly recommend you to read the license. +- [`.gitignore`](.gitignore): A list of files and directories that will be ignored by Git. Most of them are auto-generated or contain data that you wouldn't want to share publicly. +- [`pyproject.toml`](pyproject.toml): Configuration and metadata for the project, as well as the linting tool Ruff. If you're interested, you can read more about `pyproject.toml` in the [Python Packaging documentation](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/). +- [`.pre-commit-config.yaml`](.pre-commit-config.yaml): The configuration of the [pre-commit](https://pre-commit.com/) tool. +- [`.github/workflows/lint.yaml`](.github/workflows/lint.yaml): A [GitHub Actions](https://github.com/features/actions) workflow, a set of actions run by GitHub on their server after each push, to ensure the style requirements are met. + +Each of these files have comments for you to understand easily, and modify to fit your needs. + +### Ruff: general style rules + +Our first tool is Ruff. It will check your codebase and warn you about any non-conforming lines. +It is run with the command `ruff check` in the project root. + +Here is a sample output: + +```shell +$ ruff check +app.py:1:5: N802 Function name `helloWorld` should be lowercase +app.py:1:5: ANN201 Missing return type annotation for public function `helloWorld` +app.py:2:5: D400 First line should end with a period +app.py:2:5: D403 First word of the first line should be capitalized: `docstring` -> `Docstring` +app.py:3:15: W292 No newline at end of file +Found 5 errors. +``` + +Each line corresponds to an error. The first part is the file path, then the line number, and the column index. +Then comes the error code, a unique identifier of the error, and then a human-readable message. + +If, for any reason, you do not wish to comply with this specific error on a specific line, you can add `# noqa: CODE` at the end of the line. +For example: + +```python +def helloWorld(): # noqa: N802 + ... + +``` + +This will ignore the function naming issue and pass linting. + +> [!WARNING] +> We do not recommend ignoring errors unless you have a good reason to do so. + +### Ruff: formatting + +Ruff also comes with a formatter, which can be run with the command `ruff format`. +It follows the same code style enforced by [Black](https://black.readthedocs.io/en/stable/index.html), so there's no need to pick between them. + +### Pre-commit: run linting before committing + +The second tool doesn't check your code, but rather makes sure that you actually *do* check it. + +It makes use of a feature called [Git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) which allow you to run a piece of code before running `git commit`. +The good thing about it is that it will cancel your commit if the lint doesn't pass. You won't have to wait for GitHub Actions to report issues and have a second fix commit. + +It is *installed* by running `pre-commit install` and can be run manually by calling only `pre-commit`. + +[Lint before you push!](https://soundcloud.com/lemonsaurusrex/lint-before-you-push) + +#### List of hooks + +- `check-toml`: Lints and corrects your TOML files. +- `check-yaml`: Lints and corrects your YAML files. +- `end-of-file-fixer`: Makes sure you always have an empty line at the end of your file. +- `trailing-whitespace`: Removes whitespaces at the end of each line. +- `ruff-check`: Runs the Ruff linter. +- `ruff-format`: Runs the Ruff formatter. + +## How do I use this template? + +### Creating your team repository + +One person in the team, preferably the leader, will have to create the repository and add other members as collaborators. + +1. In the top right corner of your screen, where **Clone** usually is, you have a **Use this template** button to click. + ![use-this-template-button](https://docs.github.com/assets/images/help/repository/use-this-template-button.png) +2. Give the repository a name and a description. + ![create-repository-name](https://docs.github.com/assets/images/help/repository/create-repository-name.png) +3. Click **Create repository from template**. +4. Click **Settings** in your newly created repository. + ![repo-actions-settings](https://docs.github.com/assets/images/help/repository/repo-actions-settings.png) +5. In the "Access" section of the sidebar, click **Collaborators**. + ![collaborators-settings](https://github.com/python-discord/code-jam-template/assets/63936253/c150110e-d1b5-4e4d-93e0-0a2cf1de352b) +6. Click **Add people**. +7. Insert the names of each of your teammates, and invite them. Once they have accepted the invitation in their email, they will have write access to the repository. + +You are now ready to go! Sit down, relax, and wait for the kickstart! + +> [!IMPORTANT] +> Don't forget to change the project name, description, and authors at the top of the [`pyproject.toml`](pyproject.toml) file, and swap "Python Discord" in the [`LICENSE.txt`](LICENSE.txt) file for the name of each of your team members or the name of your team *after* the start of the code jam. + +### Using the default pip setup + +Our default setup includes a dependency group to be used with a [virtual environment](https://docs.python.org/3/library/venv.html). +It works with pip and uv, and we recommend this if you have never used any other dependency manager, although if you have, feel free to switch to it. +More on that [below](#how-do-i-adapt-this-template-to-my-project). + +Dependency groups are a relatively new feature, specified in [PEP 735](https://peps.python.org/pep-0735/). +You can read more about them in the [Python Packaging User Guide](https://packaging.python.org/en/latest/specifications/dependency-groups/). + +#### Creating the environment + +Create a virtual environment in the folder `.venv`. + +```shell +python -m venv .venv +``` + +#### Entering the environment + +It will change based on your operating system and shell. + +```shell +# Linux, Bash +$ source .venv/bin/activate +# Linux, Fish +$ source .venv/bin/activate.fish +# Linux, Csh +$ source .venv/bin/activate.csh +# Linux, PowerShell Core +$ .venv/bin/Activate.ps1 +# Windows, cmd.exe +> .venv\Scripts\activate.bat +# Windows, PowerShell +> .venv\Scripts\Activate.ps1 +``` + +#### Installing the dependencies + +Once the environment is created and activated, use this command to install the development dependencies. + +```shell +pip install --group dev +``` + +#### Exiting the environment + +Interestingly enough, it is the same for every platform. + +```shell +deactivate +``` + +Once the environment is activated, all the commands listed previously should work. + +> [!IMPORTANT] +> We highly recommend that you run `pre-commit install` as soon as possible. + +## How do I adapt this template to my project? + +If you wish to use Pipenv or Poetry, you will have to move the dependencies in [`pyproject.toml`](pyproject.toml) to the development dependencies of your tool. + +We've included a porting to both [Poetry](samples/pyproject.toml) and [Pipenv](samples/Pipfile) in the [`samples` folder](samples). +Note that the Poetry [`pyproject.toml`](samples/pyproject.toml) file does not include the Ruff configuration, so if you simply replace the file then the Ruff configuration will be lost. + +When installing new dependencies, don't forget to [pin](https://pip.pypa.io/en/stable/topics/repeatable-installs/#pinning-the-package-versions) them by adding a version tag at the end. +For example, if I wish to install [Click](https://click.palletsprojects.com/en/8.1.x/), a quick look at [PyPI](https://pypi.org/project/click/) tells me that `8.1.7` is the latest version. +I will then add `click~=8.1`, without the last number, to my requirements file or dependency manager. + +> [!IMPORTANT] +> A code jam project is left unmaintained after the end of the event. If the dependencies aren't pinned, the project will break after any major change in an API. + +## Final words + +> [!IMPORTANT] +> Don't forget to replace this README with an actual description of your project! Images are also welcome! + +We hope this template will be helpful. Good luck in the jam! diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..6a232d06 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +[project] +# This section contains metadata about your project. +# Don't forget to change the name, description, and authors to match your project! +name = "code-jam-template" +description = "Add your description here" +authors = [ + { name = "Your Name" } +] +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[dependency-groups] +# This `dev` group contains all the development requirements for our linting toolchain. +# Don't forget to pin your dependencies! +# This list will have to be migrated if you wish to use another dependency manager. +dev = [ + "pre-commit~=4.2.0", + "ruff~=0.12.2", +] + +[tool.ruff] +# Increase the line length. This breaks PEP8 but it is way easier to work with. +# The original reason for this limit was a standard vim terminal is only 79 characters, +# but this doesn't really apply anymore. +line-length = 119 +# Target Python 3.12. If you decide to use a different version of Python +# you will need to update this value. +target-version = "py312" +# Automatically fix auto-fixable issues. +fix = true +# The directory containing the source code. If you choose a different project layout +# you will need to update this value. +src = ["src"] + +[tool.ruff.lint] +# Enable all linting rules. +select = ["ALL"] +# Ignore some of the most obnoxious linting errors. +ignore = [ + # Missing docstrings. + "D100", + "D104", + "D105", + "D106", + "D107", + # Docstring whitespace. + "D203", + "D213", + # Docstring punctuation. + "D415", + # Docstring quotes. + "D301", + # Builtins. + "A", + # Print statements. + "T20", + # TODOs. + "TD002", + "TD003", + "FIX", +] diff --git a/samples/Pipfile b/samples/Pipfile new file mode 100644 index 00000000..dedd0c50 --- /dev/null +++ b/samples/Pipfile @@ -0,0 +1,15 @@ +# Sample Pipfile. + +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +ruff = "~=0.12.2" +pre-commit = "~=4.2.0" + +[requires] +python_version = "3.12" diff --git a/samples/pyproject.toml b/samples/pyproject.toml new file mode 100644 index 00000000..b486e506 --- /dev/null +++ b/samples/pyproject.toml @@ -0,0 +1,19 @@ +# Sample poetry configuration. + +[tool.poetry] +name = "Name" +version = "0.1.0" +description = "Description" +authors = ["Author 1 "] +license = "MIT" + +[tool.poetry.dependencies] +python = "3.12.*" + +[tool.poetry.dev-dependencies] +ruff = "~0.12.2" +pre-commit = "~4.2.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From 78cab9a7b31b0f0381bf2434ca9cde3be3d6344c Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 Aug 2025 21:32:49 -0500 Subject: [PATCH 002/117] feat: example on how to use pyodide in our repo * feat: basic stuff * feat: added a button for onclick and a text input * lint: tidy up for checks * feat: example for pyfetch on the backend. --- index.html | 37 +++++++++++++++++++++++++++++++++++++ script.py | 25 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 index.html create mode 100644 script.py diff --git a/index.html b/index.html new file mode 100644 index 00000000..73fefa34 --- /dev/null +++ b/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/script.py b/script.py new file mode 100644 index 00000000..a3bcac55 --- /dev/null +++ b/script.py @@ -0,0 +1,25 @@ +import re + +from pyodide.http import pyfetch + +c = re.compile(r"(?i)SELECT (?P.+) FROM (?P.+)") + + +def do_something(name: str) -> None: + """Nothing, just plaiting with functions.""" + print(f"Hello {name}") + + +def parse_input(sql_data: str) -> None: + """Start of the parser.""" + data = c.match(sql_data) + print(data["fields"]) + print(data["table"]) + + +async def get_user_data(user: str) -> dict: + """Pyfetch command example.""" + response = await pyfetch(f"https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor={user}") + val = await response.json() + print(val) + return val From 60a485342b58831c5d34510368a7f05eea1a9d08 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 9 Aug 2025 15:03:01 +0900 Subject: [PATCH 003/117] Setup basic environment --- .pre-commit-config.yaml | 16 +++++++++++++--- LICENSE.txt | 2 +- README.md | 14 ++++++++++++++ pyproject.toml | 20 ++------------------ requirements.txt | 22 ++++++++++++++++++++++ samples/Pipfile | 15 --------------- samples/pyproject.toml | 19 ------------------- 7 files changed, 52 insertions(+), 56 deletions(-) create mode 100644 requirements.txt delete mode 100644 samples/Pipfile delete mode 100644 samples/pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c0a8de23..de7f5fd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,3 @@ -# Pre-commit configuration. -# See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing - repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -16,3 +13,16 @@ repos: hooks: - id: ruff-check - id: ruff-format + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.8.8 + hooks: + - id: pip-compile + args: [ + "--universal", + "--python-version=3.12", + "pyproject.toml", + "--group=dev", + "-o", + "requirements.txt" + ] diff --git a/LICENSE.txt b/LICENSE.txt index 5a04926b..2f024be1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2021 Python Discord +Copyright 2021 Iridescent Ivies 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: diff --git a/README.md b/README.md index 3bf4bfba..cd662164 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +# iridescent ivies + +### development + +To get a web server up: + 1. run `python -m http.server` + 2. navigate to the URL it provides + +To run `pre-commit` on everything: `pre-commit run -a`. + +To download all dependencies: `pip install -r requirements.txt` + +--- + # Python Discord Code Jam Repository Template ## A primer diff --git a/pyproject.toml b/pyproject.toml index 6a232d06..93706f5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,37 +1,21 @@ [project] -# This section contains metadata about your project. -# Don't forget to change the name, description, and authors to match your project! -name = "code-jam-template" -description = "Add your description here" -authors = [ - { name = "Your Name" } -] +name = "sql-bsky" +description = "Social query language" version = "0.1.0" readme = "README.md" requires-python = ">=3.12" dependencies = [] [dependency-groups] -# This `dev` group contains all the development requirements for our linting toolchain. -# Don't forget to pin your dependencies! -# This list will have to be migrated if you wish to use another dependency manager. dev = [ "pre-commit~=4.2.0", "ruff~=0.12.2", ] [tool.ruff] -# Increase the line length. This breaks PEP8 but it is way easier to work with. -# The original reason for this limit was a standard vim terminal is only 79 characters, -# but this doesn't really apply anymore. line-length = 119 -# Target Python 3.12. If you decide to use a different version of Python -# you will need to update this value. target-version = "py312" -# Automatically fix auto-fixable issues. fix = true -# The directory containing the source code. If you choose a different project layout -# you will need to update this value. src = ["src"] [tool.ruff.lint] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ef00f24b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --python-version=3.12 pyproject.toml --group=dev -o requirements.txt +cfgv==3.4.0 + # via pre-commit +distlib==0.4.0 + # via virtualenv +filelock==3.18.0 + # via virtualenv +identify==2.6.12 + # via pre-commit +nodeenv==1.9.1 + # via pre-commit +platformdirs==4.3.8 + # via virtualenv +pre-commit==4.2.0 + # via sql-bsky (pyproject.toml:dev) +pyyaml==6.0.2 + # via pre-commit +ruff==0.12.8 + # via sql-bsky (pyproject.toml:dev) +virtualenv==20.33.1 + # via pre-commit diff --git a/samples/Pipfile b/samples/Pipfile deleted file mode 100644 index dedd0c50..00000000 --- a/samples/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -# Sample Pipfile. - -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] -ruff = "~=0.12.2" -pre-commit = "~=4.2.0" - -[requires] -python_version = "3.12" diff --git a/samples/pyproject.toml b/samples/pyproject.toml deleted file mode 100644 index b486e506..00000000 --- a/samples/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -# Sample poetry configuration. - -[tool.poetry] -name = "Name" -version = "0.1.0" -description = "Description" -authors = ["Author 1 "] -license = "MIT" - -[tool.poetry.dependencies] -python = "3.12.*" - -[tool.poetry.dev-dependencies] -ruff = "~0.12.2" -pre-commit = "~4.2.0" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" From a1212acf84ef7282f1723be6907700656c02a71a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 9 Aug 2025 15:59:03 +0900 Subject: [PATCH 004/117] Tokenize things --- pyproject.toml | 5 ++ requirements.txt | 12 ++++ src/parser.py | 165 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 src/parser.py diff --git a/pyproject.toml b/pyproject.toml index 93706f5f..fcb5675f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [] dev = [ "pre-commit~=4.2.0", "ruff~=0.12.2", + "pytest", ] [tool.ruff] @@ -44,4 +45,8 @@ ignore = [ "TD002", "TD003", "FIX", + # Function complexity. + "C901", + # TODO: set up proper testing structure + "S101", ] diff --git a/requirements.txt b/requirements.txt index ef00f24b..4351df33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,18 +2,30 @@ # uv pip compile --universal --python-version=3.12 pyproject.toml --group=dev -o requirements.txt cfgv==3.4.0 # via pre-commit +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest distlib==0.4.0 # via virtualenv filelock==3.18.0 # via virtualenv identify==2.6.12 # via pre-commit +iniconfig==2.1.0 + # via pytest nodeenv==1.9.1 # via pre-commit +packaging==25.0 + # via pytest platformdirs==4.3.8 # via virtualenv +pluggy==1.6.0 + # via pytest pre-commit==4.2.0 # via sql-bsky (pyproject.toml:dev) +pygments==2.19.2 + # via pytest +pytest==8.4.1 + # via sql-bsky (pyproject.toml:dev) pyyaml==6.0.2 # via pre-commit ruff==0.12.8 diff --git a/src/parser.py b/src/parser.py new file mode 100644 index 00000000..2d05e29b --- /dev/null +++ b/src/parser.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import string +from dataclasses import dataclass +from enum import Enum, auto + + +@dataclass +class Token: + """A token produced by tokenization.""" + + kind: TokenKind + text: str + start_pos: int + end_pos: int + + +class TokenKind(Enum): + """What the token represents.""" + + # keywords + SELECT = auto() + FROM = auto() + + # literals + STRING = auto() + IDENTIFIER = auto() + + # structure + COMMA = auto() + STAR = auto() + EOF = auto() + ERROR = auto() + + +KEYWORDS = { + "SELECT": TokenKind.SELECT, + "FROM": TokenKind.FROM, +} + + +@dataclass +class Cursor: + """Helper class to allow peeking into a stream of characters.""" + + contents: str + index: int = 0 + + def peek(self) -> str: + """Look one character ahead in the stream.""" + return self.contents[self.index : self.index + 1] + + def next(self) -> str: + """Get the next character in the stream.""" + c = self.peek() + if c != "": + self.index += 1 + return c + + +def tokenize(query: string) -> list[Token]: + """Turn a query into a list of tokens.""" + result = [] + + cursor = Cursor(query) + while True: + idx = cursor.index + char = cursor.next() + + if char == "": + result.append(Token(TokenKind.EOF, "", idx, idx)) + break + + if char in string.ascii_letters: + char = cursor.peek() + + while char in string.ascii_letters + ".": + cursor.next() + char = cursor.peek() + if char == "": + break + + identifier = cursor.contents[idx : cursor.index] + kind = KEYWORDS.get(identifier, TokenKind.IDENTIFIER) + result.append(Token(kind, identifier, idx, cursor.index)) + + elif char == ",": + result.append(Token(TokenKind.COMMA, ",", idx, cursor.index)) + + elif char == "*": + result.append(Token(TokenKind.STAR, ",", idx, cursor.index)) + + elif char == "'": + # idk escaping rules in SQL lol + char = cursor.peek() + while char != "'": + cursor.next() + char = cursor.peek() + if char == "": + break + + cursor.next() # get the last ' + + string_result = cursor.contents[idx : cursor.index + 1] + print(string_result) + kind = TokenKind.STRING if string_result.endswith("'") and len(string_result) > 1 else TokenKind.ERROR + result.append(Token(kind, string_result, idx, cursor.index + 1)) + + return result + + +def check_tok(before: str, after: TokenKind) -> None: + """Test helper which checks a string tokenizes to a single given token kind.""" + assert [tok.kind for tok in tokenize(before)] == [after, TokenKind.EOF] + + +def stringify_tokens(query: str) -> str: + """Test helper which turns a query into a repr of the tokens. + + Used for manual snapshot testing. + """ + tokens = tokenize(query) + result = "" + for i, c in enumerate(query): + for tok in tokens: + if tok.start_pos == i: + result += ">" + + for tok in tokens: + if tok.end_pos == i: + result += "<" + + result += c + + i += 1 + for tok in tokens[:-1]: # don't print EOF + if tok.end_pos == i: + result += "<" + + return result + + +def test_simple_tokens() -> None: + """Tests that various things tokenize correct in minimal cases.""" + assert [tok.kind for tok in tokenize("")] == [TokenKind.EOF] + check_tok("SELECT", TokenKind.SELECT) + check_tok("FROM", TokenKind.FROM) + check_tok("'hello :)'", TokenKind.STRING) + check_tok(",", TokenKind.COMMA) + check_tok("*", TokenKind.STAR) + check_tok("username", TokenKind.IDENTIFIER) + + +def test_tokenize_simple_select() -> None: + """Tests that tokenization works in more general cases.""" + assert stringify_tokens("SELECT * FROM posts") == ">SELECT< >*< >FROM< >posts<" + + +if __name__ == "__main__": + query = input("query> ") + print(stringify_tokens(query)) + from pprint import pprint + + print() + pprint(tokenize(query)) From 1ff603ad8321eba4a9d185934a6fb65a0eaae3ef Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 10 Aug 2025 04:13:16 +0900 Subject: [PATCH 005/117] Remove EOF tokens --- README.md | 4 +++- pyproject.toml | 2 ++ src/parser.py | 9 ++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cd662164..79c107f2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ To get a web server up: To run `pre-commit` on everything: `pre-commit run -a`. -To download all dependencies: `pip install -r requirements.txt` +To download all dependencies: `pip install -r requirements.txt`. + +To run tests, use `pytest`. Currently, you will need to do `pytest src/parser.py`. --- diff --git a/pyproject.toml b/pyproject.toml index fcb5675f..e88e3fc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,8 @@ ignore = [ "FIX", # Function complexity. "C901", + # Conflicts with ruff format. + "COM812", # TODO: set up proper testing structure "S101", ] diff --git a/src/parser.py b/src/parser.py index 2d05e29b..9f2bc3fc 100644 --- a/src/parser.py +++ b/src/parser.py @@ -5,6 +5,7 @@ from enum import Enum, auto +# tokenizer: @dataclass class Token: """A token produced by tokenization.""" @@ -29,7 +30,6 @@ class TokenKind(Enum): # structure COMMA = auto() STAR = auto() - EOF = auto() ERROR = auto() @@ -68,7 +68,6 @@ def tokenize(query: string) -> list[Token]: char = cursor.next() if char == "": - result.append(Token(TokenKind.EOF, "", idx, idx)) break if char in string.ascii_letters: @@ -111,7 +110,7 @@ def tokenize(query: string) -> list[Token]: def check_tok(before: str, after: TokenKind) -> None: """Test helper which checks a string tokenizes to a single given token kind.""" - assert [tok.kind for tok in tokenize(before)] == [after, TokenKind.EOF] + assert [tok.kind for tok in tokenize(before)] == [after] def stringify_tokens(query: str) -> str: @@ -133,7 +132,7 @@ def stringify_tokens(query: str) -> str: result += c i += 1 - for tok in tokens[:-1]: # don't print EOF + for tok in tokens: if tok.end_pos == i: result += "<" @@ -142,7 +141,7 @@ def stringify_tokens(query: str) -> str: def test_simple_tokens() -> None: """Tests that various things tokenize correct in minimal cases.""" - assert [tok.kind for tok in tokenize("")] == [TokenKind.EOF] + assert [tok.kind for tok in tokenize("")] == [] check_tok("SELECT", TokenKind.SELECT) check_tok("FROM", TokenKind.FROM) check_tok("'hello :)'", TokenKind.STRING) From 9f6a2200f17b7a7c4bfb5ebb4b4b08a5c3cf014a Mon Sep 17 00:00:00 2001 From: micus NoName <108684103+Drakariboo@users.noreply.github.com> Date: Sat, 9 Aug 2025 18:40:19 -0400 Subject: [PATCH 006/117] script python to send request with authentication & add atproto to requirement --- auth-request.py | 41 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ 2 files changed, 43 insertions(+) create mode 100644 auth-request.py diff --git a/auth-request.py b/auth-request.py new file mode 100644 index 00000000..7601fc91 --- /dev/null +++ b/auth-request.py @@ -0,0 +1,41 @@ +# Imports +from atproto import Client + + +class Session: + """Class to etablish an auth session""" + def __init__(self, username: str, password: str): + #Bluesky credentials + self.username = username + self.password = password + #Instance client + self.client = Client() + #Access token + self.access_jwt = None + #Refresh token + self.refresh_jwt = None + + def login(self): + """Create an authenticated session and save tokens.""" + session_info = self.client.login(self.username, self.password) + self.access_jwt = session_info.accessJwt + self.refresh_jwt = session_info.refreshJwt + print("Connexion réussie.") + print("Access token :", self.access_jwt) + print("Refresh token :", self.refresh_jwt) + + def get_profile(self): + """Example : get user profile""" + profile = self.client.app.bsky.actor.get_profile({'actor': self.username}) + return profile + + +if __name__ == "__main__": + USERNAME = "Nothing_AHAHA" + PASSWORD = "You tought i'll write the password here you fool" + + session = Session(USERNAME, PASSWORD) + session.login() + + profile = session.get_profile() + print("Nom affiché :", profile.displayName) diff --git a/requirements.txt b/requirements.txt index ef00f24b..9884ac7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,5 @@ ruff==0.12.8 # via sql-bsky (pyproject.toml:dev) virtualenv==20.33.1 # via pre-commit + +atproto~=0.0.61 \ No newline at end of file From a2c2bc868d783c977408038a426dd6ade40519d6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 Aug 2025 19:09:06 -0500 Subject: [PATCH 007/117] feat: requests based implementation of atproto session based on @mimic version --- auth-request.py => auth-request-atproto.py | 0 auth-request-requests.py | 56 ++++++++++++++++++++++ requirements.txt | 2 - 3 files changed, 56 insertions(+), 2 deletions(-) rename auth-request.py => auth-request-atproto.py (100%) create mode 100644 auth-request-requests.py diff --git a/auth-request.py b/auth-request-atproto.py similarity index 100% rename from auth-request.py rename to auth-request-atproto.py diff --git a/auth-request-requests.py b/auth-request-requests.py new file mode 100644 index 00000000..1ddf9135 --- /dev/null +++ b/auth-request-requests.py @@ -0,0 +1,56 @@ +# Imports +import requests # The system we will actually use + + + +class Session: + """Class to etablish an auth session""" + def __init__(self, username: str, password: str): + #Bluesky credentials + self.username = username + self.password = password + self.pds_host = "https://bsky.social" + #Instance client + #Access token + self.access_jwt = None + #Refresh token + self.refresh_jwt = None + + def login(self): + """Create an authenticated session and save tokens.""" + endpoint = f"{self.pds_host}/xrpc/com.atproto.server.createSession" + session_info = requests.post( + endpoint, + headers={"Content-Type": "application/json"}, + json = { + "identifier":self.username, + "password":self.password, + }, + timeout=30, + ).json() + self.access_jwt = session_info["accessJwt"] + self.refresh_jwt = session_info["refreshJwt"] + print("Connexion réussie.") + print("Access token :", self.access_jwt) + print("Refresh token :", self.refresh_jwt) + + def get_profile(self): + """Example : get user profile""" + endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={self.username}" + return requests.get( + endpoint, + headers={"Content-Type": "application/json", "Authorization": f"Bearer {self.access_jwt}"}, + timeout=30, + ).json() + + +if __name__ == "__main__": + USERNAME = "Nothing_AHAHA" + PASSWORD = "You thought i'll write the password here you fool" + + session = Session(USERNAME, PASSWORD) + session.login() + + profile = session.get_profile() + + print(profile) diff --git a/requirements.txt b/requirements.txt index 9884ac7f..ef00f24b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,5 +20,3 @@ ruff==0.12.8 # via sql-bsky (pyproject.toml:dev) virtualenv==20.33.1 # via pre-commit - -atproto~=0.0.61 \ No newline at end of file From bcacb08797b5b06177a79f8a42a414f5baeda2a3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 Aug 2025 19:13:34 -0500 Subject: [PATCH 008/117] lint: linting the auth requests --- auth-request-atproto.py | 24 ++++++++++++------------ auth-request-requests.py | 38 +++++++++++++++++++------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/auth-request-atproto.py b/auth-request-atproto.py index 7601fc91..52f894d1 100644 --- a/auth-request-atproto.py +++ b/auth-request-atproto.py @@ -3,19 +3,20 @@ class Session: - """Class to etablish an auth session""" - def __init__(self, username: str, password: str): - #Bluesky credentials + """Class to etablish an auth session.""" + + def __init__(self, username: str, password: str) -> None: + # Bluesky credentials self.username = username self.password = password - #Instance client + # Instance client self.client = Client() - #Access token + # Access token self.access_jwt = None - #Refresh token + # Refresh token self.refresh_jwt = None - def login(self): + def login(self) -> None: """Create an authenticated session and save tokens.""" session_info = self.client.login(self.username, self.password) self.access_jwt = session_info.accessJwt @@ -24,15 +25,14 @@ def login(self): print("Access token :", self.access_jwt) print("Refresh token :", self.refresh_jwt) - def get_profile(self): - """Example : get user profile""" - profile = self.client.app.bsky.actor.get_profile({'actor': self.username}) - return profile + def get_profile(self) -> dict: + """Get user profile.""" + return self.client.app.bsky.actor.get_profile({"actor": self.username}) if __name__ == "__main__": USERNAME = "Nothing_AHAHA" - PASSWORD = "You tought i'll write the password here you fool" + PASSWORD = "You tought i'll write the password here you fool" # noqa: S105 session = Session(USERNAME, PASSWORD) session.login() diff --git a/auth-request-requests.py b/auth-request-requests.py index 1ddf9135..1caf483a 100644 --- a/auth-request-requests.py +++ b/auth-request-requests.py @@ -2,40 +2,40 @@ import requests # The system we will actually use - class Session: - """Class to etablish an auth session""" - def __init__(self, username: str, password: str): - #Bluesky credentials + """Class to etablish an auth session.""" + + def __init__(self, username: str, password: str) -> None: + # Bluesky credentials self.username = username self.password = password self.pds_host = "https://bsky.social" - #Instance client - #Access token + # Instance client + # Access token self.access_jwt = None - #Refresh token + # Refresh token self.refresh_jwt = None - def login(self): + def login(self) -> None: """Create an authenticated session and save tokens.""" endpoint = f"{self.pds_host}/xrpc/com.atproto.server.createSession" session_info = requests.post( - endpoint, - headers={"Content-Type": "application/json"}, - json = { - "identifier":self.username, - "password":self.password, - }, - timeout=30, - ).json() + endpoint, + headers={"Content-Type": "application/json"}, + json={ + "identifier": self.username, + "password": self.password, + }, + timeout=30, + ).json() self.access_jwt = session_info["accessJwt"] self.refresh_jwt = session_info["refreshJwt"] print("Connexion réussie.") print("Access token :", self.access_jwt) print("Refresh token :", self.refresh_jwt) - def get_profile(self): - """Example : get user profile""" + def get_profile(self) -> dict: + """Get a user profile.""" endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={self.username}" return requests.get( endpoint, @@ -46,7 +46,7 @@ def get_profile(self): if __name__ == "__main__": USERNAME = "Nothing_AHAHA" - PASSWORD = "You thought i'll write the password here you fool" + PASSWORD = "You thought i'll write the password here you fool" # noqa: S105 session = Session(USERNAME, PASSWORD) session.login() From a675095da4f62d5978feb3bbe87d9da7dfb67594 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 Aug 2025 19:32:46 -0500 Subject: [PATCH 009/117] feat: added feeds --- auth-request-requests.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/auth-request-requests.py b/auth-request-requests.py index 1caf483a..5949c16c 100644 --- a/auth-request-requests.py +++ b/auth-request-requests.py @@ -11,6 +11,7 @@ def __init__(self, username: str, password: str) -> None: self.password = password self.pds_host = "https://bsky.social" # Instance client + self.client = requests.Session() # Access token self.access_jwt = None # Refresh token @@ -19,7 +20,7 @@ def __init__(self, username: str, password: str) -> None: def login(self) -> None: """Create an authenticated session and save tokens.""" endpoint = f"{self.pds_host}/xrpc/com.atproto.server.createSession" - session_info = requests.post( + session_info = self.client.post( endpoint, headers={"Content-Type": "application/json"}, json={ @@ -30,6 +31,13 @@ def login(self) -> None: ).json() self.access_jwt = session_info["accessJwt"] self.refresh_jwt = session_info["refreshJwt"] + self.client.headers.update( + { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_jwt}", + }, + ) + print("Connexion réussie.") print("Access token :", self.access_jwt) print("Refresh token :", self.refresh_jwt) @@ -37,16 +45,29 @@ def login(self) -> None: def get_profile(self) -> dict: """Get a user profile.""" endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={self.username}" - return requests.get( + return self.client.get( + endpoint, + ).json() + + def search(self, query: str) -> dict: + """Search Bluesky.""" + endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.searchActors?q={query}" + return self.client.get( + endpoint, + ).json() + + def get_author_feed(self, actor: str) -> dict: + """Get a specific user feed.""" + endpoint = f"{self.pds_host}/xrpc/app.bsky.feed.getAuthorFeed?actor={actor}" + print(endpoint) + return self.client.get( endpoint, - headers={"Content-Type": "application/json", "Authorization": f"Bearer {self.access_jwt}"}, - timeout=30, ).json() if __name__ == "__main__": USERNAME = "Nothing_AHAHA" - PASSWORD = "You thought i'll write the password here you fool" # noqa: S105 + PASSWORD = "You tought i'll write the password here you fool" # noqa: S105 session = Session(USERNAME, PASSWORD) session.login() @@ -54,3 +75,6 @@ def get_profile(self) -> dict: profile = session.get_profile() print(profile) + + search = session.get_actor_feeds("tess.bsky.social") + print(search) From 3df61a9a130db87f4c3eb6b4194e39c8b98dcb98 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sat, 9 Aug 2025 20:48:34 -0400 Subject: [PATCH 010/117] feat: add retro terminal ui interface Added clean ui implementation with boot sequence and visual effects. References setup.py for pyodide integration. Closes #4 --- src/boot.js | 259 +++++++++++++++++++++ src/frontend.js | 298 ++++++++++++++++++++++++ src/index.html | 138 +++++++++++ src/styles.css | 608 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1303 insertions(+) create mode 100644 src/boot.js create mode 100644 src/frontend.js create mode 100644 src/index.html create mode 100644 src/styles.css diff --git a/src/boot.js b/src/boot.js new file mode 100644 index 00000000..629fe45c --- /dev/null +++ b/src/boot.js @@ -0,0 +1,259 @@ +// boot.js +// call window.startBootSequence() to begin, window.finishBoot() when pyodide is ready + +const bootMessages = [ + { + text: "BIOS Version 2.1.87 - Copyright (C) 1987 Iridescent Ivies", + delay: 500, + }, + { text: "Memory Test: 640K OK", delay: 800 }, + { text: "Extended Memory Test: 15360K OK", delay: 600 }, + { text: "", delay: 200 }, + { text: "Detecting Hardware...", delay: 400 }, + { text: " - Primary Hard Disk.......... OK", delay: 300 }, + { text: " - Network Interface.......... OK", delay: 300 }, + { text: " - Math Coprocessor........... OK", delay: 400 }, + { text: "", delay: 200 }, + { text: "Loading SQL Social Network v1.0...", delay: 600 }, + { text: "Initializing Python Runtime Environment...", delay: 800 }, + { text: "Loading Pyodide Kernel", delay: 1000, showProgress: true }, + { text: "Installing pandas...", delay: 2000 }, + { text: "Installing sqlalchemy...", delay: 1500 }, + { text: "Configuring data structures...", delay: 800 }, + { text: "Establishing database connections...", delay: 600 }, + { text: "Loading sample datasets...", delay: 400 }, + { text: "", delay: 200 }, + { text: "System Ready!", delay: 300, blink: true }, + { text: "Press any key to continue...", delay: 500, blink: true }, +]; + +let bootScreen = null; +let isBootComplete = false; +let continuePressed = false; + +window.startBootSequence = async function () { + continuePressed = false; + + bootScreen = document.createElement("div"); + bootScreen.className = "boot-screen"; + bootScreen.innerHTML = '
'; + + document.body.appendChild(bootScreen); + document.querySelector(".interface").style.opacity = "0"; + + await showBootMessages(); + await waitForContinue(); + + isBootComplete = true; +}; + +// hide boot screen and show main interface +window.finishBoot = function () { + if (bootScreen) { + bootScreen.style.opacity = "0"; + bootScreen.style.transition = "opacity 0.5s ease"; + + setTimeout(() => { + if (bootScreen && bootScreen.parentNode) { + document.body.removeChild(bootScreen); + } + bootScreen = null; + }, 500); + } + + // show main interface + const mainInterface = document.querySelector(".interface"); + mainInterface.style.transition = "opacity 0.5s ease"; + mainInterface.style.opacity = "1"; + + console.log("boot sequence complete - system ready"); +}; + +window.isBootComplete = function () { + return isBootComplete; +}; + +// show boot messages +async function showBootMessages() { + const bootContent = document.getElementById("boot-content"); + + for (let i = 0; i < bootMessages.length; i++) { + if (continuePressed) { + const remainingMessages = bootMessages.slice(i); + remainingMessages.forEach(msg => { + const line = document.createElement("div"); + line.className = "boot-line boot-show"; + line.textContent = msg.text; + if (msg.blink) line.classList.add("boot-blink"); + bootContent.appendChild(line); + }); + break; + } + + const message = bootMessages[i]; + const line = document.createElement("div"); + line.className = "boot-line"; + + if (message.showProgress) { + line.innerHTML = + message.text + + '
'; + } else { + line.textContent = message.text; + } + + if (message.blink) { + line.classList.add("boot-blink"); + } + + bootContent.appendChild(line); + + setTimeout(() => { + line.classList.add("boot-show"); + }, 50); + + if (message.showProgress) { + await animateProgressBar("progress-bar-" + i); + } + + await new Promise((resolve) => setTimeout(resolve, message.delay)); + } +} + +function animateProgressBar(barId) { + return new Promise((resolve) => { + const progressBar = document.getElementById(barId); + if (!progressBar) { + resolve(); + return; + } + + let progress = 0; + const interval = setInterval(() => { + if (continuePressed) { + progress = 100; + clearInterval(interval); + resolve(); + return; + } + + progress += Math.random() * 15; + if (progress >= 100) { + progress = 100; + clearInterval(interval); + resolve(); + } + progressBar.style.width = progress + "%"; + }, 100); + }); +} + +function waitForContinue() { + return new Promise((resolve) => { + const handleInteraction = (e) => { + e.preventDefault(); + e.stopPropagation(); + continuePressed = true; + + document.removeEventListener("keydown", handleInteraction, true); + document.removeEventListener("click", handleInteraction, true); + bootScreen.removeEventListener("click", handleInteraction, true); + + resolve(); + }; + + document.addEventListener("keydown", handleInteraction, true); + document.addEventListener("click", handleInteraction, true); + + if (bootScreen) { + bootScreen.addEventListener("click", handleInteraction, true); + } + + const timeoutId = setTimeout(() => { + if (!continuePressed) { + continuePressed = true; + + document.removeEventListener("keydown", handleInteraction, true); + document.removeEventListener("click", handleInteraction, true); + if (bootScreen) { + bootScreen.removeEventListener("click", handleInteraction, true); + } + + resolve(); + } + }, 3000); + + const originalResolve = resolve; + resolve = () => { + clearTimeout(timeoutId); + originalResolve(); + }; + }); +} + +// styles for boot screen (inject into document head) +const bootStyles = ` +.boot-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #000; + color: #00ff00; + font-family: "JetBrains Mono", "Courier New", monospace; + z-index: 10000; + padding: 20px; + font-size: 14px; + line-height: 1.4; + overflow-y: auto; + cursor: pointer; +} + +.boot-content { + max-width: 800px; + margin: 0 auto; +} + +.boot-line { + opacity: 0; + margin-bottom: 2px; + transition: opacity 0.3s ease; +} + +.boot-line.boot-show { + opacity: 1; +} + +.boot-line.boot-blink { + animation: bootBlink 0.5s infinite; +} + +.boot-progress { + display: inline-block; + width: 200px; + height: 8px; + border: 1px solid #00ff00; + margin-left: 10px; + position: relative; + vertical-align: middle; +} + +.boot-progress-bar { + height: 100%; + background: #00ff00; + width: 0%; + transition: width 0.5s ease; +} + +@keyframes bootBlink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} +`; + +const styleSheet = document.createElement("style"); +styleSheet.textContent = bootStyles; +document.head.appendChild(styleSheet); \ No newline at end of file diff --git a/src/frontend.js b/src/frontend.js new file mode 100644 index 00000000..587eee43 --- /dev/null +++ b/src/frontend.js @@ -0,0 +1,298 @@ +// this file only handles visuals +// backend calls these functions when it needs some visual changes +// all these can be done via pyodide, I just leave it as a reference for now + +// dom elements +const queryInput = document.getElementById('query-input'); +const executeBtn = document.getElementById('execute-btn'); +const cancelBtn = document.getElementById('cancel-btn'); +const clearBtn = document.getElementById('clear-btn'); +const tableHead = document.getElementById('table-head'); +const tableBody = document.getElementById('table-body'); +const statusMessage = document.getElementById('status-message'); +const connectionInfo = document.getElementById('connection-info'); +const loadingOverlay = document.getElementById('loading-overlay'); +const electricWave = document.getElementById('electric-wave'); + + +/** + * update status message with visual effects + * @param {string} message - message to show + * @param {string} type - 'success', 'error', 'warning', 'info' + */ +window.updateStatus = function(message, type = 'info') { + statusMessage.textContent = message; + statusMessage.className = `status-${type}`; + + // blink effect for errors + if (type === 'error') { + statusMessage.style.animation = 'blink 0.5s 3'; + setTimeout(() => { + statusMessage.style.animation = ''; + }, 1500); + } +}; + +/** + * update connection info and row count + * @param {number} rowCount - number of rows + * @param {string} status - connection status + */ +window.updateConnectionInfo = function(rowCount = 0, status = 'ready') { + connectionInfo.textContent = `rows: ${rowCount} | status: ${status}`; +}; + +/** + * show/hide loading overlay with spinner + * @param {boolean} show - true to show + */ +window.showLoading = function(show = true) { + if (show) { + loadingOverlay.classList.add('show'); + triggerElectricWave(); // automatic effect when loading + } else { + loadingOverlay.classList.remove('show'); + } +}; + +/** + * trigger the electric wave effect + */ +window.triggerElectricWave = function() { + electricWave.classList.remove('active'); + setTimeout(() => { + electricWave.classList.add('active'); + }, 10); +}; + +/** + * populate table with data and appearing effects + * @param {Array} headers - column names + * @param {Array} rows - row data + */ +window.updateTable = function(headers, rows) { + // fade out effect before updating + tableHead.style.opacity = '0.3'; + tableBody.style.opacity = '0.3'; + + setTimeout(() => { + // clear table + tableHead.innerHTML = ''; + tableBody.innerHTML = ''; + + if (!headers || !rows || rows.length === 0) { + showEmptyTable(); + return; + } + + // create headers with staggered animation + const headerRow = document.createElement('tr'); + headers.forEach((header, index) => { + const th = document.createElement('th'); + th.textContent = header.toUpperCase(); + th.style.opacity = '0'; + headerRow.appendChild(th); + + // staggered header animation + setTimeout(() => { + th.style.transition = 'opacity 0.3s ease'; + th.style.opacity = '1'; + }, index * 50); + }); + tableHead.appendChild(headerRow); + + // create rows with appearing effect + rows.forEach((rowData, rowIndex) => { + const tr = document.createElement('tr'); + tr.style.opacity = '0'; + + const cellValues = Array.isArray(rowData) + ? rowData + : headers.map(header => rowData[header] || ''); + + cellValues.forEach(cellData => { + const td = document.createElement('td'); + td.textContent = cellData || ''; + tr.appendChild(td); + }); + + tableBody.appendChild(tr); + + // staggered row animation + setTimeout(() => { + tr.style.transition = 'opacity 0.4s ease'; + tr.style.opacity = '1'; + }, (rowIndex * 100) + 200); + }); + + // restore container opacity + setTimeout(() => { + tableHead.style.opacity = '1'; + tableBody.style.opacity = '1'; + }, 300); + + // update counter + window.updateConnectionInfo(rows.length, 'connected'); + + // final success effect + setTimeout(() => { + window.triggerElectricWave(); + }, (rows.length * 100) + 500); + + }, 200); +}; + +/** + * show empty table state + */ +function showEmptyTable() { + const emptyRow = document.createElement('tr'); + const emptyCell = document.createElement('td'); + emptyCell.textContent = 'no data found'; + emptyCell.colSpan = 8; + emptyCell.style.textAlign = 'center'; + emptyCell.style.padding = '40px 20px'; + emptyCell.style.color = '#666'; + emptyCell.style.fontStyle = 'italic'; + emptyRow.appendChild(emptyCell); + tableBody.appendChild(emptyRow); + + tableHead.style.opacity = '1'; + tableBody.style.opacity = '1'; + window.updateConnectionInfo(0, 'no results'); +} + +/** + * enable/disable buttons with visual effects + * @param {boolean} disabled - true to disable + */ +window.setButtonsDisabled = function(disabled) { + executeBtn.disabled = disabled; + clearBtn.disabled = disabled; + cancelBtn.disabled = !disabled; // cancel only available when executing + + // visual effects on buttons + if (disabled) { + executeBtn.style.opacity = '0.5'; + clearBtn.style.opacity = '0.5'; + cancelBtn.style.opacity = '1'; + cancelBtn.style.animation = 'blink 1s infinite'; + } else { + executeBtn.style.opacity = '1'; + clearBtn.style.opacity = '1'; + cancelBtn.style.opacity = '0.7'; + cancelBtn.style.animation = ''; + } +}; + +/** + * get current query from input + * @returns {string} current query + */ +window.getCurrentQuery = function() { + return queryInput.value.trim(); +}; + +/** + * clear input with fade effect + */ +window.clearQueryInput = function() { + // fade out effect + queryInput.style.opacity = '0.3'; + setTimeout(() => { + queryInput.value = ''; + queryInput.style.transition = 'opacity 0.3s ease'; + queryInput.style.opacity = '1'; + }, 150); +}; + +/** + * clear entire interface + */ +window.clearInterface = function() { + window.clearQueryInput(); + showEmptyTable(); + window.updateStatus('interface cleared', 'info'); + window.updateConnectionInfo(0, 'waiting'); +}; + +/** + * error effect on input field + */ +window.showInputError = function() { + queryInput.style.borderColor = '#ff0000'; + queryInput.style.boxShadow = 'inset 0 0 10px rgba(255, 0, 0, 0.3)'; + + setTimeout(() => { + queryInput.style.borderColor = '#00ff00'; + queryInput.style.boxShadow = 'inset 0 0 5px rgba(0, 255, 0, 0.3)'; + }, 1000); +}; + +/** + * success effect on input field + */ +window.showInputSuccess = function() { + queryInput.style.borderColor = '#00ff00'; + queryInput.style.boxShadow = 'inset 0 0 10px rgba(0, 255, 0, 0.5)'; + + setTimeout(() => { + queryInput.style.boxShadow = 'inset 0 0 5px rgba(0, 255, 0, 0.3)'; + }, 1000); +}; + +/** + * fullscreen flash effect for important results + */ +window.flashScreen = function(color = '#00ff00', duration = 200) { + const flash = document.createElement('div'); + flash.style.position = 'fixed'; + flash.style.top = '0'; + flash.style.left = '0'; + flash.style.width = '100%'; + flash.style.height = '100%'; + flash.style.backgroundColor = color; + flash.style.opacity = '0.1'; + flash.style.pointerEvents = 'none'; + flash.style.zIndex = '9999'; + + document.body.appendChild(flash); + + setTimeout(() => { + flash.style.transition = `opacity ${duration}ms ease`; + flash.style.opacity = '0'; + setTimeout(() => { + document.body.removeChild(flash); + }, duration); + }, 50); +}; + +// automatic system effects + +// occasional screen flicker (retro effect) +setInterval(() => { + if (Math.random() < 0.05) { + document.querySelector('.screen').style.opacity = '0.9'; + setTimeout(() => { + document.querySelector('.screen').style.opacity = '1'; + }, 100); + } +}, 5000); + +// random electric wave (ambient effect) +setInterval(() => { + if (Math.random() < 0.03) { + window.triggerElectricWave(); + } +}, 8000); + + + +document.addEventListener('DOMContentLoaded', function() { + // setup initial ui + window.updateStatus('system ready', 'success'); + window.updateConnectionInfo(0, 'waiting'); + showEmptyTable(); + + console.log("ready") +}) diff --git a/src/index.html b/src/index.html new file mode 100644 index 00000000..9f64ee31 --- /dev/null +++ b/src/index.html @@ -0,0 +1,138 @@ + + + + + + The Social Query Language v1.0 + + + + + + +
+ +
+ +
+
+ The Social Query Language v1.0 + © 1987 Iridescent Ivies +
+ +
+
+ +
+
SQL COMMAND
+
+ +
+ + + +
+
+
+ + +
+
QUERY RESULTS
+ + +
+
+ + + + + + + + + +
+ No data loaded. Execute a query to see results. +
+ + + +
+ Ready + Rows: 0 | Status: Waiting +
+ + + + + + + +
+
+
+
EXECUTING QUERY...
+
+
+ + + + + + diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 00000000..67f8a1fc --- /dev/null +++ b/src/styles.css @@ -0,0 +1,608 @@ +/* fonts */ +@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap"); + +@keyframes flicker { + 0% { + opacity: 0.27861; + } + 5% { + opacity: 0.34769; + } + 10% { + opacity: 0.23604; + } + 15% { + opacity: 0.90626; + } + 20% { + opacity: 0.18128; + } + 25% { + opacity: 0.83891; + } + 30% { + opacity: 0.65583; + } + 35% { + opacity: 0.67807; + } + 40% { + opacity: 0.26559; + } + 45% { + opacity: 0.84693; + } + 50% { + opacity: 0.96019; + } + 55% { + opacity: 0.08594; + } + 60% { + opacity: 0.20313; + } + 65% { + opacity: 0.71988; + } + 70% { + opacity: 0.53455; + } + 75% { + opacity: 0.37288; + } + 80% { + opacity: 0.71428; + } + 85% { + opacity: 0.70419; + } + 90% { + opacity: 0.7003; + } + 95% { + opacity: 0.36108; + } + 100% { + opacity: 0.24387; + } +} + + +@keyframes textShadow { + 0% { + text-shadow: 0.4389924193300864px 0 1px rgba(0,30,255,0.5), -0.4389924193300864px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 5% { + text-shadow: 2.7928974010788217px 0 1px rgba(0,30,255,0.5), -2.7928974010788217px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 10% { + text-shadow: 0.02956275843481219px 0 1px rgba(0,30,255,0.5), -0.02956275843481219px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 15% { + text-shadow: 0.40218538552878136px 0 1px rgba(0,30,255,0.5), -0.40218538552878136px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 20% { + text-shadow: 3.4794037899852017px 0 1px rgba(0,30,255,0.5), -3.4794037899852017px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 25% { + text-shadow: 1.6125630401149584px 0 1px rgba(0,30,255,0.5), -1.6125630401149584px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 30% { + text-shadow: 0.7015590085143956px 0 1px rgba(0,30,255,0.5), -0.7015590085143956px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 35% { + text-shadow: 3.896914047650351px 0 1px rgba(0,30,255,0.5), -3.896914047650351px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 40% { + text-shadow: 3.870905614848819px 0 1px rgba(0,30,255,0.5), -3.870905614848819px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 45% { + text-shadow: 2.231056963361899px 0 1px rgba(0,30,255,0.5), -2.231056963361899px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 50% { + text-shadow: 0.08084290417898504px 0 1px rgba(0,30,255,0.5), -0.08084290417898504px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 55% { + text-shadow: 2.3758461067427543px 0 1px rgba(0,30,255,0.5), -2.3758461067427543px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 60% { + text-shadow: 2.202193051050636px 0 1px rgba(0,30,255,0.5), -2.202193051050636px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 65% { + text-shadow: 2.8638780614874975px 0 1px rgba(0,30,255,0.5), -2.8638780614874975px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 70% { + text-shadow: 0.48874025155497314px 0 1px rgba(0,30,255,0.5), -0.48874025155497314px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 75% { + text-shadow: 1.8948491305757957px 0 1px rgba(0,30,255,0.5), -1.8948491305757957px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 80% { + text-shadow: 0.0833037308038857px 0 1px rgba(0,30,255,0.5), -0.0833037308038857px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 85% { + text-shadow: 0.09769827255241735px 0 1px rgba(0,30,255,0.5), -0.09769827255241735px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 90% { + text-shadow: 3.443339761481782px 0 1px rgba(0,30,255,0.5), -3.443339761481782px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 95% { + text-shadow: 2.1841838852799786px 0 1px rgba(0,30,255,0.5), -2.1841838852799786px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } + 100% { + text-shadow: 2.6208764473832513px 0 1px rgba(0,30,255,0.5), -2.6208764473832513px 0 1px rgba(255,0,80,0.3), 0 0 3px; + } +} +.crt { + animation: textShadow 1.6s infinite; +} + +.crt::before { + content: " "; + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); + z-index: 2; + background-size: 100% 2px, 3px 100%; + pointer-events: none; +} +.crt::after { + content: " "; + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgba(18, 16, 16, 0.1); + opacity: 0; + z-index: 2; + pointer-events: none; + animation: flicker 0.15s infinite; +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* base layout */ +body { + font-family: "JetBrains Mono", "Courier New", monospace; + background: #000; + color: #00ff00; + height: 100vh; + overflow: hidden; +} + +.screen { + width: 100vw; + height: 100vh; + background: radial-gradient(ellipse at center, #001a00 0%, #000000 70%); + position: relative; + overflow: hidden; + box-shadow: inset 0 0 100px rgba(0, 255, 0, 0.1); +} + +/* crt effect */ +.screen::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 255, 0, 0.02) 2px, + rgba(0, 255, 0, 0.02) 4px + ); + pointer-events: none; + z-index: 10; +} + +.screen::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + ellipse at center, + transparent 60%, + rgba(0, 0, 0, 0.4) 100% + ); + pointer-events: none; + z-index: 5; +} + +/* electric wave */ +.electric-wave { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 0deg, + transparent 0%, + rgba(0, 255, 0, 0.1) 49%, + rgba(0, 255, 0, 0.3) 50%, + rgba(0, 255, 0, 0.1) 51%, + transparent 100% + ); + transform: translateY(-100%); + pointer-events: none; + z-index: 15; + opacity: 0; +} + +.electric-wave.active { + animation: electricWave 2s ease-out; +} + +@keyframes electricWave { + 0% { + transform: translateY(-100%); + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateY(100vh); + opacity: 0; + } +} + +/* main interface */ +.interface { + position: relative; + z-index: 1; + height: 100%; + padding: 30px; + display: flex; + flex-direction: column; +} + +.title-bar { + background: #00ff00; + color: #000; + padding: 8px 16px; + font-weight: bold; + font-size: 16px; + margin-bottom: 2px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.window { + border: 2px solid #00ff00; + background: #000; + flex: 1; + display: flex; + flex-direction: column; +} + +.content-area { + flex: 1; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* panels */ +.query-panel, +.results-panel { + border: 1px solid #00ff00; + background: #001100; +} + +.panel-header { + background: #003300; + padding: 4px 8px; + font-size: 14px; + font-weight: bold; + border-bottom: 1px solid #00ff00; +} + +.panel-content { + padding: 12px; +} + +/* query input */ +.sql-input { + width: 100%; + height: 100px; + background: #000; + border: 1px inset #00ff00; + color: #00ff00; + font-family: inherit; + font-size: 15px; + padding: 8px; + resize: none; + outline: none; +} + +.sql-input:focus { + background: #001100; + box-shadow: inset 0 0 5px rgba(0, 255, 0, 0.3); +} + +.sql-input::placeholder { + color: #006600; + opacity: 0.7; +} + +/* buttons */ +.button-row { + margin-top: 8px; + display: flex; + gap: 8px; +} + +.btn { + background: #003300; + border: 2px outset #00ff00; + color: #00ff00; + padding: 8px 20px; + font-family: inherit; + font-size: 14px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.2s ease; +} + +.btn:hover { + background: #004400; +} + +.btn:active { + border: 2px inset #00ff00; + background: #002200; +} + +.btn:disabled { + background: #001100; + color: #004400; + cursor: not-allowed; + border-color: #004400; +} + +.btn-danger { + background: #330000; + border-color: #ff0000; + color: #ff0000; +} + +.btn-danger:hover { + background: #440000; +} + +.btn-danger:active { + background: #220000; +} + +/* results table */ +.results-panel { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.table-area { + flex: 1; + overflow: auto; + background: #000; + border: 1px inset #00ff00; + margin: 12px; + max-height: calc(100vh - 400px); +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + min-width: 800px; +} + +.data-table th { + background: #003300; + border: 1px solid #00ff00; + padding: 8px 15px; + text-align: left; + font-weight: bold; + position: sticky; + top: 0; + z-index: 2; +} + +.data-table td { + border: 1px solid #004400; + padding: 8px 15px; + white-space: nowrap; +} + +.data-table tr:nth-child(even) { + background: #001100; +} + +.data-table tr:hover { + background: #002200; +} + +/* empty state */ +.data-table .empty-state { + text-align: center; + padding: 40px 20px; + color: #666; + font-style: italic; +} + +/* status line */ +.status-line { + background: #003300; + border-top: 1px solid #00ff00; + padding: 4px 12px; + font-size: 11px; + display: flex; + justify-content: space-between; + flex-shrink: 0; +} + +/* status message colors */ +.status-success { + color: #00ff00; +} + +.status-error { + color: #ff0000; +} + +.status-warning { + color: #ffff00; +} + +.status-info { + color: #00ffff; +} + +/* loading overlay */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 1000; + display: none; + align-items: center; + justify-content: center; +} + +.loading-overlay.show { + display: flex; +} + +.loading-content { + text-align: center; + color: #00ff00; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid #003300; + border-top: 3px solid #00ff00; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +.loading-text { + font-size: 18px; + font-weight: bold; + letter-spacing: 2px; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* scrollbars */ +.table-area::-webkit-scrollbar { + width: 16px; + height: 16px; +} + +.table-area::-webkit-scrollbar-track { + background: #003300; + border: 1px solid #00ff00; +} + +.table-area::-webkit-scrollbar-thumb { + background: #00ff00; + border: 1px solid #003300; +} + +.table-area::-webkit-scrollbar-corner { + background: #003300; +} + +/* responsive (it works :D!!) */ +@media (max-width: 768px) { + .interface { + padding: 10px; + } + + .button-row { + flex-direction: column; + } + + .btn { + width: 100%; + } + + .title-bar { + font-size: 14px; + padding: 6px 12px; + } + + .title-bar span:last-child { + display: none; + } + + .sql-input { + height: 80px; + font-size: 13px; + } + + .data-table { + font-size: 12px; + min-width: 600px; + } + + .data-table th, + .data-table td { + padding: 6px 10px; + } +} + +@media (max-width: 480px) { + .interface { + padding: 5px; + } + + .content-area { + padding: 8px; + gap: 8px; + } + + .panel-content { + padding: 8px; + } + + .sql-input { + height: 60px; + font-size: 12px; + } +} From 7a6691c663849e315fe79a8829e46db73262e1c0 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sat, 9 Aug 2025 21:14:21 -0400 Subject: [PATCH 011/117] edit: credits on the CRT effect --- src/styles.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/styles.css b/src/styles.css index 67f8a1fc..f09f56b1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,6 +1,9 @@ /* fonts */ @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap"); +/* Thanks to this article on the CRT effect: https://aleclownes.com/2017/02/01/crt-display.html */ +/* Read for reference on the values used */ + @keyframes flicker { 0% { opacity: 0.27861; From 36d4a03086235a5c552ea78f35844bf0a3169b14 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 Aug 2025 20:19:38 -0500 Subject: [PATCH 012/117] feat: added the pyfetch session and start of bsky api layer issue #6 --- src/auth_session.py | 98 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/auth_session.py diff --git a/src/auth_session.py b/src/auth_session.py new file mode 100644 index 00000000..b1fd974a --- /dev/null +++ b/src/auth_session.py @@ -0,0 +1,98 @@ +# Imports +import json + +from pyodide.http import FetchResponse, pyfetch # The system we will actually use + + +class PyfetchSession: + """Pyfetch Session, emulating the request Session.""" + + def __init__(self, headers: dict|None=None) -> None: + """Pyfetch Session, emulating the request Session.""" + self.default_headers = headers or {} + + async def get(self, url: str, headers:dict|None=None) -> FetchResponse: + """Get request for the pyfetch.""" + merged_headers = self.default_headers.copy() + if headers: + merged_headers.update(headers) + return await pyfetch( + url, + method="GET", + headers=merged_headers, + ) + + async def post(self, url:str, obj:dict|None="", data:dict|None=None, headers=None) -> FetchResponse: + """Post request.""" + merged_headers = self.default_headers.copy() + if headers: + merged_headers.update(headers) + return await pyfetch( + url, + method="POST", + headers=merged_headers, + body=json.dumps(obj) if isinstance(obj, dict) else data, + ) + + +class BskySession: + """Class to etablish an auth session.""" + + def __init__(self, username: str, password: str) -> None: + # Bluesky credentials + self.username = username + self.password = password + self.pds_host = "https://bsky.social" + # Instance client + self.client = PyfetchSession() + # Access token + self.access_jwt = None + # Refresh token + self.refresh_jwt = None + + async def login(self) -> None: + """Create an authenticated session and save tokens.""" + endpoint = f"{self.pds_host}/xrpc/com.atproto.server.createSession" + session_info = await self.client.post( + endpoint, + headers={"Content-Type": "application/json"}, + obj={ + "identifier": self.username, + "password": self.password, + }, + ) + session_info = await session_info.json() + self.access_jwt = session_info["accessJwt"] + self.refresh_jwt = session_info["refreshJwt"] + self.client.default_headers.update( + { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_jwt}", + }, + ) + + async def get_profile(self) -> dict: + """Get a user profile.""" + endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={self.username}" + response = await self.client.get( + endpoint, + ) + val = await response.json() + print(val) + return val + + def search(self, query: str) -> dict: + """Search Bluesky.""" + endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.searchActors?q={query}" + return self.client.get( + endpoint, + ).json() + + def get_author_feed(self, actor: str) -> dict: + """Get a specific user feed.""" + endpoint = f"{self.pds_host}/xrpc/app.bsky.feed.getAuthorFeed?actor={actor}" + print(endpoint) + return self.client.get( + endpoint, + ).json() + From e8931040f081e61d068376d7e5a364039a05e6a9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 Aug 2025 20:22:30 -0500 Subject: [PATCH 013/117] lint: making the linter happy --- src/auth_session.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/auth_session.py b/src/auth_session.py index b1fd974a..e8f382b1 100644 --- a/src/auth_session.py +++ b/src/auth_session.py @@ -7,11 +7,11 @@ class PyfetchSession: """Pyfetch Session, emulating the request Session.""" - def __init__(self, headers: dict|None=None) -> None: + def __init__(self, headers: dict | None = None) -> None: """Pyfetch Session, emulating the request Session.""" self.default_headers = headers or {} - async def get(self, url: str, headers:dict|None=None) -> FetchResponse: + async def get(self, url: str, headers: dict | None = None) -> FetchResponse: """Get request for the pyfetch.""" merged_headers = self.default_headers.copy() if headers: @@ -22,7 +22,13 @@ async def get(self, url: str, headers:dict|None=None) -> FetchResponse: headers=merged_headers, ) - async def post(self, url:str, obj:dict|None="", data:dict|None=None, headers=None) -> FetchResponse: + async def post( + self, + url: str, + obj: dict | None = "", + data: dict | None = None, + headers: dict | None = None, + ) -> FetchResponse: """Post request.""" merged_headers = self.default_headers.copy() if headers: @@ -95,4 +101,3 @@ def get_author_feed(self, actor: str) -> dict: return self.client.get( endpoint, ).json() - From 93e71ea7f59cecf98956776ce15244e79f13cc73 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 Aug 2025 21:01:51 -0500 Subject: [PATCH 014/117] feat: adding proper async await for the bskysession --- src/auth_session.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/auth_session.py b/src/auth_session.py index e8f382b1..daad7f89 100644 --- a/src/auth_session.py +++ b/src/auth_session.py @@ -83,21 +83,20 @@ async def get_profile(self) -> dict: response = await self.client.get( endpoint, ) - val = await response.json() - print(val) - return val + return await response.json() - def search(self, query: str) -> dict: + async def search(self, query: str) -> dict: """Search Bluesky.""" endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.searchActors?q={query}" - return self.client.get( + response = await self.client.get( endpoint, - ).json() + ) + return await response.json() - def get_author_feed(self, actor: str) -> dict: + async def get_author_feed(self, actor: str) -> dict: """Get a specific user feed.""" endpoint = f"{self.pds_host}/xrpc/app.bsky.feed.getAuthorFeed?actor={actor}" - print(endpoint) - return self.client.get( + response = await self.client.get( endpoint, - ).json() + ) + return await response.json() From d660c75bffa0decf00480d4db4d343487683aa6d Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 10 Aug 2025 11:55:47 +0900 Subject: [PATCH 015/117] Create a basic parser --- pyproject.toml | 2 +- src/parser.py | 237 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 229 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e88e3fc3..63effd16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,6 @@ ignore = [ "C901", # Conflicts with ruff format. "COM812", - # TODO: set up proper testing structure + # Asserts are good, actually. "S101", ] diff --git a/src/parser.py b/src/parser.py index 9f2bc3fc..a10e8f72 100644 --- a/src/parser.py +++ b/src/parser.py @@ -1,8 +1,10 @@ from __future__ import annotations import string -from dataclasses import dataclass +import textwrap +from dataclasses import dataclass, field from enum import Enum, auto +from typing import Literal # tokenizer: @@ -14,6 +16,7 @@ class Token: text: str start_pos: int end_pos: int + errors: list[str] = field(default_factory=list) class TokenKind(Enum): @@ -31,6 +34,7 @@ class TokenKind(Enum): COMMA = auto() STAR = auto() ERROR = auto() + EOF = auto() # this is a fake token only made and used in the parser KEYWORDS = { @@ -87,7 +91,7 @@ def tokenize(query: string) -> list[Token]: result.append(Token(TokenKind.COMMA, ",", idx, cursor.index)) elif char == "*": - result.append(Token(TokenKind.STAR, ",", idx, cursor.index)) + result.append(Token(TokenKind.STAR, "*", idx, cursor.index)) elif char == "'": # idk escaping rules in SQL lol @@ -101,13 +105,188 @@ def tokenize(query: string) -> list[Token]: cursor.next() # get the last ' string_result = cursor.contents[idx : cursor.index + 1] - print(string_result) kind = TokenKind.STRING if string_result.endswith("'") and len(string_result) > 1 else TokenKind.ERROR result.append(Token(kind, string_result, idx, cursor.index + 1)) return result +# parser +# heavily inspired by https://matklad.github.io/2023/05/21/resilient-ll-parsing-tutorial.html +@dataclass +class Parser: + """Helper class that provides useful parser functionality.""" + + contents: list[Token] + events: list[Event] = field(default_factory=list) + index: int = 0 + unreported_errors: list[str] = field(default_factory=list) + + def eof(self) -> bool: + """Check whether the token stream is done.""" + return self.index == len(self.contents) + + def peek(self) -> Token: + """Look at the next token in the stream.""" + if self.eof(): + return Token(TokenKind.EOF, "", -1, -1) + return self.contents[self.index] + + def advance(self) -> None: + """Move to the next token in the stream.""" + self.index += 1 + self.events.append("ADVANCE") + + def advance_with_error(self, error: str) -> None: + """Mark the current token as being wrong.""" + if self.eof(): + # this should probably be done better... + self.unreported_errors.append(error) + else: + self.contents[self.index].errors.append(error) + self.advance() + + def open(self) -> int: + """Start nesting children.""" + result = len(self.events) + self.events.append(("OPEN", ParentKind.ERROR_TREE)) + return result + + def close(self, kind: ParentKind, where: int) -> None: + """Stop nesting children and note the tree type.""" + self.events[where] = ("OPEN", kind) + self.events.append("CLOSE") + + def expect(self, kind: TokenKind, error: str) -> None: + """Ensure the next token is a specific kind and advance.""" + if self.at(kind): + self.advance() + else: + self.advance_with_error(error) + + def at(self, kind: TokenKind) -> None: + """Check if the next token is a specific kind.""" + return self.peek().kind == kind + + +@dataclass +class Parent: + """Syntax tree element with children.""" + + kind: ParentKind + children: list[Tree] + errors: list[str] = field(default_factory=list) + + +class ParentKind(Enum): + """Kinds of syntax tree elements that have children.""" + + SELECT_STMT = auto() + ERROR_TREE = auto() + FIELD_LIST = auto() + FROM_CLAUSE = auto() + EXPR_NAME = auto() + FILE = auto() + + +Tree = Parent | Token +Event = Literal["ADVANCE", "CLOSE"] | tuple[Literal["OPEN"], ParentKind] + + +def turn_tokens_into_events(tokens: list[Token]) -> list[Event]: + """Parse a token stream into a list of events.""" + parser = Parser(tokens, []) + while not parser.eof(): + _parse_stmt(parser) + return parser.events, parser.unreported_errors + + +def parse(tokens: list[Token]) -> Tree: + """Parse a token stream into a syntax tree.""" + events, errors = turn_tokens_into_events(tokens) + stack = [("OPEN", ParentKind.FILE)] + events.append("CLOSE") + + i = 0 + for event in events: + if event == "ADVANCE": + stack.append(tokens[i]) + i += 1 + elif event == "CLOSE": + inner = [] + while True: + e = stack.pop() + if isinstance(e, tuple) and e[0] == "OPEN": + inner.reverse() + stack.append(Parent(e[1], inner)) + break + inner.append(e) + else: + assert isinstance(event, tuple) + assert event[0] == "OPEN" + stack.append(event) + + assert i == len(tokens) + assert len(stack) == 1 + result = stack[0] + assert isinstance(result, Tree) + assert result.kind == ParentKind.FILE + result.errors.extend(errors) + return result + + +# free parser functions +def _parse_stmt(parser: Parser) -> None: + # + _parse_select_stmt(parser) + + +def _parse_select_stmt(parser: Parser) -> None: + # 'SELECT' [ ',' ]* [ 'FROM' IDENTIFIER ] + start = parser.open() + parser.expect(TokenKind.SELECT, "only SELECT is supported") + + fields_start = parser.open() + _parse_field(parser) + while parser.at(TokenKind.COMMA): + parser.advance() + _parse_field(parser) + parser.close(ParentKind.FIELD_LIST, fields_start) + + if parser.at(TokenKind.FROM): + # from clause + from_start = parser.open() + parser.advance() + + parser.expect(TokenKind.IDENTIFIER, "expected to select from a table") + parser.close(ParentKind.FROM_CLAUSE, from_start) + + parser.close(ParentKind.SELECT_STMT, start) + + +def _parse_field(parser: Parser) -> None: + # '*' | + if parser.at(TokenKind.STAR): + parser.advance() + else: + _parse_expr(parser) + + +def _parse_expr(parser: Parser) -> None: + # + _parse_small_expr(parser) + + +def _parse_small_expr(parser: Parser) -> None: + # IDENTIFIER + start = parser.open() + parser.expect(TokenKind.IDENTIFIER, "expected an expression") + parser.close(ParentKind.EXPR_NAME, start) + + +##### tests: (this should be moved to a proper tests folder) + + def check_tok(before: str, after: TokenKind) -> None: """Test helper which checks a string tokenizes to a single given token kind.""" assert [tok.kind for tok in tokenize(before)] == [after] @@ -121,14 +300,14 @@ def stringify_tokens(query: str) -> str: tokens = tokenize(query) result = "" for i, c in enumerate(query): - for tok in tokens: - if tok.start_pos == i: - result += ">" - for tok in tokens: if tok.end_pos == i: result += "<" + for tok in tokens: + if tok.start_pos == i: + result += ">" + result += c i += 1 @@ -139,6 +318,30 @@ def stringify_tokens(query: str) -> str: return result +def _stringify_tree(tree: Tree) -> list[str]: + result = [] + if isinstance(tree, Parent): + result.append(f"{tree.kind}") + result.extend(" " + line for child in tree.children for line in _stringify_tree(child)) + else: + repr = f'{tree.kind} ("{tree.text}")' + if tree.errors: + repr += " -- " + repr += " / ".join(tree.errors) + result.append(repr) + + return result + + +def stringify_tree(tree: Tree) -> str: + """Test helper that turns a syntax tree into a representation of it. + + Used for manual snapshot testing + """ + assert not tree.errors + return "\n".join(_stringify_tree(tree)) + + def test_simple_tokens() -> None: """Tests that various things tokenize correct in minimal cases.""" assert [tok.kind for tok in tokenize("")] == [] @@ -155,10 +358,26 @@ def test_tokenize_simple_select() -> None: assert stringify_tokens("SELECT * FROM posts") == ">SELECT< >*< >FROM< >posts<" +def test_parse_simple() -> None: + """Tests that parsing works in some specific cases.""" + assert ( + stringify_tree(parse(tokenize("SELECT * FROM posts"))) + == textwrap.dedent(""" + ParentKind.FILE + ParentKind.SELECT_STMT + TokenKind.SELECT ("SELECT") + ParentKind.FIELD_LIST + TokenKind.STAR ("*") + ParentKind.FROM_CLAUSE + TokenKind.FROM ("FROM") + TokenKind.IDENTIFIER ("posts") + """).strip() + ) + + if __name__ == "__main__": query = input("query> ") print(stringify_tokens(query)) - from pprint import pprint print() - pprint(tokenize(query)) + print(stringify_tree(parse(tokenize(query)))) From 0c57e63d6d0fc397df71e589383f6f939bdf30c4 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 10 Aug 2025 12:45:36 +0900 Subject: [PATCH 016/117] Start parsing where clauses --- src/parser.py | 136 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 23 deletions(-) diff --git a/src/parser.py b/src/parser.py index a10e8f72..1e113b52 100644 --- a/src/parser.py +++ b/src/parser.py @@ -25,14 +25,16 @@ class TokenKind(Enum): # keywords SELECT = auto() FROM = auto() + WHERE = auto() # literals STRING = auto() IDENTIFIER = auto() + STAR = auto() + EQUALS = auto() # structure COMMA = auto() - STAR = auto() ERROR = auto() EOF = auto() # this is a fake token only made and used in the parser @@ -40,6 +42,7 @@ class TokenKind(Enum): KEYWORDS = { "SELECT": TokenKind.SELECT, "FROM": TokenKind.FROM, + "WHERE": TokenKind.WHERE, } @@ -108,6 +111,9 @@ def tokenize(query: string) -> list[Token]: kind = TokenKind.STRING if string_result.endswith("'") and len(string_result) > 1 else TokenKind.ERROR result.append(Token(kind, string_result, idx, cursor.index + 1)) + elif char == "=": + result.append(Token(TokenKind.EQUALS, "=", idx, cursor.index)) + return result @@ -126,11 +132,11 @@ def eof(self) -> bool: """Check whether the token stream is done.""" return self.index == len(self.contents) - def peek(self) -> Token: - """Look at the next token in the stream.""" + def peek(self) -> TokenKind: + """Look at the next kind of token in the stream.""" if self.eof(): - return Token(TokenKind.EOF, "", -1, -1) - return self.contents[self.index] + return TokenKind.EOF + return self.contents[self.index].kind def advance(self) -> None: """Move to the next token in the stream.""" @@ -152,10 +158,16 @@ def open(self) -> int: self.events.append(("OPEN", ParentKind.ERROR_TREE)) return result - def close(self, kind: ParentKind, where: int) -> None: + def open_before(self, index: int) -> int: + """Start nesting children before a given point.""" + self.events.insert(index, ("OPEN", ParentKind.ERROR_TREE)) + return index + + def close(self, kind: ParentKind, where: int) -> int: """Stop nesting children and note the tree type.""" self.events[where] = ("OPEN", kind) self.events.append("CLOSE") + return where def expect(self, kind: TokenKind, error: str) -> None: """Ensure the next token is a specific kind and advance.""" @@ -166,7 +178,7 @@ def expect(self, kind: TokenKind, error: str) -> None: def at(self, kind: TokenKind) -> None: """Check if the next token is a specific kind.""" - return self.peek().kind == kind + return self.peek() == kind @dataclass @@ -185,7 +197,10 @@ class ParentKind(Enum): ERROR_TREE = auto() FIELD_LIST = auto() FROM_CLAUSE = auto() + WHERE_CLAUSE = auto() EXPR_NAME = auto() + EXPR_STRING = auto() + EXPR_BINARY = auto() FILE = auto() @@ -242,7 +257,7 @@ def _parse_stmt(parser: Parser) -> None: def _parse_select_stmt(parser: Parser) -> None: - # 'SELECT' [ ',' ]* [ 'FROM' IDENTIFIER ] + # 'SELECT' [ ',' ]* [ 'FROM' IDENTIFIER ] [ 'WHERE' ] start = parser.open() parser.expect(TokenKind.SELECT, "only SELECT is supported") @@ -261,6 +276,14 @@ def _parse_select_stmt(parser: Parser) -> None: parser.expect(TokenKind.IDENTIFIER, "expected to select from a table") parser.close(ParentKind.FROM_CLAUSE, from_start) + if parser.at(TokenKind.WHERE): + # where clause + where_start = parser.open() + parser.advance() + + _parse_expr(parser) + parser.close(ParentKind.WHERE_CLAUSE, where_start) + parser.close(ParentKind.SELECT_STMT, start) @@ -273,15 +296,63 @@ def _parse_field(parser: Parser) -> None: def _parse_expr(parser: Parser) -> None: - # - _parse_small_expr(parser) + # | = + _parse_expr_inner(parser, TokenKind.EOF) -def _parse_small_expr(parser: Parser) -> None: +def _parse_expr_inner(parser: Parser, left_op: TokenKind) -> None: + left = _parse_small_expr(parser) + + while True: + right_op = parser.peek() + if right_goes_first(left_op, right_op): + # if we have A B C ..., + # then we need to parse (A (B C ...)) + outer = parser.open_before(left) + parser.advance() + _parse_expr_inner(parser, right_op) # (B C ...) + parser.close(ParentKind.EXPR_BINARY, outer) + else: + # (A B) C will be handled + # (if this were toplevel, right_goes_first will happen) + break + + +def _parse_small_expr(parser: Parser) -> int: # IDENTIFIER start = parser.open() - parser.expect(TokenKind.IDENTIFIER, "expected an expression") - parser.close(ParentKind.EXPR_NAME, start) + if parser.at(TokenKind.IDENTIFIER): + parser.advance() + return parser.close(ParentKind.EXPR_NAME, start) + if parser.at(TokenKind.STRING): + parser.advance() + return parser.close(ParentKind.EXPR_STRING, start) + parser.advance_with_error("expected expression") + return parser.close(ParentKind.ERROR_TREE, start) + + +TABLE = [[TokenKind.EQUALS]] + + +def right_goes_first(left: TokenKind, right: TokenKind) -> bool: + """Understand which token type binds tighter. + + We say that A B C is equivalent to: + - A (B C) if we return True + - (A B) C if we return False + """ + left_idx = next((i for i, r in enumerate(TABLE) if left in r), None) + right_idx = next((i for i, r in enumerate(TABLE) if right in r), None) + + if right_idx is None: + # evaluate left-to-right + return False + if left_idx is None: + # well, maybe left doesn't exist? + assert left == TokenKind.EOF + return True + + return right_idx > left_idx ##### tests: (this should be moved to a proper tests folder) @@ -321,10 +392,10 @@ def stringify_tokens(query: str) -> str: def _stringify_tree(tree: Tree) -> list[str]: result = [] if isinstance(tree, Parent): - result.append(f"{tree.kind}") + result.append(f"{tree.kind.name}") result.extend(" " + line for child in tree.children for line in _stringify_tree(child)) else: - repr = f'{tree.kind} ("{tree.text}")' + repr = f'{tree.kind.name} ("{tree.text}")' if tree.errors: repr += " -- " repr += " / ".join(tree.errors) @@ -363,14 +434,33 @@ def test_parse_simple() -> None: assert ( stringify_tree(parse(tokenize("SELECT * FROM posts"))) == textwrap.dedent(""" - ParentKind.FILE - ParentKind.SELECT_STMT - TokenKind.SELECT ("SELECT") - ParentKind.FIELD_LIST - TokenKind.STAR ("*") - ParentKind.FROM_CLAUSE - TokenKind.FROM ("FROM") - TokenKind.IDENTIFIER ("posts") + FILE + SELECT_STMT + SELECT ("SELECT") + FIELD_LIST + STAR ("*") + FROM_CLAUSE + FROM ("FROM") + IDENTIFIER ("posts") + """).strip() + ) + + assert ( + stringify_tree(parse(tokenize("SELECT * WHERE actor = 'aaa'"))) + == textwrap.dedent(""" + FILE + SELECT_STMT + SELECT ("SELECT") + FIELD_LIST + STAR ("*") + WHERE_CLAUSE + WHERE ("WHERE") + EXPR_BINARY + EXPR_NAME + IDENTIFIER ("actor") + EQUALS ("=") + EXPR_STRING + STRING ("'aaa'") """).strip() ) From 37346f9bae5a2cbf3fb0694d90b9aaf850d11083 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 10 Aug 2025 13:30:37 +0900 Subject: [PATCH 017/117] Fix strings --- src/parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parser.py b/src/parser.py index 1e113b52..f838f041 100644 --- a/src/parser.py +++ b/src/parser.py @@ -65,7 +65,7 @@ def next(self) -> str: return c -def tokenize(query: string) -> list[Token]: +def tokenize(query: str) -> list[Token]: """Turn a query into a list of tokens.""" result = [] @@ -107,9 +107,9 @@ def tokenize(query: string) -> list[Token]: cursor.next() # get the last ' - string_result = cursor.contents[idx : cursor.index + 1] + string_result = cursor.contents[idx : cursor.index] kind = TokenKind.STRING if string_result.endswith("'") and len(string_result) > 1 else TokenKind.ERROR - result.append(Token(kind, string_result, idx, cursor.index + 1)) + result.append(Token(kind, string_result, idx, cursor.index)) elif char == "=": result.append(Token(TokenKind.EQUALS, "=", idx, cursor.index)) From 90a6911fa0f5f49ed4aca60895ac50da3709e522 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 10 Aug 2025 12:58:57 -0400 Subject: [PATCH 018/117] fix: lint errors --- src/boot.js | 16 ++++++++-------- src/styles.css | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/boot.js b/src/boot.js index 629fe45c..d45b9462 100644 --- a/src/boot.js +++ b/src/boot.js @@ -33,7 +33,7 @@ let continuePressed = false; window.startBootSequence = async function () { continuePressed = false; - + bootScreen = document.createElement("div"); bootScreen.className = "boot-screen"; bootScreen.innerHTML = '
'; @@ -138,7 +138,7 @@ function animateProgressBar(barId) { resolve(); return; } - + progress += Math.random() * 15; if (progress >= 100) { progress = 100; @@ -156,17 +156,17 @@ function waitForContinue() { e.preventDefault(); e.stopPropagation(); continuePressed = true; - + document.removeEventListener("keydown", handleInteraction, true); document.removeEventListener("click", handleInteraction, true); bootScreen.removeEventListener("click", handleInteraction, true); - + resolve(); }; document.addEventListener("keydown", handleInteraction, true); document.addEventListener("click", handleInteraction, true); - + if (bootScreen) { bootScreen.addEventListener("click", handleInteraction, true); } @@ -174,13 +174,13 @@ function waitForContinue() { const timeoutId = setTimeout(() => { if (!continuePressed) { continuePressed = true; - + document.removeEventListener("keydown", handleInteraction, true); document.removeEventListener("click", handleInteraction, true); if (bootScreen) { bootScreen.removeEventListener("click", handleInteraction, true); } - + resolve(); } }, 3000); @@ -256,4 +256,4 @@ const bootStyles = ` const styleSheet = document.createElement("style"); styleSheet.textContent = bootStyles; -document.head.appendChild(styleSheet); \ No newline at end of file +document.head.appendChild(styleSheet); diff --git a/src/styles.css b/src/styles.css index f09f56b1..6e86b67d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2,7 +2,7 @@ @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap"); /* Thanks to this article on the CRT effect: https://aleclownes.com/2017/02/01/crt-display.html */ -/* Read for reference on the values used */ +/* Read for reference on the values used */ @keyframes flicker { 0% { From 244205559ad616b37b243e6e389587dc18ce8709 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 10 Aug 2025 18:22:36 -0400 Subject: [PATCH 019/117] fix(boot): load core modules in background Ensured pyodide, setup.py, functions.py, and frontend.py load asynchronously during boot sequence. --- src/boot.js | 337 ++++++++++++++++++++++++++++++++++++------------- src/index.html | 147 +++++++++++++++++---- 2 files changed, 367 insertions(+), 117 deletions(-) diff --git a/src/boot.js b/src/boot.js index d45b9462..bd45011c 100644 --- a/src/boot.js +++ b/src/boot.js @@ -1,38 +1,85 @@ // boot.js -// call window.startBootSequence() to begin, window.finishBoot() when pyodide is ready +// exposes boot progress API for actual pyodide loading const bootMessages = [ { text: "BIOS Version 2.1.87 - Copyright (C) 1987 Iridescent Ivies", delay: 500, + stage: "bios" }, - { text: "Memory Test: 640K OK", delay: 800 }, - { text: "Extended Memory Test: 15360K OK", delay: 600 }, - { text: "", delay: 200 }, - { text: "Detecting Hardware...", delay: 400 }, - { text: " - Primary Hard Disk.......... OK", delay: 300 }, - { text: " - Network Interface.......... OK", delay: 300 }, - { text: " - Math Coprocessor........... OK", delay: 400 }, - { text: "", delay: 200 }, - { text: "Loading SQL Social Network v1.0...", delay: 600 }, - { text: "Initializing Python Runtime Environment...", delay: 800 }, - { text: "Loading Pyodide Kernel", delay: 1000, showProgress: true }, - { text: "Installing pandas...", delay: 2000 }, - { text: "Installing sqlalchemy...", delay: 1500 }, - { text: "Configuring data structures...", delay: 800 }, - { text: "Establishing database connections...", delay: 600 }, - { text: "Loading sample datasets...", delay: 400 }, - { text: "", delay: 200 }, - { text: "System Ready!", delay: 300, blink: true }, - { text: "Press any key to continue...", delay: 500, blink: true }, + { text: "Memory Test: 640K OK", delay: 400, stage: "memory" }, + { text: "Extended Memory Test: 15360K OK", delay: 300, stage: "memory" }, + { text: "", delay: 200, stage: "memory" }, + { text: "Detecting Hardware...", delay: 400, stage: "hardware" }, + { text: " - Primary Hard Disk.......... OK", delay: 300, stage: "hardware" }, + { text: " - Network Interface.......... OK", delay: 300, stage: "hardware" }, + { text: " - Math Coprocessor........... OK", delay: 200, stage: "hardware" }, + { text: "", delay: 200, stage: "hardware" }, + { text: "Loading SQL Social Network v1.0...", delay: 400, stage: "init" }, + { text: "Initializing Python Runtime Environment...", delay: 300, stage: "pyodide_start" }, + { text: "Loading Pyodide Kernel", delay: 0, showProgress: true, stage: "pyodide_load", waitForCallback: true }, + { text: "Installing setup scripts...", delay: 0, showProgress: true, stage: "setup_load", waitForCallback: true }, + { text: "Configuring Python modules...", delay: 0, showProgress: true, stage: "modules_load", waitForCallback: true }, + { text: "Loading parser and functions...", delay: 0, showProgress: true, stage: "functions_load", waitForCallback: true }, + { text: "Establishing database connections...", delay: 400, stage: "db_init" }, + { text: "Loading sample datasets...", delay: 300, stage: "data_init" }, + { text: "", delay: 200, stage: "complete" }, + { text: "System Ready!", delay: 300, blink: true, stage: "complete" }, + { text: "Press any key to continue...", delay: 500, blink: true, stage: "complete" }, ]; let bootScreen = null; let isBootComplete = false; let continuePressed = false; +let currentMessageIndex = 0; +let bootContent = null; +let progressCallbacks = {}; + + +window.bootProgress = { + start: startBootSequence, + pyodideLoaded: () => advanceToStage("setup_load"), + setupLoaded: () => advanceToStage("modules_load"), + modulesLoaded: () => advanceToStage("functions_load"), + functionsLoaded: () => advanceToStage("db_init"), + complete: finishBoot, + isComplete: () => isBootComplete, + + updateProgress: updateCurrentProgress, + setProgressMessage: setProgressMessage +}; -window.startBootSequence = async function () { +// update current progress bar to a specific percentage +function updateCurrentProgress(percentage) { + const currentProgressBars = document.querySelectorAll('[id^="progress-bar-"]'); + const lastBar = currentProgressBars[currentProgressBars.length - 1]; + if (lastBar) { + // clear any existing interval for this bar to prevent conflicts + const barId = lastBar.id; + if (progressCallbacks[barId]) { + clearInterval(progressCallbacks[barId]); + delete progressCallbacks[barId]; + } + + lastBar.style.width = Math.min(85, Math.max(0, percentage)) + "%"; + } +} + +// add a custom progress message (optional) +function setProgressMessage(message) { + const bootLines = bootContent.querySelectorAll('.boot-line'); + const lastLine = bootLines[bootLines.length - 1]; + if (lastLine && !lastLine.classList.contains('boot-blink')) { + const progressDiv = lastLine.querySelector('.boot-progress'); + if (progressDiv) { + lastLine.innerHTML = message + progressDiv.outerHTML; + } + } +} + +async function startBootSequence() { continuePressed = false; + currentMessageIndex = 0; bootScreen = document.createElement("div"); bootScreen.className = "boot-screen"; @@ -41,87 +88,154 @@ window.startBootSequence = async function () { document.body.appendChild(bootScreen); document.querySelector(".interface").style.opacity = "0"; - await showBootMessages(); - await waitForContinue(); + bootContent = document.getElementById("boot-content"); - isBootComplete = true; -}; + // start showing messages up to first callback point + await showMessagesUpToStage("pyodide_load"); + + console.log("Boot sequence waiting for pyodide load..."); +} -// hide boot screen and show main interface -window.finishBoot = function () { - if (bootScreen) { - bootScreen.style.opacity = "0"; - bootScreen.style.transition = "opacity 0.5s ease"; +async function showMessagesUpToStage(targetStage) { + while (currentMessageIndex < bootMessages.length) { + if (continuePressed) { + // fast-forward remaining messages + showRemainingMessages(); + break; + } - setTimeout(() => { - if (bootScreen && bootScreen.parentNode) { - document.body.removeChild(bootScreen); - } - bootScreen = null; - }, 500); + const message = bootMessages[currentMessageIndex]; + + // stop if we hit a callback stage that doesn't match the target + if (message.waitForCallback && message.stage !== targetStage) { + break; + } + + await showMessage(message, currentMessageIndex); + currentMessageIndex++; + + // if this was the target stage and it's a callback, stop here + if (message.stage === targetStage && message.waitForCallback) { + break; + } } +} - // show main interface - const mainInterface = document.querySelector(".interface"); - mainInterface.style.transition = "opacity 0.5s ease"; - mainInterface.style.opacity = "1"; +async function advanceToStage(targetStage) { + console.log(`Advancing boot to stage: ${targetStage}`); + + // complete current progress bar smoothly if there is one + await completeCurrentProgressBar(); + + // continue to next stage + currentMessageIndex++; + await showMessagesUpToStage(targetStage); +} - console.log("boot sequence complete - system ready"); -}; +function completeCurrentProgressBar() { + return new Promise((resolve) => { + const currentProgressBars = document.querySelectorAll('[id^="progress-bar-"]'); + if (currentProgressBars.length === 0) { + resolve(); + return; + } -window.isBootComplete = function () { - return isBootComplete; -}; + let completed = 0; + currentProgressBars.forEach(bar => { + const barId = bar.id; + + if (progressCallbacks[barId]) { + clearInterval(progressCallbacks[barId]); + delete progressCallbacks[barId]; + } + + const currentWidth = parseFloat(bar.style.width) || 0; -// show boot messages -async function showBootMessages() { - const bootContent = document.getElementById("boot-content"); + if (currentWidth >= 100) { + completed++; + if (completed === currentProgressBars.length) { + resolve(); + } + return; + } + + const interval = setInterval(() => { + const width = parseFloat(bar.style.width) || 0; + if (width >= 100) { + bar.style.width = "100%"; + clearInterval(interval); + completed++; + if (completed === currentProgressBars.length) { + resolve(); + } + } else { + bar.style.width = Math.min(100, width + 12) + "%"; + } + }, 30); + }); + }); +} - for (let i = 0; i < bootMessages.length; i++) { - if (continuePressed) { - const remainingMessages = bootMessages.slice(i); - remainingMessages.forEach(msg => { - const line = document.createElement("div"); - line.className = "boot-line boot-show"; - line.textContent = msg.text; - if (msg.blink) line.classList.add("boot-blink"); - bootContent.appendChild(line); - }); - break; - } +async function showMessage(message, index) { + const line = document.createElement("div"); + line.className = "boot-line"; - const message = bootMessages[i]; - const line = document.createElement("div"); - line.className = "boot-line"; - - if (message.showProgress) { - line.innerHTML = - message.text + - '
'; - } else { - line.textContent = message.text; - } + if (message.showProgress) { + line.innerHTML = message.text + + '
'; + } else { + line.textContent = message.text; + } - if (message.blink) { - line.classList.add("boot-blink"); - } + if (message.blink) { + line.classList.add("boot-blink"); + } - bootContent.appendChild(line); + bootContent.appendChild(line); - setTimeout(() => { - line.classList.add("boot-show"); - }, 50); + setTimeout(() => { + line.classList.add("boot-show"); + }, 50); - if (message.showProgress) { - await animateProgressBar("progress-bar-" + i); - } + if (message.showProgress && !message.waitForCallback) { + await animateProgressBar("progress-bar-" + index); + } else if (message.showProgress && message.waitForCallback) { + startProgressBar("progress-bar-" + index); + } - await new Promise((resolve) => setTimeout(resolve, message.delay)); + if (message.delay > 0) { + await new Promise(resolve => setTimeout(resolve, message.delay)); } } +function startProgressBar(barId) { + const progressBar = document.getElementById(barId); + if (!progressBar) return; + + // start at 0% and slowly increase until callback + let progress = 0; + progressBar.style.width = progress + "%"; + + const interval = setInterval(() => { + if (continuePressed) { + progress = 100; + progressBar.style.width = progress + "%"; + clearInterval(interval); + return; + } + + // slowly increase but never complete without callback + // slow down as it gets closer to 90% + const increment = progress < 50 ? Math.random() * 8 : Math.random() * 2; + progress += increment; + if (progress > 85) progress = 85; + progressBar.style.width = progress + "%"; + }, 300); + + progressCallbacks[barId] = interval; +} + function animateProgressBar(barId) { return new Promise((resolve) => { const progressBar = document.getElementById(barId); @@ -150,6 +264,51 @@ function animateProgressBar(barId) { }); } +function showRemainingMessages() { + const remainingMessages = bootMessages.slice(currentMessageIndex); + remainingMessages.forEach((msg, i) => { + const line = document.createElement("div"); + line.className = "boot-line boot-show"; + line.textContent = msg.text; + if (msg.blink) line.classList.add("boot-blink"); + bootContent.appendChild(line); + }); + currentMessageIndex = bootMessages.length; +} + +async function finishBoot() { + console.log("Finishing boot sequence..."); + + // complete any remaining progress bars + completeCurrentProgressBar(); + + // show remaining messages + currentMessageIndex++; + await showMessagesUpToStage("complete"); + await waitForContinue(); + + if (bootScreen) { + bootScreen.style.opacity = "0"; + bootScreen.style.transition = "opacity 0.5s ease"; + + setTimeout(() => { + if (bootScreen && bootScreen.parentNode) { + document.body.removeChild(bootScreen); + } + bootScreen = null; + }, 500); + } + + const mainInterface = document.querySelector(".interface"); + if (mainInterface) { + mainInterface.style.transition = "opacity 0.5s ease"; + mainInterface.style.opacity = "1"; + } + + isBootComplete = true; + console.log("Boot sequence complete - system ready"); +} + function waitForContinue() { return new Promise((resolve) => { const handleInteraction = (e) => { @@ -159,28 +318,28 @@ function waitForContinue() { document.removeEventListener("keydown", handleInteraction, true); document.removeEventListener("click", handleInteraction, true); - bootScreen.removeEventListener("click", handleInteraction, true); + if (bootScreen) { + bootScreen.removeEventListener("click", handleInteraction, true); + } resolve(); }; document.addEventListener("keydown", handleInteraction, true); document.addEventListener("click", handleInteraction, true); - if (bootScreen) { bootScreen.addEventListener("click", handleInteraction, true); } + // Auto-continue after 3 seconds const timeoutId = setTimeout(() => { if (!continuePressed) { continuePressed = true; - document.removeEventListener("keydown", handleInteraction, true); document.removeEventListener("click", handleInteraction, true); if (bootScreen) { bootScreen.removeEventListener("click", handleInteraction, true); } - resolve(); } }, 3000); @@ -245,7 +404,7 @@ const bootStyles = ` height: 100%; background: #00ff00; width: 0%; - transition: width 0.5s ease; + transition: width 0.2s ease; } @keyframes bootBlink { diff --git a/src/index.html b/src/index.html index 9f64ee31..8ec845c0 100644 --- a/src/index.html +++ b/src/index.html @@ -32,9 +32,7 @@ placeholder="SELECT username, message, likes FROM posts WHERE likes > 10" >
- +
@@ -49,10 +47,10 @@
- + - +
- + From 925ddcea30179043ddc7301d32bbbf758f76fefb Mon Sep 17 00:00:00 2001 From: giplgwm Date: Fri, 15 Aug 2025 23:11:47 -0400 Subject: [PATCH 070/117] pull image extraction into a function to avoid duplicate code --- src/functions.py | 51 ++++++++++++++++++------------------------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/src/functions.py b/src/functions.py index f805be1a..14ed4e37 100644 --- a/src/functions.py +++ b/src/functions.py @@ -82,6 +82,23 @@ async def parse_input(_: Event) -> None: await get_author_feed(tree) +def _extract_images_from_post(data: dict) -> str: + """Extract any embedded images from a post and return them as a delimited string.""" + post = data["post"] + if "embed" in post: + embed_type = post["embed"]["$type"] + images = None + if embed_type == "app.bsky.embed.images#view": + images = post["embed"]["images"] + image_links = [] + if images: + for image in images: + image_link = f"{image['thumb']},{image['fullsize']},{image['alt']}" + image_links.append(image_link) + return " | ".join(image_links) + return "" # make an empty field to avoid errors in posts without images + + async def get_user_timeline(tokens: Tree) -> dict: """Get the current users timeline.""" fields = extract_fields(tokens) @@ -96,22 +113,7 @@ async def get_user_timeline(tokens: Tree) -> dict: body = [] for i in val: data = i - - # Extract any embedded images from the post and put their link in data - post = data["post"] - if "embed" in post: - embed_type = post["embed"]["$type"] - images = None - if embed_type == "app.bsky.embed.images#view": - images = post["embed"]["images"] - image_links = [] - if images: - for image in images: - image_link = f"{image['thumb']},{image['fullsize']},{image['alt']}" - image_links.append(image_link) - post["images"] = " | ".join(image_links) - else: - post["images"] = "" # make an empty field to avoid errors in posts without images + data["post"]["images"] = _extract_images_from_post(data) d = flatten_response(data) if field_tokens: @@ -139,22 +141,7 @@ async def get_author_feed(tokens: Tree) -> dict: body = [] for i in val: data = i - - # Extract any embedded images from the post and put their link in data - post = data["post"] - if "embed" in post: - embed_type = post["embed"]["$type"] - images = None - if embed_type == "app.bsky.embed.images#view": - images = post["embed"]["images"] - image_links = [] - if images: - for image in images: - image_link = f"{image['thumb']},{image['fullsize']},{image['alt']}" - image_links.append(image_link) - post["images"] = " | ".join(image_links) - else: - post["images"] = "" # make an empty field to avoid errors in posts without images + data["post"]["images"] = _extract_images_from_post(data) d = flatten_response(data) if field_tokens: From 59c1465bfcbbda805262b8e20fd38bc56e02d93c Mon Sep 17 00:00:00 2001 From: giplgwm Date: Fri, 15 Aug 2025 23:31:52 -0400 Subject: [PATCH 071/117] move image link handling into its own function --- src/frontend.py | 65 +++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/frontend.py b/src/frontend.py index e615649c..1a175949 100644 --- a/src/frontend.py +++ b/src/frontend.py @@ -135,6 +135,40 @@ def _animate() -> None: EMBED_IMAGE_LEN = 3 +def _handle_image(image: str) -> Element: + """Take in an image string and create the hyperlink element. + + The string is expected to be either 1 link or a comma-separated list of 3. + """ + items = image.split(",") + thumbnail_link = items[0] + full_size_link = "" + alt_text = "" + # handle embedded images vs profile pics + if len(items) == EMBED_IMAGE_LEN: + full_size_link = items[1] + alt_text = items[2] + hyperlink = document.createElement("a") + hyperlink.href = "#" + hyperlink.textContent = "Image" + + def create_click_handler(img_url: str, fullsize_url: str, alt: str) -> callable: + """Capture the image value. + + without this there is a weird issue where all of the images are the same. + """ + + async def _handler( + _: Event, img_url: str = img_url, fullsize_url: str = fullsize_url, alt: str = alt + ) -> callable: + await show_image_modal(img_url, fullsize_url, alt) + + return _handler + + hyperlink.addEventListener("click", create_proxy(create_click_handler(thumbnail_link, full_size_link, alt_text))) + return hyperlink + + def _create_table_rows(headers: list, rows: list[dict]) -> None: """Create table rows with appearing effect.""" for row_index, row_data in enumerate(rows): @@ -148,35 +182,8 @@ def _create_table_rows(headers: list, rows: list[dict]) -> None: if cell_data.startswith("https://cdn.bsky.app/img/"): images = cell_data.split(" | ") for image in images: - items = image.split(",") - thumbnail_link = items[0] - full_size_link = "" - alt_text = "" - # handle embedded images vs profile pics - if len(items) == EMBED_IMAGE_LEN: - full_size_link = items[1] - alt_text = items[2] - hyperlink = document.createElement("a") - hyperlink.href = "#" - hyperlink.textContent = "Image" - - def create_click_handler(img_url: str, fullsize_url: str, alt: str) -> callable: - """Capture the image value. - - without this there is a weird issue where all of the images are the same. - """ - - async def _handler( - _: Event, img_url: str = img_url, fullsize_url: str = fullsize_url, alt: str = alt - ) -> callable: - await show_image_modal(img_url, fullsize_url, alt) - - return _handler - - hyperlink.addEventListener( - "click", create_proxy(create_click_handler(thumbnail_link, full_size_link, alt_text)) - ) - td.append(hyperlink) + image_element = _handle_image(image) + td.append(image_element) else: td.textContent = str(cell_data) if cell_data else "" tr.appendChild(td) From fba4ed204f86e9335f4ff64aea70638c52211910 Mon Sep 17 00:00:00 2001 From: giplgwm Date: Sat, 16 Aug 2025 00:26:30 -0400 Subject: [PATCH 072/117] Handle failed logins --- src/auth_modal.py | 29 ++++++++++++++++++++++++----- src/index.html | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/auth_modal.py b/src/auth_modal.py index c1597863..78f7a27a 100644 --- a/src/auth_modal.py +++ b/src/auth_modal.py @@ -15,6 +15,7 @@ USERNAME_INPUT = None PASSWORD_INPUT = None AUTH_FORM = None +STATUS_TEXT = None # authentication data auth_data = None @@ -23,7 +24,7 @@ def init_auth_modal() -> None: """Initialize the authentication modal.""" - global AUTH_MODAL, LOGIN_BTN, STEALTH_BTN, USERNAME_INPUT, PASSWORD_INPUT, AUTH_FORM # noqa: PLW0603 + global AUTH_MODAL, LOGIN_BTN, STEALTH_BTN, USERNAME_INPUT, PASSWORD_INPUT, AUTH_FORM, STATUS_TEXT # noqa: PLW0603 AUTH_MODAL = document.getElementById("auth-modal") LOGIN_BTN = document.getElementById("login-btn") @@ -31,6 +32,7 @@ def init_auth_modal() -> None: USERNAME_INPUT = document.getElementById("bluesky-username") PASSWORD_INPUT = document.getElementById("bluesky-password") AUTH_FORM = document.getElementById("auth-form") + STATUS_TEXT = document.getElementById("security-notice") setup_event_listeners() print("Auth modal initialized") @@ -130,10 +132,9 @@ async def complete_auth() -> None: auth_data = {"username": username, "password": password, "mode": "authenticated"} window.session = BskySession(username, password) is_logged_in = await window.session.login() - if is_logged_in: - print("logged in") - else: - print("No Log IN") # TODO: Handle the Failed Auth System + if not is_logged_in: + handle_failed_auth() + return LOGIN_BTN.innerHTML = "AUTHENTICATED ✓" LOGIN_BTN.style.background = "#004400" @@ -146,6 +147,24 @@ def finish_auth() -> None: set_timeout(create_proxy(complete_auth), 2000) +def handle_failed_auth() -> None: + """Handle a failed login.""" + LOGIN_BTN.disabled = False + LOGIN_BTN.innerHTML = "LOGIN" + USERNAME_INPUT.style.borderColor = "#ff0000" + PASSWORD_INPUT.style.borderColor = "#ff0000" + PASSWORD_INPUT.value = "" + STATUS_TEXT.innerText = "Incorrect Username or Password." + + def reset_status() -> None: + STATUS_TEXT.innerText = "Your credentials are processed locally and never stored permanently. \ + Stealth mode allows read-only access to public posts." + USERNAME_INPUT.style.borderColor = "#00ff00" + PASSWORD_INPUT.style.borderColor = "#00ff00" + + set_timeout(create_proxy(reset_status), 2000) + + def handle_stealth_mode() -> None: """Enable stealth/anonymous mode.""" STEALTH_BTN.disabled = True diff --git a/src/index.html b/src/index.html index 0bb158f3..cb71afdd 100644 --- a/src/index.html +++ b/src/index.html @@ -126,7 +126,7 @@ -
+
Your credentials are processed locally and never stored permanently. Stealth mode allows read-only access to public posts.
From b8c04312f3110360545e03b3b90d6198ff1219de Mon Sep 17 00:00:00 2001 From: daniel Date: Sat, 16 Aug 2025 01:27:24 -0500 Subject: [PATCH 073/117] fix: move the favicon to src folder --- favicon.png => src/favicon.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename favicon.png => src/favicon.png (100%) diff --git a/favicon.png b/src/favicon.png similarity index 100% rename from favicon.png rename to src/favicon.png From 92e9cfde1eb4fd28638769638317e3290b9a1a3b Mon Sep 17 00:00:00 2001 From: daniel Date: Sat, 16 Aug 2025 02:11:03 -0500 Subject: [PATCH 074/117] lint: fixing lint --- requirements.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3f1c73cb..3721e7c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,9 +21,7 @@ nodeenv==1.9.1 packaging==25.0 # via pytest pillow==11.3.0 - # via - # sql-bsky (pyproject.toml) - # ascii-magic + # via ascii-magic platformdirs==4.3.8 # via virtualenv pluggy==1.6.0 From efe2b2b0620a5e32156f35c3f8ff239eb9ea182b Mon Sep 17 00:00:00 2001 From: daniel Date: Sat, 16 Aug 2025 02:28:53 -0500 Subject: [PATCH 075/117] feat: added random endpoints to sql-2-api --- src/auth_session.py | 2 ++ src/functions.py | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/auth_session.py b/src/auth_session.py index 3c6537d0..53a5934e 100644 --- a/src/auth_session.py +++ b/src/auth_session.py @@ -135,6 +135,8 @@ async def get_preferences(self) -> dict: async def get_profile(self, actor: str) -> dict: """Get a user profile.""" + if actor == None: + actor = self.handle endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={actor}" response = await self.client.get( endpoint, diff --git a/src/functions.py b/src/functions.py index 6900fb7e..2be576b9 100644 --- a/src/functions.py +++ b/src/functions.py @@ -116,7 +116,7 @@ async def sql_to_api_handler(tokens: Tree) -> dict: field_tokens = [i.children[0] for i in fields if i.kind != TokenKind.STAR] for i in where_expr: - if i[0] in ["actor", "author"]: + if i[0] in ["actor", "author", "feed"]: api = i break else: @@ -129,9 +129,42 @@ async def sql_to_api_handler(tokens: Tree) -> dict: elif api[0] == "author": feed = await window.session.get_author_feed(api[2]) val = feed["feed"] - else: + else api[0] == "feed": feed = await window.session.get_timeline() val = feed["feed"] + elif table == "timeline": + feed = await window.session.get_timeline() + val = feed["feed"] + elif table == "profile": + if api[0] == "actors": + feed = await window.session.get_profile(api[2]) + val = feed + feed = await window.session.get_profile(None) + val = feed + elif table == "suggestions": + if api[0] == "actors": + feed = await window.session.get_suggestions(api[2]) + val = feed["actors"] + else: + feed = await window.session.get_suggested_feeds() + val = feed["feeds"] + elif table == "likes": + if api[0] == "actor": + feed = await window.session.get_actor_likes(api[2]) + val = feed["feeds"] + elif table=="followers": + if api[0] == "actor": + feed = await window.session.get_followers(api[2]) + val = feed["followers"] + elif table=="following": + if api[0] == "actor": + feed = await window.session.get_followers(api[2]) + val = feed["followers"] + elif table=="mutuals": + if api[0] == "actor": + feed = await window.session.get_followers(api[2]) + val = feed["followers"] + else: frontend.clear_interface("") frontend.update_status(f"Error getting from {table}", "error") From 834b23dd691362d4ff12c32d9331317edf01c91a Mon Sep 17 00:00:00 2001 From: daniel Date: Sat, 16 Aug 2025 02:39:32 -0500 Subject: [PATCH 076/117] lint: cleaning some funcs up --- src/auth_session.py | 2 +- src/functions.py | 66 ++++++++++++++++++++++++++------------------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/auth_session.py b/src/auth_session.py index 53a5934e..e40e8bb9 100644 --- a/src/auth_session.py +++ b/src/auth_session.py @@ -135,7 +135,7 @@ async def get_preferences(self) -> dict: async def get_profile(self, actor: str) -> dict: """Get a user profile.""" - if actor == None: + if actor is None: actor = self.handle endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={actor}" response = await self.client.get( diff --git a/src/functions.py b/src/functions.py index 2be576b9..084f5ef8 100644 --- a/src/functions.py +++ b/src/functions.py @@ -108,20 +108,9 @@ async def parse_input(_: Event) -> None: await sql_to_api_handler(tree) -async def sql_to_api_handler(tokens: Tree) -> dict: - """Handle going from SQL to the API.""" - where_expr = extract_where(tokens) - table = extract_table(tokens) - fields = extract_fields(tokens) - field_tokens = [i.children[0] for i in fields if i.kind != TokenKind.STAR] - - for i in where_expr: - if i[0] in ["actor", "author", "feed"]: - api = i - break - else: - # No Where Expression Matches - api = ["", ""] +async def processor(api: tuple[str, str], table: str) -> dict: # noqa: PLR0912 C901 + """Process the sql statements into a api call.""" + val = {} if table == "feed": if api[0] == "actor": feed = await window.session.get_actor_feeds(api[2]) @@ -129,43 +118,66 @@ async def sql_to_api_handler(tokens: Tree) -> dict: elif api[0] == "author": feed = await window.session.get_author_feed(api[2]) val = feed["feed"] - else api[0] == "feed": - feed = await window.session.get_timeline() - val = feed["feed"] elif table == "timeline": feed = await window.session.get_timeline() val = feed["feed"] elif table == "profile": - if api[0] == "actors": + if api[0] == "actors": feed = await window.session.get_profile(api[2]) val = feed - feed = await window.session.get_profile(None) - val = feed + else: + feed = await window.session.get_profile(None) + val = feed elif table == "suggestions": - if api[0] == "actors": + if api[0] == "actors": feed = await window.session.get_suggestions(api[2]) val = feed["actors"] else: feed = await window.session.get_suggested_feeds() val = feed["feeds"] elif table == "likes": - if api[0] == "actor": + if api[0] == "actor": feed = await window.session.get_actor_likes(api[2]) val = feed["feeds"] - elif table=="followers": + else: + pass + elif table == "followers": if api[0] == "actor": feed = await window.session.get_followers(api[2]) val = feed["followers"] - elif table=="following": + else: + pass + elif table == "following": if api[0] == "actor": - feed = await window.session.get_followers(api[2]) + feed = await window.session.get_following(api[2]) val = feed["followers"] - elif table=="mutuals": + else: + pass + elif table == "mutuals": if api[0] == "actor": - feed = await window.session.get_followers(api[2]) + feed = await window.session.get_mutual_followers(api[2]) val = feed["followers"] + else: + pass + return val + +async def sql_to_api_handler(tokens: Tree) -> dict: + """Handle going from SQL to the API.""" + where_expr = extract_where(tokens) + table = extract_table(tokens) + fields = extract_fields(tokens) + field_tokens = [i.children[0] for i in fields if i.kind != TokenKind.STAR] + + for i in where_expr: + if i[0] in ["actor", "author", "feed"]: + api = i + break else: + # No Where Expression Matches + api = ["", ""] + val = processor(api, table) + if not val: frontend.clear_interface("") frontend.update_status(f"Error getting from {table}", "error") frontend.trigger_electric_wave() From 226419d09b691b3b7d0fe2ee4ac97f0961a40eeb Mon Sep 17 00:00:00 2001 From: daniel Date: Sun, 17 Aug 2025 12:46:09 -0500 Subject: [PATCH 077/117] lint: cleaning the branch --- src/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions.py b/src/functions.py index 1db55191..2492a562 100644 --- a/src/functions.py +++ b/src/functions.py @@ -161,6 +161,7 @@ async def processor(api: tuple[str, str], table: str) -> dict: # noqa: PLR0912 pass return val + def _extract_images_from_post(data: dict) -> str: """Extract any embedded images from a post and return them as a delimited string.""" post = data["post"] @@ -178,7 +179,6 @@ def _extract_images_from_post(data: dict) -> str: return "" # make an empty field to avoid errors in posts without images - async def sql_to_api_handler(tokens: Tree) -> dict: """Handle going from SQL to the API.""" where_expr = extract_where(tokens) From 3eb84066fcf6188192da21eac9f7c46882bc88ec Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 15:17:20 -0400 Subject: [PATCH 078/117] feat: add drop easter egg Added DROP TABLE user; Easter Egg, fixed un-awaited function on line 245 and added proper proper handler for when the query is empty --- src/functions.py | 64 +++++++++++++++++++++++++++++-- src/styles.css | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 3 deletions(-) diff --git a/src/functions.py b/src/functions.py index 2492a562..165b442c 100644 --- a/src/functions.py +++ b/src/functions.py @@ -2,6 +2,7 @@ from js import Event, document, window from pyodide.ffi import create_proxy +from pyodide.ffi.wrappers import set_timeout import frontend from frontend import CLEAR_BUTTON, EXECUTE_BUTTON, clear_interface, update_table @@ -28,6 +29,53 @@ def _flatten(current: dict, name: str = "") -> dict: return flattened_result +def blue_screen_of_death() -> None: + """Easter Egg: Show WinXP Blue Screen of Death.""" + input_field = document.getElementById("query-input") + if input_field: + input_field.value = "" + + bsod = document.createElement("div") + bsod.className = "bsod-overlay" + bsod.innerHTML = ( + '
' + '
A problem has been detected and Windows has been shut down to prevent damage ' + " to your computer.
" + '
IRQL_NOT_LESS_OR_EQUAL
' + '
' + " If this is the first time you've seen this stop error screen, " + " restart your computer. If this screen appears again, follow these steps:

" + " Check to make sure any new hardware or software is properly installed. " + " If this is a new installation, ask your hardware or software manufacturer " + " for any Windows updates you might need.

" + " If problems continue, disable or remove any newly installed hardware or software. " + " Disable BIOS memory options such as caching or shadowing. If you need to use " + " Safe Mode to remove or disable components, restart your computer, press F8 " + " to select Advanced Startup Options, and then select Safe Mode." + "
" + '
' + " Technical information:

" + " *** STOP: 0x0000000A (0xFE520004, 0x00000001, 0x00000001, 0x804F9319)

" + " *** Address 804F9319 base at 804D7000, DateStamp 3844d96e - ntoskrnl.exe

" + " Beginning dump of physical memory
" + " Physical memory dump complete.
" + " Contact your system administrator or technical support group for further assistance." + "
" + "
" + ) + + document.body.appendChild(bsod) + frontend.flash_screen("#0000ff", 100) + + def remove_bsod() -> None: + if bsod.parentNode: + document.body.removeChild(bsod) + frontend.update_status("System recovered from critical error", "warning") + frontend.trigger_electric_wave() + + set_timeout(create_proxy(remove_bsod), 4000) + + def clean_value(text: str) -> str: """Remove surrounding single/double quotes if present.""" if isinstance(text, str) and (text[0] == text[-1]) and text[0] in ("'", '"'): @@ -103,8 +151,18 @@ def extract_table(tree: Tree) -> str: async def parse_input(_: Event) -> None: """Start of the parser.""" - y = document.getElementById("query-input").value - tree: Tree = parse(tokenize(y)) + query = document.getElementById("query-input").value.strip() + + if not query: + frontend.update_status("Enter a SQL query to execute", "warning") + return + + clean_query = query.upper().replace(";", "").replace(",", "").strip() + if "DROP TABLE USERS" in clean_query: + blue_screen_of_death() + return + + tree: Tree = parse(tokenize(query)) await sql_to_api_handler(tree) @@ -193,7 +251,7 @@ async def sql_to_api_handler(tokens: Tree) -> dict: else: # No Where Expression Matches api = ["", ""] - val = processor(api, table) + val = await processor(api, table) if not val: frontend.clear_interface("") frontend.update_status(f"Error getting from {table}", "error") diff --git a/src/styles.css b/src/styles.css index 3c5042e7..9d5ebe96 100644 --- a/src/styles.css +++ b/src/styles.css @@ -921,3 +921,101 @@ body { font-size: 12px; } } + +/* BSOD Styles */ +.bsod-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #0000aa; + color: white; + font-family: Consolas, "Lucida Console", "Courier New", monospace; + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + animation: bsodFlicker 0.1s ease-in; +} + +.bsod-content { + max-width: 800px; + padding: 40px; + line-height: 1.4; + font-size: 13px; + font-weight: normal; +} + +.bsod-header { + font-size: 14px; + margin-bottom: 20px; + font-weight: normal; +} + +.bsod-error { + font-size: 16px; + font-weight: bold; + margin: 20px 0; + color: #ffffff; + text-decoration: underline; +} + +.bsod-text { + margin: 20px 0; + line-height: 1.5; + font-size: 13px; +} + +.bsod-technical { + margin-top: 30px; + font-family: Consolas, "Lucida Console", "Courier New", monospace; + font-size: 11px; + color: #cccccc; + line-height: 1.3; +} + +@keyframes bsodFlicker { + 0% { opacity: 0; } + 10% { opacity: 1; } + 15% { opacity: 0; } + 20% { opacity: 1; } + 25% { opacity: 0; } + 30% { opacity: 1; } + 100% { opacity: 1; } +} + +/* Responsive BSOD */ +@media (max-width: 768px) { + .bsod-content { + padding: 20px; + font-size: 11px; + } + + .bsod-header { + font-size: 12px; + } + + .bsod-error { + font-size: 14px; + } + + .bsod-technical { + font-size: 9px; + } +} + +@media (max-width: 480px) { + .bsod-content { + padding: 15px; + font-size: 10px; + } + + .bsod-header { + font-size: 11px; + } + + .bsod-error { + font-size: 12px; + } +} From 58f461b43c882189b237207094b49b8b6076d592 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 15:39:43 -0400 Subject: [PATCH 079/117] add: ctr on/off button Quick add of the CRT enable and disable button in the Auth modal --- src/auth_modal.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++- src/index.html | 6 ++++ src/styles.css | 56 ++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/src/auth_modal.py b/src/auth_modal.py index 78f7a27a..f6e3e097 100644 --- a/src/auth_modal.py +++ b/src/auth_modal.py @@ -12,6 +12,7 @@ AUTH_MODAL = None LOGIN_BTN = None STEALTH_BTN = None +CRT_TOGGLE_BTN = None USERNAME_INPUT = None PASSWORD_INPUT = None AUTH_FORM = None @@ -20,20 +21,27 @@ # authentication data auth_data = None is_modal_visible = False +crt_enabled = True # CRT starts enabled by default def init_auth_modal() -> None: """Initialize the authentication modal.""" - global AUTH_MODAL, LOGIN_BTN, STEALTH_BTN, USERNAME_INPUT, PASSWORD_INPUT, AUTH_FORM, STATUS_TEXT # noqa: PLW0603 + global AUTH_MODAL, LOGIN_BTN, STEALTH_BTN, USERNAME_INPUT, PASSWORD_INPUT, AUTH_FORM, STATUS_TEXT, CRT_TOGGLE_BTN # noqa: PLW0603 AUTH_MODAL = document.getElementById("auth-modal") LOGIN_BTN = document.getElementById("login-btn") STEALTH_BTN = document.getElementById("stealth-btn") + CRT_TOGGLE_BTN = document.getElementById("crt-toggle-btn") USERNAME_INPUT = document.getElementById("bluesky-username") PASSWORD_INPUT = document.getElementById("bluesky-password") AUTH_FORM = document.getElementById("auth-form") STATUS_TEXT = document.getElementById("security-notice") + # Debug logging + print(f"CRT Toggle Button found: {CRT_TOGGLE_BTN is not None}") + if CRT_TOGGLE_BTN: + print(f"CRT Button ID: {CRT_TOGGLE_BTN.id}") + setup_event_listeners() print("Auth modal initialized") @@ -44,6 +52,14 @@ def setup_event_listeners() -> None: PASSWORD_INPUT.addEventListener("input", create_proxy(on_input_change)) AUTH_FORM.addEventListener("submit", create_proxy(on_form_submit)) STEALTH_BTN.addEventListener("click", create_proxy(on_stealth_click)) + + # Configure CRT toggle button event listener + if CRT_TOGGLE_BTN: + CRT_TOGGLE_BTN.addEventListener("click", create_proxy(on_crt_toggle_click)) + print("CRT toggle event listener attached") + else: + print("ERROR: CRT toggle button not found!") + document.addEventListener("keydown", create_proxy(on_keydown)) @@ -69,6 +85,12 @@ def on_stealth_click(_event: Event) -> None: handle_stealth_mode() +def on_crt_toggle_click(_event: Event) -> None: + """Handle CRT effect toggle.""" + print("CRT toggle button clicked") + toggle_crt_effect() + + def on_keydown(event: Event) -> None: """Keyboard shortcuts - not visually indicated *yet?.""" if not is_modal_visible: @@ -80,6 +102,45 @@ def on_keydown(event: Event) -> None: handle_authentication() +def toggle_crt_effect() -> None: + """Toggle the CRT effect on/off.""" + global crt_enabled # noqa: PLW0603 + + print(f"Toggle CRT called. Current state: {crt_enabled}") + + if not CRT_TOGGLE_BTN: + print("ERROR: CRT toggle button is None!") + return + + body = document.body + + if crt_enabled: + # Disable CRT effect + body.classList.remove("crt") + CRT_TOGGLE_BTN.innerHTML = "CRT EFFECT: OFF" + CRT_TOGGLE_BTN.style.background = "#333300" + CRT_TOGGLE_BTN.style.borderColor = "#ffff00" + CRT_TOGGLE_BTN.style.color = "#ffff00" + crt_enabled = False + print("CRT effect disabled") + else: + # Enable CRT effect + body.classList.add("crt") + CRT_TOGGLE_BTN.innerHTML = "CRT EFFECT: ON" + CRT_TOGGLE_BTN.style.background = "#003300" + CRT_TOGGLE_BTN.style.borderColor = "#00ff00" + CRT_TOGGLE_BTN.style.color = "#00ff00" + crt_enabled = True + print("CRT effect enabled") + + CRT_TOGGLE_BTN.style.transform = "scale(0.95)" + + def reset_scale() -> None: + CRT_TOGGLE_BTN.style.transform = "scale(1.0)" + + set_timeout(create_proxy(reset_scale), 150) + + def show_modal() -> None: """Show the modal.""" global is_modal_visible # noqa: PLW0603 @@ -268,6 +329,16 @@ def is_stealth_mode() -> bool: return auth_data is not None and auth_data.get("mode") == "stealth" +def is_crt_enabled() -> bool: + """Check if CRT effect is currently enabled.""" + return crt_enabled + + +def get_crt_status() -> str: + """Get current CRT status as string.""" + return "ON" if crt_enabled else "OFF" + + def show_auth_modal_after_boot() -> None: """Show modal after boot sequence.""" print("Initializing authentication modal...") diff --git a/src/index.html b/src/index.html index ccbdb5db..14819f2c 100644 --- a/src/index.html +++ b/src/index.html @@ -144,6 +144,12 @@
+
+ +
+
Your credentials are processed locally and never stored permanently. Stealth mode allows read-only access to public posts. diff --git a/src/styles.css b/src/styles.css index 9d5ebe96..a2f3d5b4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -856,6 +856,52 @@ body { background: #222200; } +/* CRT Toggle Button Styles */ +.crt-toggle-container { + margin: 12px 0 8px 0; + display: flex; + justify-content: center; +} + +.btn-crt { + background: #003300; + border: 2px outset #00ff00; + color: #00ff00; + padding: 8px 20px; + font-family: inherit; + font-size: 14px; + cursor: pointer; + font-weight: bold; + transition: all 0.2s ease; + text-transform: uppercase; + letter-spacing: 1px; + min-width: 150px; + transform: scale(1.0); +} + +.btn-crt:hover { + background: #004400; + box-shadow: 0 0 8px rgba(0, 255, 0, 0.3); +} + +.btn-crt:active { + border: 2px inset #00ff00; + background: #002200; + transform: scale(0.98); +} + +/* CRT effect disabled state styles */ +.btn-crt.crt-disabled { + background: #333300; + border-color: #ffff00; + color: #ffff00; +} + +.btn-crt.crt-disabled:hover { + background: #444400; + box-shadow: 0 0 8px rgba(255, 255, 0, 0.3); +} + .security-notice { background: #330000; border: 1px solid #ff6600; @@ -902,6 +948,11 @@ body { width: 100%; } + .btn-crt { + width: 100%; + margin: 8px 0; + } + .modal-header { font-size: 14px; padding: 6px 12px; @@ -920,6 +971,11 @@ body { .form-input { font-size: 12px; } + + .btn-crt { + font-size: 14px; + padding: 8px 20px; + } } /* BSOD Styles */ From f854e0abd9cc6302e76a79cfbbf0e747ef1d459b Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 17:54:53 -0400 Subject: [PATCH 080/117] edit: bug fixes and debug Fix: Handle stealth mode and missing post structures - Fix KeyError when extracting images from non-post data (suggestions, followers) - Fix AttributeError when getting own profile in stealth mode - Add friendly user messages instead of Python exceptions - Improve error handling for anonymous API access --- src/auth_modal.py | 5 ---- src/auth_session.py | 8 ++++++- src/functions.py | 56 +++++++++++++++++++++++++++++++++------------ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/auth_modal.py b/src/auth_modal.py index f6e3e097..3baa1d1f 100644 --- a/src/auth_modal.py +++ b/src/auth_modal.py @@ -37,11 +37,6 @@ def init_auth_modal() -> None: AUTH_FORM = document.getElementById("auth-form") STATUS_TEXT = document.getElementById("security-notice") - # Debug logging - print(f"CRT Toggle Button found: {CRT_TOGGLE_BTN is not None}") - if CRT_TOGGLE_BTN: - print(f"CRT Button ID: {CRT_TOGGLE_BTN.id}") - setup_event_listeners() print("Auth modal initialized") diff --git a/src/auth_session.py b/src/auth_session.py index 2230dac4..d123f03b 100644 --- a/src/auth_session.py +++ b/src/auth_session.py @@ -135,8 +135,14 @@ async def get_preferences(self) -> dict: async def get_profile(self, actor: str) -> dict: """Get a user profile.""" + # If no actor specified and we're authenticated, use our handle if actor is None: - actor = self.handle + if hasattr(self, "handle") and self.handle: + actor = self.handle + else: + # Return special error object for stealth mode + return {"stealth_error": True} + endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={actor}" response = await self.client.get( endpoint, diff --git a/src/functions.py b/src/functions.py index 165b442c..8bff44e4 100644 --- a/src/functions.py +++ b/src/functions.py @@ -166,7 +166,7 @@ async def parse_input(_: Event) -> None: await sql_to_api_handler(tree) -async def processor(api: tuple[str, str], table: str) -> dict: # noqa: PLR0912 C901 +async def processor(api: tuple[str, str], table: str) -> dict: # noqa: PLR0912 C901 PLR0915 """Process the sql statements into a api call.""" val = {} if table == "feed": @@ -185,6 +185,8 @@ async def processor(api: tuple[str, str], table: str) -> dict: # noqa: PLR0912 val = feed else: feed = await window.session.get_profile(None) + if isinstance(feed, dict) and feed.get("stealth_error"): + return "stealth_error" val = feed elif table == "suggestions": if api[0] == "actors": @@ -222,19 +224,31 @@ async def processor(api: tuple[str, str], table: str) -> dict: # noqa: PLR0912 def _extract_images_from_post(data: dict) -> str: """Extract any embedded images from a post and return them as a delimited string.""" + if "post" not in data: + return "" + post = data["post"] - if "embed" in post: - embed_type = post["embed"]["$type"] - images = None - if embed_type == "app.bsky.embed.images#view": - images = post["embed"]["images"] - image_links = [] - if images: - for image in images: - image_link = f"{image['thumb']},{image['fullsize']},{image['alt']}" - image_links.append(image_link) - return " | ".join(image_links) - return "" # make an empty field to avoid errors in posts without images + + # Check if the post has embedded content + if "embed" not in post: + return "" + + embed_type = post["embed"].get("$type", "") + + # Only process image embeds + if embed_type != "app.bsky.embed.images#view": + return "" + + images = post["embed"].get("images", []) + if not images: + return "" + + image_links = [] + for image in images: + image_link = f"{image['thumb']},{image['fullsize']},{image['alt']}" + image_links.append(image_link) + + return " | ".join(image_links) async def sql_to_api_handler(tokens: Tree) -> dict: @@ -257,6 +271,16 @@ async def sql_to_api_handler(tokens: Tree) -> dict: frontend.update_status(f"Error getting from {table}", "error") frontend.trigger_electric_wave() return {} + + # Handle stealth mode error for profile queries + if val == "stealth_error": + frontend.clear_interface("") + frontend.update_status( + "Cannot get own profile in stealth mode. Try: SELECT * FROM profile WHERE actors = 'username.bsky.social'", + "warning", + ) + frontend.trigger_electric_wave() + return {} tb = document.getElementById("table-body") tb.innerHTML = "" head = [] @@ -265,7 +289,11 @@ async def sql_to_api_handler(tokens: Tree) -> dict: body = [] for i in val: data = i - data["post"]["images"] = _extract_images_from_post(data) + + # Only try to extract images if the data structure supports it + images = _extract_images_from_post(data) + if images and "post" in data: + data["post"]["images"] = images d = flatten_response(data) if field_tokens: From a814a19b7d7b8c25b520717e657a78abdaac08c0 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 18:18:51 -0400 Subject: [PATCH 081/117] edit: better post handling --- src/functions.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/functions.py b/src/functions.py index 8bff44e4..c59424a1 100644 --- a/src/functions.py +++ b/src/functions.py @@ -224,30 +224,33 @@ async def processor(api: tuple[str, str], table: str) -> dict: # noqa: PLR0912 def _extract_images_from_post(data: dict) -> str: """Extract any embedded images from a post and return them as a delimited string.""" - if "post" not in data: + if not isinstance(data, dict): return "" + if "post" not in data: + return "" + post = data["post"] - + # Check if the post has embedded content if "embed" not in post: return "" - + embed_type = post["embed"].get("$type", "") - + # Only process image embeds if embed_type != "app.bsky.embed.images#view": return "" - + images = post["embed"].get("images", []) if not images: return "" - + image_links = [] for image in images: image_link = f"{image['thumb']},{image['fullsize']},{image['alt']}" image_links.append(image_link) - + return " | ".join(image_links) @@ -271,14 +274,11 @@ async def sql_to_api_handler(tokens: Tree) -> dict: frontend.update_status(f"Error getting from {table}", "error") frontend.trigger_electric_wave() return {} - + # Handle stealth mode error for profile queries if val == "stealth_error": frontend.clear_interface("") - frontend.update_status( - "Cannot get own profile in stealth mode. Try: SELECT * FROM profile WHERE actors = 'username.bsky.social'", - "warning", - ) + frontend.update_status("Cannot get own profile in stealth mode. Try: SELECT * FROM profile WHERE actors = 'username.bsky.social'", "warning") frontend.trigger_electric_wave() return {} tb = document.getElementById("table-body") @@ -289,7 +289,7 @@ async def sql_to_api_handler(tokens: Tree) -> dict: body = [] for i in val: data = i - + # Only try to extract images if the data structure supports it images = _extract_images_from_post(data) if images and "post" in data: @@ -308,4 +308,4 @@ async def sql_to_api_handler(tokens: Tree) -> dict: EXECUTE_BUTTON.addEventListener("click", create_proxy(parse_input)) -CLEAR_BUTTON.addEventListener("click", create_proxy(clear_interface)) +CLEAR_BUTTON.addEventListener("click", create_proxy(clear_interface)) \ No newline at end of file From c3c7bb8a31f3cac5313b21fd6dde1317f5941aea Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 18:38:03 -0400 Subject: [PATCH 082/117] fix: profile fix --- src/functions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/functions.py b/src/functions.py index c59424a1..2c427888 100644 --- a/src/functions.py +++ b/src/functions.py @@ -268,6 +268,7 @@ async def sql_to_api_handler(tokens: Tree) -> dict: else: # No Where Expression Matches api = ["", ""] + val = await processor(api, table) if not val: frontend.clear_interface("") @@ -281,12 +282,17 @@ async def sql_to_api_handler(tokens: Tree) -> dict: frontend.update_status("Cannot get own profile in stealth mode. Try: SELECT * FROM profile WHERE actors = 'username.bsky.social'", "warning") frontend.trigger_electric_wave() return {} + + if isinstance(val, dict): + val = [val] + tb = document.getElementById("table-body") tb.innerHTML = "" head = [] if field_tokens: head = [j.text for j in field_tokens] body = [] + for i in val: data = i @@ -296,8 +302,9 @@ async def sql_to_api_handler(tokens: Tree) -> dict: data["post"]["images"] = images d = flatten_response(data) + if field_tokens: - body.append({j: d[j.lower()] for j in head}) + body.append({j: d.get(j.lower(), "") for j in head}) else: body.append(d) [head.append(k) for k in d if k not in head] From d0b871cf0eafeee47d6beb797e5c5988e082084c Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 18:57:58 -0400 Subject: [PATCH 083/117] fix: where clause fix --- src/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions.py b/src/functions.py index 2c427888..58298b66 100644 --- a/src/functions.py +++ b/src/functions.py @@ -262,7 +262,7 @@ async def sql_to_api_handler(tokens: Tree) -> dict: field_tokens = [i.children[0] for i in fields if i.kind != TokenKind.STAR] for i in where_expr: - if i[0] in ["actor", "author", "feed"]: + if i[0] in ["actor", "author", "feed", "actors"]: api = i break else: From 9cfbb2144b44a8667de12b0f662d96d116187568 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 18:58:53 -0400 Subject: [PATCH 084/117] fix: lint errors --- src/functions.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/functions.py b/src/functions.py index 58298b66..d4203ca4 100644 --- a/src/functions.py +++ b/src/functions.py @@ -229,28 +229,28 @@ def _extract_images_from_post(data: dict) -> str: if "post" not in data: return "" - + post = data["post"] - + # Check if the post has embedded content if "embed" not in post: return "" - + embed_type = post["embed"].get("$type", "") - + # Only process image embeds if embed_type != "app.bsky.embed.images#view": return "" - + images = post["embed"].get("images", []) if not images: return "" - + image_links = [] for image in images: image_link = f"{image['thumb']},{image['fullsize']},{image['alt']}" image_links.append(image_link) - + return " | ".join(image_links) @@ -268,18 +268,21 @@ async def sql_to_api_handler(tokens: Tree) -> dict: else: # No Where Expression Matches api = ["", ""] - + val = await processor(api, table) if not val: frontend.clear_interface("") frontend.update_status(f"Error getting from {table}", "error") frontend.trigger_electric_wave() return {} - + # Handle stealth mode error for profile queries if val == "stealth_error": frontend.clear_interface("") - frontend.update_status("Cannot get own profile in stealth mode. Try: SELECT * FROM profile WHERE actors = 'username.bsky.social'", "warning") + frontend.update_status( + "Cannot get own profile in stealth mode. Try: SELECT * FROM profile WHERE actors = 'username.bsky.social'", + "warning", + ) frontend.trigger_electric_wave() return {} @@ -292,17 +295,17 @@ async def sql_to_api_handler(tokens: Tree) -> dict: if field_tokens: head = [j.text for j in field_tokens] body = [] - + for i in val: data = i - + # Only try to extract images if the data structure supports it images = _extract_images_from_post(data) if images and "post" in data: data["post"]["images"] = images d = flatten_response(data) - + if field_tokens: body.append({j: d.get(j.lower(), "") for j in head}) else: @@ -315,4 +318,4 @@ async def sql_to_api_handler(tokens: Tree) -> dict: EXECUTE_BUTTON.addEventListener("click", create_proxy(parse_input)) -CLEAR_BUTTON.addEventListener("click", create_proxy(clear_interface)) \ No newline at end of file +CLEAR_BUTTON.addEventListener("click", create_proxy(clear_interface)) From 55b3ee8b4781d8ed90157bc31169d4aa34c872c3 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 19:14:06 -0400 Subject: [PATCH 085/117] edit: typo of mine --- src/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions.py b/src/functions.py index d4203ca4..6b0c626d 100644 --- a/src/functions.py +++ b/src/functions.py @@ -262,7 +262,7 @@ async def sql_to_api_handler(tokens: Tree) -> dict: field_tokens = [i.children[0] for i in fields if i.kind != TokenKind.STAR] for i in where_expr: - if i[0] in ["actor", "author", "feed", "actors"]: + if i[0] in ["actor", "author", "feed"]: api = i break else: From e1f88faefc9e918d1f187de23be9f176b0cda78f Mon Sep 17 00:00:00 2001 From: daniel Date: Sun, 17 Aug 2025 18:51:56 -0500 Subject: [PATCH 086/117] fix: no more console.log plain text passwords --- src/auth_modal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/auth_modal.py b/src/auth_modal.py index 3baa1d1f..40ece8fb 100644 --- a/src/auth_modal.py +++ b/src/auth_modal.py @@ -262,7 +262,11 @@ def reset_field_style(field: Element = input_field) -> None: def on_auth_complete(auth_result: dict) -> None: """Complete authentication and show interface.""" - print(f"Authentication completed: {auth_result}") + safe_to_print = auth_result.copy() + + if safe_to_print["mode"] != "stealth": + safe_to_print["password"] = "********" # noqa: S105 + print(f"Authentication completed: {safe_to_print}") # update global JavaScript state if hasattr(window, "AppState"): From 74aa20b93edbaef2f4125d366c18c0148a210872 Mon Sep 17 00:00:00 2001 From: Walkercito Date: Sun, 17 Aug 2025 21:13:49 -0400 Subject: [PATCH 087/117] feat: maassive file re-struct --- auth-request-atproto.py | 41 ----------------- auth-request-requests.py | 80 ---------------------------------- dev.py | 29 ++++++++++++ index.html | 37 ---------------- src/{ => api}/auth_modal.py | 0 src/{ => api}/auth_session.py | 0 src/{ => core}/parser.py | 0 src/functions.py | 7 ++- src/index.html | 6 +-- src/setup.py | 10 ++--- src/{ => ui}/frontend.py | 0 src/{ => ui}/image_modal.py | 0 src/{ => web}/boot.js | 0 src/{ => web}/favicon.png | Bin src/{ => web}/styles.css | 0 15 files changed, 40 insertions(+), 170 deletions(-) delete mode 100644 auth-request-atproto.py delete mode 100644 auth-request-requests.py create mode 100644 dev.py delete mode 100644 index.html rename src/{ => api}/auth_modal.py (100%) rename src/{ => api}/auth_session.py (100%) rename src/{ => core}/parser.py (100%) rename src/{ => ui}/frontend.py (100%) rename src/{ => ui}/image_modal.py (100%) rename src/{ => web}/boot.js (100%) rename src/{ => web}/favicon.png (100%) rename src/{ => web}/styles.css (100%) diff --git a/auth-request-atproto.py b/auth-request-atproto.py deleted file mode 100644 index 52f894d1..00000000 --- a/auth-request-atproto.py +++ /dev/null @@ -1,41 +0,0 @@ -# Imports -from atproto import Client - - -class Session: - """Class to etablish an auth session.""" - - def __init__(self, username: str, password: str) -> None: - # Bluesky credentials - self.username = username - self.password = password - # Instance client - self.client = Client() - # Access token - self.access_jwt = None - # Refresh token - self.refresh_jwt = None - - def login(self) -> None: - """Create an authenticated session and save tokens.""" - session_info = self.client.login(self.username, self.password) - self.access_jwt = session_info.accessJwt - self.refresh_jwt = session_info.refreshJwt - print("Connexion réussie.") - print("Access token :", self.access_jwt) - print("Refresh token :", self.refresh_jwt) - - def get_profile(self) -> dict: - """Get user profile.""" - return self.client.app.bsky.actor.get_profile({"actor": self.username}) - - -if __name__ == "__main__": - USERNAME = "Nothing_AHAHA" - PASSWORD = "You tought i'll write the password here you fool" # noqa: S105 - - session = Session(USERNAME, PASSWORD) - session.login() - - profile = session.get_profile() - print("Nom affiché :", profile.displayName) diff --git a/auth-request-requests.py b/auth-request-requests.py deleted file mode 100644 index 5949c16c..00000000 --- a/auth-request-requests.py +++ /dev/null @@ -1,80 +0,0 @@ -# Imports -import requests # The system we will actually use - - -class Session: - """Class to etablish an auth session.""" - - def __init__(self, username: str, password: str) -> None: - # Bluesky credentials - self.username = username - self.password = password - self.pds_host = "https://bsky.social" - # Instance client - self.client = requests.Session() - # Access token - self.access_jwt = None - # Refresh token - self.refresh_jwt = None - - def login(self) -> None: - """Create an authenticated session and save tokens.""" - endpoint = f"{self.pds_host}/xrpc/com.atproto.server.createSession" - session_info = self.client.post( - endpoint, - headers={"Content-Type": "application/json"}, - json={ - "identifier": self.username, - "password": self.password, - }, - timeout=30, - ).json() - self.access_jwt = session_info["accessJwt"] - self.refresh_jwt = session_info["refreshJwt"] - self.client.headers.update( - { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.access_jwt}", - }, - ) - - print("Connexion réussie.") - print("Access token :", self.access_jwt) - print("Refresh token :", self.refresh_jwt) - - def get_profile(self) -> dict: - """Get a user profile.""" - endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.getProfile?actor={self.username}" - return self.client.get( - endpoint, - ).json() - - def search(self, query: str) -> dict: - """Search Bluesky.""" - endpoint = f"{self.pds_host}/xrpc/app.bsky.actor.searchActors?q={query}" - return self.client.get( - endpoint, - ).json() - - def get_author_feed(self, actor: str) -> dict: - """Get a specific user feed.""" - endpoint = f"{self.pds_host}/xrpc/app.bsky.feed.getAuthorFeed?actor={actor}" - print(endpoint) - return self.client.get( - endpoint, - ).json() - - -if __name__ == "__main__": - USERNAME = "Nothing_AHAHA" - PASSWORD = "You tought i'll write the password here you fool" # noqa: S105 - - session = Session(USERNAME, PASSWORD) - session.login() - - profile = session.get_profile() - - print(profile) - - search = session.get_actor_feeds("tess.bsky.social") - print(search) diff --git a/dev.py b/dev.py new file mode 100644 index 00000000..f258be90 --- /dev/null +++ b/dev.py @@ -0,0 +1,29 @@ +import http.server +import socketserver +import os +import sys +from pathlib import Path + +# use src as start point +src_dir = Path(__file__).parent / "src" +if src_dir.exists(): + os.chdir(src_dir) + print(f"[*] Serving from: {src_dir.absolute()}") +else: + print("[-] src/ dir not found") + sys.exit(1) + +PORT = 8000 +Handler = http.server.SimpleHTTPRequestHandler + +try: + with socketserver.TCPServer(("", PORT), Handler) as httpd: + print(f"[*] Server running at: http://localhost:{PORT}") + print(f"[*] Open: http://localhost:{PORT}/") + print("[-] Press Ctrl+C to stop") + httpd.serve_forever() +except KeyboardInterrupt: + print("\nServer stopped") +except OSError as e: + print(f"[-] Error: {e}") + print(f"[-] Try a different port: python dev.py --port 8001") \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 73fefa34..00000000 --- a/index.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - diff --git a/src/auth_modal.py b/src/api/auth_modal.py similarity index 100% rename from src/auth_modal.py rename to src/api/auth_modal.py diff --git a/src/auth_session.py b/src/api/auth_session.py similarity index 100% rename from src/auth_session.py rename to src/api/auth_session.py diff --git a/src/parser.py b/src/core/parser.py similarity index 100% rename from src/parser.py rename to src/core/parser.py diff --git a/src/functions.py b/src/functions.py index 6b0c626d..5784a2e8 100644 --- a/src/functions.py +++ b/src/functions.py @@ -1,12 +1,11 @@ """The main script file for Pyodide.""" -from js import Event, document, window -from pyodide.ffi import create_proxy -from pyodide.ffi.wrappers import set_timeout - import frontend from frontend import CLEAR_BUTTON, EXECUTE_BUTTON, clear_interface, update_table +from js import Event, document, window from parser import ParentKind, Token, TokenKind, Tree, parse, tokenize +from pyodide.ffi import create_proxy +from pyodide.ffi.wrappers import set_timeout def flatten_response(data: dict) -> dict: diff --git a/src/index.html b/src/index.html index 14819f2c..fdb3d7ba 100644 --- a/src/index.html +++ b/src/index.html @@ -6,8 +6,8 @@ The Social Query Language v1.0 - - + + @@ -159,7 +159,7 @@
- +