From bcc7e5d8c2326fe50fdb51a3ea314f1a6d80559f Mon Sep 17 00:00:00 2001 From: Lord Lumineer <56554075+LordLumineer@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:24:31 -0500 Subject: [PATCH 01/15] V1.0.5 BETA (#15) * :sparkles: v1.0.5 - Try It Out * Removed the bridge to the titles and cleaned so of the code * feat: :test_tube: Better CI/CD to include the pre-release branch Invalid version on purpose to test the version validation Invalid version on purpose to test the version validation * run version check on pre-release PR * Now with valid version --- .github/workflows/ci-cd.yml | 45 ++++++++++++++++++++++++++++++++----- app/main.py | 2 +- static/index.html | 14 +++++------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 1db671c..42e3842 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,9 +2,9 @@ name: CI/CD on: push: - branches: [ master ] + branches: [ master, pre-release ] pull_request: - branches: [ master ] + branches: [ master, pre-release ] jobs: test: @@ -15,6 +15,14 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Check version for pre-release + if: github.base_ref == 'pre-release' + run: | + validState=$(grep -oP 'version=\"\K[0-9.]+-\K['-']+' app/main.py || echo "valid") + if [ "$validState" != "valid" ]; then + echo "Fail: invalid development state (missing)" + exit 1 + fi - name: Set up Python uses: actions/setup-python@v5 @@ -43,7 +51,7 @@ jobs: deploy: needs: test - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/pre-release' runs-on: ubuntu-latest steps: - name: Checkout @@ -55,7 +63,11 @@ jobs: id: get_version run: | version=$(grep -oP "version=\"\K[0-9.]+" app/main.py || echo "0.0.0") + versionState=$(grep -oP 'version=\"\K[0-9.]+-\K[a-zA-Z]+' app/main.py || echo "dev") + versionSub=$(grep -oP 'version=\"\K[0-9.]+-\K[a-zA-Z]+-\K[a-zA-Z0-9.]+' app/main.py || echo "missing") echo "version=$version" >> $GITHUB_OUTPUT + echo "versionState=$versionState" >> $GITHUB_OUTPUT + echo "versionSub=$versionSub" >> $GITHUB_OUTPUT - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -69,10 +81,33 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - - name: Build and push Docker images + - name: Build and push Docker images (master) + if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v6 with: push: true tags: | ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:latest - ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:${{ steps.get_version.outputs.version }} \ No newline at end of file + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:${{ steps.get_version.outputs.version }} + + - name: Build and push Docker images (pre-release with subversion) + if: github.ref == 'refs/heads/pre-release' && steps.get_version.outputs.versionSub != 'missing' + uses: docker/build-push-action@v6 + with: + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev-latest + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:latest-${{ steps.get_version.outputs.versionState }}-latest + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:latest-${{ steps.get_version.outputs.versionState }}-${{ steps.get_version.outputs.versionSub }} + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:${{ steps.get_version.outputs.version }}-${{ steps.get_version.outputs.versionState }}-latest + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:${{ steps.get_version.outputs.version }}-${{ steps.get_version.outputs.versionState }}-${{ steps.get_version.outputs.versionSub }} + + - name: Build and push Docker images (pre-release without subversion) + if: github.ref == 'refs/heads/pre-release' && steps.get_version.outputs.versionSub == 'missing' + uses: docker/build-push-action@v6 + with: + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:dev-latest + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:latest-${{ steps.get_version.outputs.versionState }} + ${{ secrets.DOCKER_USERNAME }}/kofi-websocket:${{ steps.get_version.outputs.version }}-${{ steps.get_version.outputs.versionState }} \ No newline at end of file diff --git a/app/main.py b/app/main.py index f9553ed..b51fbda 100644 --- a/app/main.py +++ b/app/main.py @@ -29,7 +29,7 @@ active_connections: dict[str, WebSocket] = {} app = FastAPI( - version="1.0.5-beta_1", + version="1.0.5-beta-v1", docs_url=None, # Disable Swagger UI redoc_url=None # Disable ReDoc ) diff --git a/static/index.html b/static/index.html index 7f9cb71..b5974a8 100644 --- a/static/index.html +++ b/static/index.html @@ -4,14 +4,14 @@ - Ko-fi WebSocket Bridge + Ko-fi WebSocket - + @@ -24,7 +24,7 @@ - + @@ -581,7 +581,7 @@

- Ko-fi WebSocket Bridge + Ko-fi WebSocket
Loading version...

@@ -669,8 +669,7 @@

3. Connect to WebSocket

4. Handle Events

Example code to handle Ko-fi notifications:

-

-ws.onmessage = (event) => {
+                
ws.onmessage = (event) => {
     const data = JSON.parse(event.data);
     switch(data.type) {
         case "Donation":
@@ -689,8 +688,7 @@ 

4. Handle Events

Example Implementation

Here's a complete, fully functional, example that connects on page load:

-

-<!-- Add this to your HTML -->
+                
<!-- Add this to your HTML -->
 <div id="kofi-notifications"></div>
 
 <!-- Add this to your JavaScript file or <script> tag -->

From c3014a3467b8284324127ece8951b9652918d1f6 Mon Sep 17 00:00:00 2001
From: Lord Lumineer <56554075+LordLumineer@users.noreply.github.com>
Date: Fri, 7 Feb 2025 22:47:18 -0500
Subject: [PATCH 02/15] beta v2 (#16)

---
 app/main.py                                      |  12 ++++++++----
 static/index.html                                |   8 ++++----
 static/kofi-websocket-icon.png                   | Bin 0 -> 9979 bytes
 ...ocket-preview.png => kofi-websocket-logo.png} | Bin
 tests/test_main.py                               |  12 +++---------
 5 files changed, 15 insertions(+), 17 deletions(-)
 create mode 100644 static/kofi-websocket-icon.png
 rename static/{kofi-websocket-preview.png => kofi-websocket-logo.png} (100%)

diff --git a/app/main.py b/app/main.py
index b51fbda..6cff7bc 100644
--- a/app/main.py
+++ b/app/main.py
@@ -29,7 +29,7 @@
 active_connections: dict[str, WebSocket] = {}
 
 app = FastAPI(
-    version="1.0.5-beta-v1",
+    version="1.0.5-beta-v2",
     docs_url=None,  # Disable Swagger UI
     redoc_url=None  # Disable ReDoc
 )
@@ -55,9 +55,13 @@ async def root():
 async def _favicon():
     return FileResponse("static/favicon.ico")
 
-@app.get("/kofi-websocket-preview.png")
-async def _favicon():
-    return FileResponse("static/kofi-websocket-preview.png")
+@app.get("/logo.png")
+async def _logo():
+    return FileResponse("static/kofi-websocket-logo.png")
+
+@app.get("/icon.png")
+async def _icon():
+    return FileResponse("static/kofi-websocket-icon.png")
 
 @app.get("/ping")
 async def _ping():
diff --git a/static/index.html b/static/index.html
index b5974a8..f255812 100644
--- a/static/index.html
+++ b/static/index.html
@@ -14,20 +14,20 @@
     
 
     
-    
+    
     
     
     
     
-    
+    
 
     
-    
+    
     
     
-    
+    
 
     
diff --git a/static/kofi-websocket-icon.png b/static/kofi-websocket-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..830e04003aea709f84ac00ed87c688e1b6e4b379
GIT binary patch
literal 9979
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
YHD4(i{M8yfxCT*>Rh9W6W%A$u07bAS" in response.content
 
-
-def test_favicon_endpoint(client):
-    """Test the favicon endpoint."""
-    response = client.get("/favicon.ico")
-    assert response.status_code == 200
-    assert "image/" in response.headers["content-type"]
-
-def test_SEO_image(client):
+@pytest.mark.parametrize("endpoint", ["/favicon.ico", "/logo.png", "/icon.png"])
+def test_image_endpoint(client, endpoint):
     """Test the favicon endpoint."""
-    response = client.get("/kofi-websocket-preview.png")
+    response = client.get(endpoint)
     assert response.status_code == 200
     assert "image/" in response.headers["content-type"]
 

From 323ed54a9fb816057821a5b784371eaa9a7583cc Mon Sep 17 00:00:00 2001
From: Lord Lumineer <56554075+LordLumineer@users.noreply.github.com>
Date: Sat, 8 Feb 2025 00:43:44 -0500
Subject: [PATCH 03/15] V1.0.5   try it out (#17)

* feat: :sparkles: v1.0.5 - Try It Out

Added a box to try out the service directly in the home page

* v1.0.5 - BETA

* beta 1

* removed the bridge to the titles and cleaned so of the code

* feat: :test_tube: Better CI/CD to include the pre-release branch

Invalid version on purpose to test the version validation

Invalid version on purpose to test the version validation

* run version check on pre-release PR

* Now with valid version

* beta v2 fixed homepage tags

* fix: :bug: Fixed wrong endpoint for SEO
---
 app/main.py       | 2 +-
 static/index.html | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/app/main.py b/app/main.py
index 6cff7bc..b51cc26 100644
--- a/app/main.py
+++ b/app/main.py
@@ -29,7 +29,7 @@
 active_connections: dict[str, WebSocket] = {}
 
 app = FastAPI(
-    version="1.0.5-beta-v2",
+    version="1.0.5-beta-v3",
     docs_url=None,  # Disable Swagger UI
     redoc_url=None  # Disable ReDoc
 )
diff --git a/static/index.html b/static/index.html
index f255812..71a7ca0 100644
--- a/static/index.html
+++ b/static/index.html
@@ -20,14 +20,14 @@
     
     
     
-    
+    
 
     
     
     
     
-    
+    
 
     

From c91ef0ed8a5f3a7882d67c4ec58b76d4cbee8490 Mon Sep 17 00:00:00 2001
From: Lord Lumineer <56554075+LordLumineer@users.noreply.github.com>
Date: Mon, 10 Feb 2025 21:51:47 -0500
Subject: [PATCH 04/15] :sparkles: Refactoring of the homepage to use libraries
 (#18)

---
 CHANGES.md                                   |   17 +
 app/main.py                                  |   40 +-
 static/{kofi-websocket-icon.png => icon.png} |  Bin
 static/index.html                            | 1098 +++++-------------
 static/{kofi-websocket-logo.png => logo.png} |  Bin
 static/script.js                             |  300 +++++
 tests/test_main.py                           |   23 -
 7 files changed, 642 insertions(+), 836 deletions(-)
 rename static/{kofi-websocket-icon.png => icon.png} (100%)
 rename static/{kofi-websocket-logo.png => logo.png} (100%)
 create mode 100644 static/script.js

diff --git a/CHANGES.md b/CHANGES.md
index 66c0c6c..adac01f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,23 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [1.1.0 - UNRELEASED] - 2025-02-10
+
+### Changes in 1.1.0
+
+- You can try the service directly from the homepage now
+  - The button to display the try it out section is located at the bottom right of the page (and is always visible)
+- Addition of a navigation panel to quickly jump to the different sections of the homepage
+- Theme switcher to change between light and dark mode
+
+- Now using libraries for the UI of the homepage
+  - TailwindCSS
+  - DaisyUI
+  - Json Formatter (to format the json in the try it out section)
+- General refactoring of the homepage
+
+- Reduced the endpoints by directly mounting the static files in the server
+
 ## [1.0.4] - 2025-02-02
 
 ### Changes in 1.0.4
diff --git a/app/main.py b/app/main.py
index b51cc26..4b41ef3 100644
--- a/app/main.py
+++ b/app/main.py
@@ -22,14 +22,15 @@
 import asyncio
 import json
 from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Form
-from fastapi.responses import FileResponse, HTMLResponse
+# from fastapi.responses import FileResponse, HTMLResponse
 from fastapi.exceptions import HTTPException
+from fastapi.staticfiles import StaticFiles
 from starlette.middleware.cors import CORSMiddleware
 
 active_connections: dict[str, WebSocket] = {}
 
 app = FastAPI(
-    version="1.0.5-beta-v3",
+    version="1.1.0-beta-v1",
     docs_url=None,  # Disable Swagger UI
     redoc_url=None  # Disable ReDoc
 )
@@ -43,25 +44,25 @@
 )
 
 
-@app.get("/", response_class=HTMLResponse)
-async def root():
-    """
-    Returns the home page of the application, which explains the purpose
-    and functionalities of the Ko-fi WebSocket bridge.
-    """
-    return FileResponse("static/index.html")
+# @app.get("/", response_class=HTMLResponse)
+# async def root():
+#     """
+#     Returns the home page of the application, which explains the purpose
+#     and functionalities of the Ko-fi WebSocket bridge.
+#     """
+#     return FileResponse("static/index.html")
 
-@app.get("/favicon.ico")
-async def _favicon():
-    return FileResponse("static/favicon.ico")
+# @app.get("/favicon.ico")
+# async def _favicon():
+#     return FileResponse("static/favicon.ico")
 
-@app.get("/logo.png")
-async def _logo():
-    return FileResponse("static/kofi-websocket-logo.png")
+# @app.get("/logo.png")
+# async def _logo():
+#     return FileResponse("static/kofi-websocket-logo.png")
 
-@app.get("/icon.png")
-async def _icon():
-    return FileResponse("static/kofi-websocket-icon.png")
+# @app.get("/icon.png")
+# async def _icon():
+#     return FileResponse("static/kofi-websocket-icon.png")
 
 @app.get("/ping")
 async def _ping():
@@ -135,3 +136,6 @@ async def websocket_endpoint(websocket: WebSocket, verification_token: str):
     except WebSocketDisconnect:
         if verification_token in active_connections:
             del active_connections[verification_token]
+
+# Keep it at the end to prevent routing issues
+app.mount("/", StaticFiles(directory="static",html = True), name="static")
diff --git a/static/kofi-websocket-icon.png b/static/icon.png
similarity index 100%
rename from static/kofi-websocket-icon.png
rename to static/icon.png
diff --git a/static/index.html b/static/index.html
index 71a7ca0..2501809 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1,5 +1,5 @@
 
-
+
 
 
     
@@ -20,656 +20,296 @@
     
     
     
-    
+    
 
     
     
     
     
-    
+    
 
+    
+    
+    
+
+    
     
+        id="prism-light">
     
-
-    
 
 
-
-    
-
-

- Ko-fi WebSocket -
-
Loading version...
-

-

- A modern, real-time bridge between Ko-fi webhooks and WebSocket connections, enabling instant - notifications for your Ko-fi events. -

-
- View on GitHub + +
+
+ +
+ + -
-
-

Real-time Updates

-

Instant notification delivery via WebSocket connections

-
-
-

Secure

-

Token-based authentication for secure communications

-
-
-

Universal Support

-

Compatible with all Ko-fi event types

+
+
+
+

Real-time Updates

+

Instant notification delivery via WebSocket connections

+
-
-

Easy Integration

-

Simple to integrate with any WebSocket client

+
+
+

Secure

+

Token-based authentication for secure communications

+
-
- -
-

Try It Out

-
-
- - - - +
+
+

Universal Support

+

Compatible with all Ko-fi event types

-
-
- WebSocket Terminal - -
-
+
+
+
+

Easy Integration

+

Simple to integrate with any WebSocket client

-
-

Quick Start

-

Connect to the WebSocket endpoint using your verification token:

-
-
{{ WSprotocol }}//{{ HOSTNAME }}/ws/{verification_token}
+ + +
+
+

Quick Start

+

Connect to the WebSocket endpoint using your verification token:

+
+
{{ WSprotocol }}//{{ HOSTNAME }}/ws/{verification_token}
+
-
-

Getting Started

-

1. Get Your Verification Token

-

To find your Ko-fi verification token:

-
    -
  1. Go to your Ko-fi - Dashboard
  2. -
  3. Click on "API" in the left menu (under "More")
  4. -
  5. Find your verification token in the "Advanced" section
  6. -
  7. Copy the token, you'll need it for the WebSocket setup
  8. -
- -

2. Set Up Ko-fi Webhook

-

In your Ko-fi settings, set your Webhook URL to:

-
-
{{ HTTPprotocol }}//{{ HOSTNAME }}/webhook
-
+
+
+

Getting Started

+

1. Get Your Verification Token

+

To find your Ko-fi verification token:

+
    +
  1. Go to your Ko-fi + Dashboard
  2. +
  3. Click on "API" in the left menu (under "More")
  4. +
  5. Find your verification token in the "Advanced" section
  6. +
  7. Copy the token, you'll need it for the WebSocket setup
  8. +
+ +

2. Set Up Ko-fi Webhook

+

In your Ko-fi settings, set your Webhook URL to:

+
+
{{ HTTPprotocol }}//{{ HOSTNAME }}/webhook
+
-

3. Connect to WebSocket

-

Connect to the WebSocket using your verification token:

-
-
const ws = new WebSocket('{{ WSprotocol }}//{{ HOSTNAME }}/ws/YOUR_VERIFICATION_TOKEN');
-
+

3. Connect to WebSocket

+

Connect to the WebSocket using your verification token:

+
+
const ws = new WebSocket('{{ WSprotocol }}//{{ HOSTNAME }}/ws/YOUR_VERIFICATION_TOKEN');
+
-

4. Handle Events

-

Example code to handle Ko-fi notifications:

-
-
ws.onmessage = (event) => {
+                

4. Handle Events

+

Example code to handle Ko-fi notifications:

+
+
ws.onmessage = (event) => {
     const data = JSON.parse(event.data);
     switch(data.type) {
         case "Donation":
@@ -682,13 +322,14 @@ 

4. Handle Events

console.log(`New shop order from ${data.from_name}`); break; } -};
-
+}; +
+
-

Example Implementation

-

Here's a complete, fully functional, example that connects on page load:

-
-
<!-- Add this to your HTML -->
+                

Example Implementation

+

Here's a complete, fully functional, example that connects on page load:

+
+
<!-- Add this to your HTML -->
 <div id="kofi-notifications"></div>
 
 <!-- Add this to your JavaScript file or <script> tag -->
@@ -788,257 +429,124 @@ 

Example Implementation

}); </script>
+
-
-
- - - - - + + + + + + \ No newline at end of file diff --git a/static/kofi-websocket-logo.png b/static/logo.png similarity index 100% rename from static/kofi-websocket-logo.png rename to static/logo.png diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..f619fbf --- /dev/null +++ b/static/script.js @@ -0,0 +1,300 @@ +// Fetch the version from the server +fetch('/version') + .then(response => response.json()) + .then(data => { + const version = 'v' + data.version; + document.getElementById('version-badge').textContent = version; + }) + .catch(error => { + console.error('Error fetching version:', error); + document.getElementById('version-badge').textContent = 'Version unknown'; + }); + +document.querySelectorAll('.code-block').forEach(block => { + const pre = block.querySelector('pre'); + + // Set the correct protocol based on the current page + if (pre.textContent.includes('protocol')) { + const protocol = window.location.protocol; + pre.textContent = pre.textContent.replace('{{ WSprotocol }}', protocol.replace('http', 'ws')); + pre.textContent = pre.textContent.replace('{{ HTTPprotocol }}', protocol); + } + + // Set the correct hostname based on the current page + if (pre.textContent.includes('HOSTNAME')) { + const hostname = window.location.hostname; + const port = window.location.port; + pre.textContent = pre.textContent.replace('{{ HOSTNAME }}', hostname + (port ? ':' + port : '')); + } + + // Add copy button only to the webhook setup code block + if (pre.id === 'webhook-url' || pre.id === 'example-implementation') { + const copyButton = document.createElement('button'); + copyButton.className = 'copy-button'; + copyButton.innerHTML = ` + + + + `; + + copyButton.addEventListener('click', async () => { + const code = pre.textContent; + await navigator.clipboard.writeText(code); + + copyButton.classList.add('copied'); + copyButton.innerHTML = ` + + + + `; + + setTimeout(() => { + copyButton.classList.remove('copied'); + copyButton.innerHTML = ` + + + `; + }, 2000); + }); + + block.appendChild(copyButton); + } + + // Convert code blocks to use Prism.js + let language = 'javascript'; + if (pre.id === 'example-implementation') language = 'html'; + pre.className = `language-${language}`; + pre.innerHTML = `${pre.innerHTML}`; +}); + +// Highlight all code blocks +Prism.highlightAll(); + +let testWs = null; + +function formatJSON(jsonData) { + // The second parameter (2) controls the default expand depth + const formatter = new JSONFormatter(jsonData, 2, + { + theme: 'dark', + animateOpen: true, + animateClose: true, + hoverPreviewEnabled: true, + hoverPreviewArrayCount: 100, + hoverPreviewFieldCount: 5 + } + ); + return formatter.render(); +} + +function writeToTerminal(message, type = 'data') { + const terminal = document.getElementById('terminal'); + const entry = document.createElement('div'); + entry.className = `terminal-entry ${type === 'error' ? 'text-error' : type === 'success' ? 'text-success' : ''}`; + console.log(message); + + if (typeof message === 'object') { + if (Object.keys(message).includes('status')) { + entry.textContent = "Status: " + message['status']; + } else { + const formattedElement = formatJSON(message); + entry.appendChild(formattedElement); // Changed from innerHTML to appendChild + } + } else { + entry.textContent = message; + } + + terminal.appendChild(entry); + terminal.scrollTop = terminal.scrollHeight; +} + +function clearTerminal() { + document.getElementById('terminal').innerHTML = ''; +} + +function updateButtons(connected) { + const connectButton = document.getElementById('connect-button'); + document.getElementById('send-test-button').disabled = !connected; + document.getElementById('token-input').disabled = connected; + + connectButton.textContent = connected ? 'Disconnect' : 'Connect'; + connectButton.classList.toggle('connected', connected); +} + +function toggleConnection() { + if (testWs) { + disconnectTestSocket(); + } else { + connectTestSocket(); + } +} + +function connectTestSocket() { + const token = document.getElementById('token-input').value.trim(); + if (!token) { + writeToTerminal('Please enter a verification token', 'error'); + return; + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/${token}`; + + testWs = new WebSocket(wsUrl); + + testWs.onopen = () => { + writeToTerminal('WebSocket Connected', 'success'); + updateButtons(true); + }; + + testWs.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + writeToTerminal(data); + } catch { + writeToTerminal(event.data, 'data'); + } + }; + + testWs.onclose = () => { + writeToTerminal('WebSocket Disconnected', 'error'); + updateButtons(false); + testWs = null; + }; + + testWs.onerror = (error) => { + writeToTerminal(`WebSocket Error: ${error.message}`, 'error'); + }; +} + +function disconnectTestSocket() { + if (testWs) { + testWs.close(); + } +} + +function sendTestMessage() { + if (!testWs) { + writeToTerminal('WebSocket not connected', 'error'); + return; + } + + const token = document.getElementById('token-input').value.trim(); + if (!token) { + writeToTerminal('Please enter a verification token', 'error'); + return; + } + + const baseUrl = `${window.location.protocol}//${window.location.host}`; + + writeToTerminal('Sending test message...', 'data'); + + const webhookData = { + verification_token: token, + message_id: `test_${Date.now()}`, + timestamp: new Date().toISOString(), + type: "Donation", + is_public: true, + from_name: "Test User", + message: "This is a test donation!", + amount: "5.00", + url: "https://ko-fi.com/test", + email: "test@example.com", + currency: "USD", + is_subscription_payment: false, + is_first_subscription_payment: false, + kofi_transaction_id: `TX_${Date.now()}`, + shop_items: null, + tier_name: null, + shipping: null + }; + + const formData = new URLSearchParams(); + formData.append('data', JSON.stringify(webhookData)); + + fetch(`${baseUrl}/webhook`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }) + .then(response => response.json()) + .then(data => writeToTerminal(data, 'success')) + .catch(error => writeToTerminal(`Error: ${error.message}`, 'error')); +} + +document.addEventListener('scroll', highlightSection); +document.addEventListener('DOMContentLoaded', () => { + highlightSection(); + + // Theme management + // window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + // const isDark = e.matches; + // document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); + // }); + const themeToggle = document.getElementById('theme-toggle'); + const sunIcon = document.getElementById('sun-icon'); + const moonIcon = document.getElementById('moon-icon'); + + function setTheme(isDark) { + // Update the color scheme preference + document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + + // Toggle Prism themes + document.getElementById('prism-light').disabled = isDark; + document.getElementById('prism-dark').disabled = !isDark; + + sunIcon.classList.toggle('hidden', !isDark); + moonIcon.classList.toggle('hidden', isDark); + } + + // Initialize theme based on system preference + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + setTheme(prefersDark); + + // Theme toggle button handler + themeToggle.addEventListener('click', () => { + const currentTheme = document.documentElement.getAttribute('data-theme'); + setTheme(currentTheme === 'light'); + }); + + highlightSection(); +}); + +window.addEventListener('hashchange', () => { + setTimeout(highlightSection, 100); +}); + +function highlightSection() { + const sections = ['header', 'features', 'quick-start', 'getting-started']; + let current = ''; + + // Calculate scroll progress + const scrollProgress = document.getElementById('scroll-progress'); + const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100; + scrollProgress.style.height = `${scrollPercent}%`; + + // Find current section + for (const id of sections) { + const section = document.getElementById(id); + if (section && section.getBoundingClientRect().top <= 100) { + current = id; + } + } + + // Update navigation links + document.querySelectorAll('nav a').forEach(link => { + const href = link.getAttribute('href').substring(1); // Remove # + if (href === current) { + link.classList.add('text-primary', 'font-medium'); + } else { + link.classList.remove('text-primary', 'font-medium'); + } + }); +} + +// Add scroll event listener +window.addEventListener('scroll', () => { + requestAnimationFrame(highlightSection); +}); \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 1b1b421..387dcce 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -180,21 +180,6 @@ def test_version_endpoint(client): assert response.json() == {"version": app.version} -def test_root_endpoint(client): - """Test the root endpoint.""" - response = client.get("/") - assert response.status_code == 200 - assert response.headers["content-type"].startswith("text/html") - assert b"" in response.content - -@pytest.mark.parametrize("endpoint", ["/favicon.ico", "/logo.png", "/icon.png"]) -def test_image_endpoint(client, endpoint): - """Test the favicon endpoint.""" - response = client.get(endpoint) - assert response.status_code == 200 - assert "image/" in response.headers["content-type"] - - def test_webhook_missing_verification_token(client): """Test webhook endpoint with missing verification token.""" invalid_data = { @@ -210,14 +195,6 @@ def test_webhook_missing_verification_token(client): assert "Missing verification_token" in response.json()["detail"] -@pytest.mark.asyncio -async def test_invalid_websocket_token(): - """Test WebSocket connection with invalid token.""" - with pytest.raises(WebSocketDisconnect): - with TestClient(app).websocket_connect("/ws/") as websocket: - websocket.send_text("ping") - - @pytest.mark.asyncio async def test_multiple_websocket_connections(): """Test multiple WebSocket connections with the same token.""" From 33819f2650872c9f7cdc4da5d1b5a3302793e943 Mon Sep 17 00:00:00 2001 From: Lord Lumineer <56554075+LordLumineer@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:40:38 -0500 Subject: [PATCH 05/15] :sparkles: Allow Multple connection with same token + cleaned bit the Home Page --- .vscode/settings.json | 1 + CHANGES.md | 4 +- app/main.py | 70 ++++---- static/index.html | 361 ++++++++++++++++++------------------------ static/og_image.png | Bin 0 -> 272342 bytes static/script.js | 122 ++++++++------ 6 files changed, 263 insertions(+), 295 deletions(-) create mode 100644 static/og_image.png diff --git a/.vscode/settings.json b/.vscode/settings.json index 7450b68..3480667 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,6 +14,7 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "cSpell.words": [ + "Iconify", "kofi", "pytest" ] diff --git a/CHANGES.md b/CHANGES.md index adac01f..61d2952 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes in 1.1.0 +- You can now have multiple websockets connected with the same token, without having the other connection not receiving the messages + - You can try the service directly from the homepage now - The button to display the try it out section is located at the bottom right of the page (and is always visible) - Addition of a navigation panel to quickly jump to the different sections of the homepage @@ -21,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Json Formatter (to format the json in the try it out section) - General refactoring of the homepage -- Reduced the endpoints by directly mounting the static files in the server +- Reduced the amount of endpoints by directly mounting the static files in the server ## [1.0.4] - 2025-02-02 diff --git a/app/main.py b/app/main.py index 4b41ef3..738eb77 100644 --- a/app/main.py +++ b/app/main.py @@ -20,6 +20,7 @@ """ import asyncio +from collections import defaultdict import json from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Form # from fastapi.responses import FileResponse, HTMLResponse @@ -27,10 +28,11 @@ from fastapi.staticfiles import StaticFiles from starlette.middleware.cors import CORSMiddleware -active_connections: dict[str, WebSocket] = {} +# Change from dict[str, WebSocket] to dict[str, set[WebSocket]] +active_connections: dict[str, set[WebSocket]] = defaultdict(set) app = FastAPI( - version="1.1.0-beta-v1", + version="1.1.0-beta-v2", docs_url=None, # Disable Swagger UI redoc_url=None # Disable ReDoc ) @@ -43,27 +45,6 @@ allow_headers=["*"], ) - -# @app.get("/", response_class=HTMLResponse) -# async def root(): -# """ -# Returns the home page of the application, which explains the purpose -# and functionalities of the Ko-fi WebSocket bridge. -# """ -# return FileResponse("static/index.html") - -# @app.get("/favicon.ico") -# async def _favicon(): -# return FileResponse("static/favicon.ico") - -# @app.get("/logo.png") -# async def _logo(): -# return FileResponse("static/kofi-websocket-logo.png") - -# @app.get("/icon.png") -# async def _icon(): -# return FileResponse("static/kofi-websocket-icon.png") - @app.get("/ping") async def _ping(): return {"message": "pong"} @@ -93,22 +74,30 @@ async def ko_fi_webhook(data: str = Form(...)): if not verification_token: raise HTTPException( status_code=400, detail="Missing verification_token") + + # Get all connections for this token if verification_token in active_connections: - websocket = active_connections[verification_token] - for _ in range(3): # Try 3 times - try: - await websocket.send_json(webhook_data) - break - except WebSocketDisconnect: - if verification_token in active_connections: - del active_connections[verification_token] - break - except (ConnectionError, RuntimeError): - await asyncio.sleep(1) - else: - if verification_token in active_connections: + closed_connections = set() + for websocket in active_connections[verification_token]: + for _ in range(3): # Try 3 times + try: + await websocket.send_json(webhook_data) + break + except WebSocketDisconnect: + closed_connections.add(websocket) + break + except (ConnectionError, RuntimeError): + await asyncio.sleep(1) + else: + closed_connections.add(websocket) await websocket.close() - del active_connections[verification_token] + + # Remove closed connections + active_connections[verification_token].difference_update( + closed_connections) + if not active_connections[verification_token]: + del active_connections[verification_token] + return {"status": "success"} @@ -126,7 +115,7 @@ async def websocket_endpoint(websocket: WebSocket, verification_token: str): active connections dictionary. """ await websocket.accept() - active_connections[verification_token] = websocket + active_connections[verification_token].add(websocket) try: while True: message = await websocket.receive_text() @@ -134,8 +123,9 @@ async def websocket_endpoint(websocket: WebSocket, verification_token: str): await websocket.send_text("pong") # Keep connection alive except WebSocketDisconnect: - if verification_token in active_connections: + active_connections[verification_token].discard(websocket) + if not active_connections[verification_token]: del active_connections[verification_token] # Keep it at the end to prevent routing issues -app.mount("/", StaticFiles(directory="static",html = True), name="static") +app.mount("/", StaticFiles(directory="static", html=True), name="static") diff --git a/static/index.html b/static/index.html index 2501809..88f7b6b 100644 --- a/static/index.html +++ b/static/index.html @@ -1,5 +1,5 @@ - + @@ -20,14 +20,14 @@ - + - + - + @@ -42,14 +42,20 @@ + + + @@ -60,16 +66,10 @@ text-overflow: ellipsis; } - /* - overflow: auto; - } - - code[class*="language-"] { - white-space: pre-wrap; - } - */ .code-block { position: relative; + display: grid; + width: 100%; } .copy-button { @@ -112,90 +112,6 @@ .terminal-entry .jsoneditor-menu { display: none; } - - [data-theme="dark"] .jsoneditor { - --jse-theme-color: #2a2a2a; - --jse-theme-color-highlight: #3a3a3a; - } - - [data-theme="light"] .jsoneditor { - --jse-theme-color: #f5f5f5; - --jse-theme-color-highlight: #e5e5e5; - } - - /* JSON Formatter theme customization */ - /*[data-theme="dark"] .json-formatter-row { - --json-formatter-color: #e0e0e0; - --json-formatter-str-color: #89ddf3; - --json-formatter-num-color: #efb27c; - --json-formatter-bool-color: #ef9a9a; - --json-formatter-null-color: #b39ddb; - --json-formatter-bracket-color: #8c8c8c; - --json-formatter-key-color: #80cbc4; - } - - [data-theme="light"] .json-formatter-row { - --json-formatter-color: #2a2a2a; - --json-formatter-str-color: #0277bd; - --json-formatter-num-color: #e65100; - --json-formatter-bool-color: #b71c1c; - --json-formatter-null-color: #4527a0; - --json-formatter-bracket-color: #616161; - --json-formatter-key-color: #00695c; - } - - .json-formatter-row { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - font-size: 0.875rem; - line-height: 1.25rem; - } - - .json-formatter-row .json-formatter-string { - color: var(--json-formatter-str-color); - } - - .json-formatter-row .json-formatter-number { - color: var(--json-formatter-num-color); - } - - .json-formatter-row .json-formatter-boolean { - color: var(--json-formatter-bool-color); - } - - .json-formatter-row .json-formatter-null { - color: var(--json-formatter-null-color); - } - - .json-formatter-row .json-formatter-bracket { - color: var(--json-formatter-bracket-color); - } - - .json-formatter-row .json-formatter-key { - color: var(--json-formatter-key-color); - padding-right: 0.5em; - } - - .json-formatter-row, - .json-formatter-row a { - color: var(--json-formatter-color); - } - - .json-formatter-row .json-formatter-toggler { - opacity: 0.6; - margin-right: 0.5em; - } - - .json-formatter-row .json-formatter-toggler:after { - content: "►"; - } - - .json-formatter-row.json-formatter-open > .json-formatter-toggler:after { - content: "▼"; - } - - .json-formatter-row .json-formatter-toggler:hover { - opacity: 1; - }*/ @@ -203,20 +119,16 @@
-
@@ -282,34 +197,60 @@

Quick Start

Getting Started

-

1. Get Your Verification Token

-

To find your Ko-fi verification token:

-
    -
  1. Go to your Ko-fi - Dashboard
  2. -
  3. Click on "API" in the left menu (under "More")
  4. -
  5. Find your verification token in the "Advanced" section
  6. -
  7. Copy the token, you'll need it for the WebSocket setup
  8. -
- -

2. Set Up Ko-fi Webhook

-

In your Ko-fi settings, set your Webhook URL to:

-
-
{{ HTTPprotocol }}//{{ HOSTNAME }}/webhook
-
- -

3. Connect to WebSocket

-

Connect to the WebSocket using your verification token:

-
-
const ws = new WebSocket('{{ WSprotocol }}//{{ HOSTNAME }}/ws/YOUR_VERIFICATION_TOKEN');
-
- -

4. Handle Events

-

Example code to handle Ko-fi notifications:

-
-
ws.onmessage = (event) => {
+                
+
+ +
+ 1. Get Your Verification Token +
+
+

To find your Ko-fi verification token:

+
    +
  1. Go to your Ko-fi + Dashboard
  2. +
  3. Click on "API" in the left menu (under "More")
  4. +
  5. Find your verification token in the "Advanced" section
  6. +
  7. Copy the token, you'll need it for the WebSocket setup
  8. +
+
+
+
+ +
+ 2. Set Up Ko-fi Webhook +
+
+

In your Ko-fi settings, set your Webhook URL to:

+
+
{{ HTTPprotocol }}//{{ HOSTNAME }}/webhook
+
+
+
+
+ +
+ 3. Connect to WebSocket +
+
+

Connect to the WebSocket using your verification token:

+
+
const ws = new WebSocket('{{ WSprotocol }}//{{ HOSTNAME }}/ws/YOUR_VERIFICATION_TOKEN');
+
+
+
+
+ +
+ 4. Handle Events +
+
+

Example code to handle Ko-fi notifications:

+
+
ws.onmessage = (event) => {
     const data = JSON.parse(event.data);
     switch(data.type) {
         case "Donation":
@@ -324,9 +265,16 @@ 

4. Handle Events

} };
+
+
+
+
+
-

Example Implementation

+
+
+

Example Implementation

Here's a complete, fully functional, example that connects on page load:

<!-- Add this to your HTML -->
@@ -433,7 +381,6 @@ 

Example Implementation

-