From 13b87b97a38213cf0c2c7cdbc858f6b0664c69b9 Mon Sep 17 00:00:00 2001 From: "Morgan 'ARR\\!' Allen" Date: Wed, 18 Oct 2023 13:51:32 -0700 Subject: [PATCH] its a demo --- app.py | 71 ++++++++++++++ requirements.txt | 2 + static/index.js | 219 +++++++++++++++++++++++++++++++++++++++++++ templates/index.html | 41 ++++++++ 4 files changed, 333 insertions(+) create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 static/index.js create mode 100644 templates/index.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..05077fb --- /dev/null +++ b/app.py @@ -0,0 +1,71 @@ +import time +import json +from random import choice, random +from flask import Flask, render_template +from flask_sock import Sock + +app = Flask(__name__) +sock = Sock(app) + +app.debug = True + +@app.after_request +def add_header(r): + """ + Add headers to both force latest IE rendering engine or Chrome Frame, + and also to cache the rendered page for 10 minutes. + """ + r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + r.headers["Pragma"] = "no-cache" + r.headers["Expires"] = "0" + r.headers['Cache-Control'] = 'public, max-age=0' + return r + +def strftime(): + return time.strftime('%H:%m:%S') + +@app.route('/') +def index(): + return render_template('index.html') + +@sock.route('/time') +def route_time(sock): + while True: + sock.send(strftime()) + time.sleep(1) + +@sock.route('/logs') +def route_logs(sock): + id = 0; + + sock.send(json.dumps({ + 'id': 'Id', + 'time': 'Time', + 'value': 'Value', + 'message': 'Message' + })) + + with open('/usr/share/wordlists/wordlist') as wl: + lines = wl.readlines() + + while True: + time.sleep(random() * 3) + + message = '{} {} {}'.format(choice(lines), choice(lines), choice(lines)) + + sock.send(json.dumps({ + 'id': id, + 'time': str(strftime()), + 'value': int(random() * 100), + 'message': message + })) + + id = id + 1 + +@sock.route('/ticker') +def route_ticker(sock): + tick = 0; + while True: + sock.send(tick); + tick = tick + 1 + time.sleep(0.5) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1d2d004 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.0 +flask-sock==0.7.0 diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..699bc11 --- /dev/null +++ b/static/index.js @@ -0,0 +1,219 @@ +class WSElement extends HTMLElement { + static #sockets = {}; + #delay = 1000; + + constructor() { + super(); + } + + connectedCallback() { + // track `src` and `autoreconnect` attributes for later use + // `autoreconnect` defaults to `true`, unless explicitly set to `false` + this.src = this.getAttribute('src'); + this.autoreconnect = !this.hasAttribute('autoreconnect') || this.getAttribute('autoreconnect').toLowerCase() !== 'false'; + + this.#connect_socket(false); + } + + #connect_socket(recreate) { + var ws; + + // `recreate` is used to handle reconnects, removeing the old websocket object + // and recreating a new one + if(recreate && WSElement.#sockets[this.src]) { + delete WSElement.#sockets[this.src]; + } + + // a single WebSocket can be reused for multiple elements + // check to see if one exists for this `src` else create + // a new one. + if(!(this.src in WSElement.#sockets)) { + ws = WSElement.#newSocket(this.src); + WSElement.#sockets[this.src] = ws; + } else { + ws = WSElement.#sockets[this.src]; + } + + // setup event handlers for this WebSocket/Elemnent pair + ws.addEventListener('error', this.#on_ws_error.bind(this)); + ws.addEventListener('message', this.#on_ws_message.bind(this)); + ws.addEventListener('open', this.#on_ws_open.bind(this)); + ws.addEventListener('close', this.#on_ws_close.bind(this)); + } + + #on_ws_open(sock) { + // set connected attribute for use in CSS + // ``` + // ws-element[connected] { + // color: green; + // } + // ws-element:not(connected) { + // color: red; + // } + // ``` + this.setAttribute('connected', ''); + + // call this.open if defined by subclass + if('open' in this) { + this.open(sock); + } + } + + #on_ws_message(sock) { + // call this.message, which is always defined + this.message(sock.data); + } + + #on_ws_close(sock) { + this.removeAttribute('connected'); + + // clean up event handlers for this element + sock.target.removeEventListener('error', this.#on_ws_error.bind(this)); + sock.target.removeEventListener('message', this.#on_ws_message.bind(this)); + sock.target.removeEventListener('open', this.#on_ws_open.bind(this)); + sock.target.removeEventListener('close', this.#on_ws_close.bind(this)); + + // call this.close if subclass has defined it + if('close' in this) { + this.close(sock); + } + + // handle autoreconnect and delay throttling + if(this.autoreconnect) { + setTimeout(this.#connect_socket.call(this, true), this.#delay); + if(this.#delay < 10000) { + this.#delay += 1000; + } + } + } + + #on_ws_error(sock) { + // call this.error if subclass has defined it + if('error' in this) { + this.error(sock); + } + } + + // default behavior on this.message is just update innerText + message(data) { + this.innerText = data; + } + + // static method for creating new sockets, which are tracked + // on the static propert WSElement.#sockets + static #newSocket(src) { + var s = window.location.protocol.slice(-2) === 'p:' ? '' : 's'; + var host = window.location.host; + var path = src[0] === '/' ? src.slice(1) : src; + var wsurl = `ws${s}://${host}/${path}`; + + return new WebSocket(wsurl); + } +} + +// Example subclass to display a basic 'Time: 00:00:00' +class WSTime extends WSElement { + connectedCallback() { + super.connectedCallback(); + + // insert subelements for the label (Time:) and the value; + this.insertAdjacentHTML('beforeend', ''); + // track the value element for updating later + this.value = this.querySelector('span.time-value'); + } + + // override this.message to update the innerText of the value span element + message(data) { + this.value.innerText = data; + } +} + +// Example subclass to parse arbitrary json and build a table +// with dynamically controlled limit +class WSTable extends WSElement { + // observe limit to dynamically update + static observedAttributes = [ 'limit' ]; + + connectedCallback() { + super.connectedCallback(); + + // ignoreNext is to handle duplicate headers being sent on reconnect + this.ignoreNext = false; + + // insert and track table element + this.insertAdjacentHTML('beforeend', '
'); + this.table = this.querySelector('table'); + + // track limit attribute, defaulting to no limit + if(this.hasAttribute('limit')) { + this.limit = parseInt(this.getAttribute('limit')) || -1; + } + } + + attributeChangedCallback(name, oldValue, newValue) { + // update the tracked limit, defaulting to none if no value provided + if(oldValue === null) { + this.limit = parseInt(newValue) || -1; + } + } + + message(data) { + // ignore one message, because the demo server is a bit dumb + if(this.ignoreNext) { + this.ignoreNext = false; + return; + } + + data = JSON.parse(data); + + // with autoheader set the first message will create a header row instead of body + if(this.hasAttribute('autoheader') && !('header' in this)) { + // createElement works just as well as insertAdjacentHTML for single, to be tracked elements + this.thead = document.createElement('thead'); + this.header = document.createElement('tr'); + + this.thead.appendChild(this.header); + this.table.appendChild(this.thead); + + // add a `th` for each key/value pair in the json data + for(let k in data) { + this.header.insertAdjacentHTML('beforeend', `${data[k]}`); + } + + // work is done, this block will be ignored in future messages + return; + } + + // create the body if it doesn't yet exists + if(!('tbody' in this)) { + this.tbody = document.createElement('tbody'); + this.table.appendChild(this.tbody); + } + + // create TD for each key/value pair in message data + var inner = ''; + for(let k in data) { + inner += `${data[k]}`; + } + // instead the above TDs into a new TR + this.tbody.insertAdjacentHTML('beforeend', `${inner}`); + + // check limits are enforced + if(this.limit > -1 && this.tbody.childElementCount > this.limit) { + while(this.tbody.childElementCount > this.limit) { + this.tbody.removeChild(this.tbody.childNodes[0]); + } + } + } + + // ignore the next message if we're disconnected, + // demo server always sends headers on connect + close(sock) { + this.ignoreNext = true; + } +} + +// define custom elements +customElements.define('ws-element', WSElement); +customElements.define('ws-time', WSTime); +customElements.define('ws-table', WSTable); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ac5f2be --- /dev/null +++ b/templates/index.html @@ -0,0 +1,41 @@ + + + + + + + + +
+
+ +
+ +
+ +
+
+ +