switched api to fastapi, simplified setup

dev
Anton Franzluebbers 2022-06-22 18:37:14 -04:00
parent 01c2bf861d
commit 1f292187f9
40 changed files with 309 additions and 1295 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ gunicorn.log
config_mysql.py config_mysql.py
hugo.exe hugo.exe
spectre/ spectre/
velconnect_backup.sql

26
README.md Normal file
View File

@ -0,0 +1,26 @@
## VELConnect API Setup
1. `cd velconnect`
2. Create pip env: `python3 -m venv env`
3. Activate the env `. env/bin/activate`
4. Install packages `pip install -r requirements.txt`
5. Run `./run_server.sh`
- Or set up systemctl service:
```ini
[Unit]
Description=VelNet Logging
Requires=network.target
After=network.target
[Service]
User=ubuntu
Group=ubuntu
Environment="PATH=/home/ubuntu/VEL-Connect/velconnect/env/bin"
WorkingDirectory=/home/ubuntu/VEL-Connect/velconnect
ExecStart=/home/ubuntu/VEL-Connect/velconnect/env/bin/uvicorn --port 8005 main:app
[Install]
WantedBy=multi-user.target
```
- Enter the above in `/etc/systemd/system/velconnect.service`
- `sudo systemctl enable velconnect.service`
- `sudo systemctl start velconnect.service`

View File

@ -1,6 +0,0 @@
flask
simplejson
Flask-Limiter
pymysql
gunicorn
flask-cors

View File

@ -1,84 +0,0 @@
from flask import Flask, jsonify, make_response, request
from flask_cors import CORS
from velconnect.auth import limiter
from velconnect.logger import logger
from time import strftime
import traceback
def create_app():
app = Flask(
__name__,
instance_relative_config=False,
)
app.config.from_pyfile('config.py')
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
limiter.init_app(app)
from .routes import api
app.register_blueprint(api.bp, url_prefix='/api')
from .routes import website
app.register_blueprint(website.bp)
# Error handlers
app.register_error_handler(404, resource_not_found)
app.register_error_handler(401, noapikey_handler)
app.register_error_handler(429, ratelimit_handler)
app.register_error_handler(Exception, exceptions)
app.after_request(after_request)
return app
# @app.after_request
def after_request(response):
""" Logging after every request. """
# This avoids the duplication of registry in the log,
# since that 500 is already logged via @app.errorhandler.
if response.status_code != 500:
ts = strftime('[%Y-%b-%d %H:%M]')
logger.error('%s %s %s %s %s %s',
ts,
request.remote_addr,
request.method,
request.scheme,
request.full_path,
response.status)
return response
# @app.errorhandler(Exception)
def exceptions(e):
""" Logging after every Exception. """
ts = strftime('[%Y-%b-%d %H:%M]')
tb = traceback.format_exc()
logger.error('%s %s %s %s %s 5xx INTERNAL SERVER ERROR\n%s',
ts,
request.remote_addr,
request.method,
request.scheme,
request.full_path,
tb)
return "SERVER ERROR", 500
# @app.errorhandler(429)
def ratelimit_handler(e):
return make_response(
jsonify(error="ratelimit exceeded %s" % e.description), 429
)
def resource_not_found(e):
return jsonify(error=str(e)), 404
# @app.errorhandler(401)
def noapikey_handler(e):
return make_response(
jsonify(error="not authorized %s" % e.description), 401
)

221
velconnect/api.py Normal file
View File

@ -0,0 +1,221 @@
from fastapi import APIRouter
from fastapi import Query
from typing import Optional
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi import FastAPI, Body, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from db import query, insert
from pydantic import BaseModel
from typing import Union
# APIRouter creates path operations for user module
router = APIRouter(
prefix="/api",
tags=["API"],
responses={404: {"description": "Not found"}},
)
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token") # use token authentication
def api_key_auth(api_key: str = Depends(oauth2_scheme)):
return True
values = query(
"SELECT * FROM `APIKey` WHERE `key`=%(key)s;", {'key': api_key})
if not (len(values) > 0 and values['auth_level'] < 0):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Forbidden"
)
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
async def read_root():
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 charecters -->
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
<title>API Reference</title>
</head>
<body>
<rapi-doc
render-style = "read"
primary-color = "#bc1f2d"
show-header = "false"
show-info = "true"
spec-url = "/openapi.json"
default-schema-tab = 'example'
>
<div slot="nav-logo" style="display: flex; align-items: center; justify-content: center;">
<img src = "http://velconnect.ugavel.com/static/favicons/android-chrome-256x256.png" style="width:10em; margin: auto;" />
</div>
</rapi-doc>
</body>
</html>
"""
@router.get('/get_all_headsets')
def get_all_headsets():
"""Returns a list of all headsets and details associated with them."""
values = query("SELECT * FROM `Headset`;")
return values
@router.get('/pair_headset/{pairing_code}')
def pair_headset(pairing_code: str):
values = query("SELECT * FROM `Headset` WHERE `pairing_code`=%(pairing_code)s;",
{'pairing_code': pairing_code})
if len(values) == 1:
print(values[0]['hw_id'])
return {'hw_id': values[0]['hw_id']}
return {'error': 'Not found'}, 400
class UpdatePairingCode(BaseModel):
hw_id: str
pairing_code: str
@router.post('/update_pairing_code')
def update_paring_code(data: UpdatePairingCode):
"""This also creates a headset if it doesn't exist"""
if 'hw_id' not in data:
return 'Must supply hw_id', 400
if 'pairing_code' not in data:
return 'Must supply pairing_code', 400
insert("""
INSERT INTO `Headset`(
`hw_id`,
`pairing_code`,
`last_used`
) VALUES (
%(hw_id)s,
%(pairing_code)s,
CURRENT_TIMESTAMP
)
ON DUPLICATE KEY UPDATE
pairing_code=%(pairing_code)s,
last_used=CURRENT_TIMESTAMP;
""", data)
return {'success': True}
@router.get('/get_state/{hw_id}')
def get_headset_details(hw_id: str):
data = get_headset_details_db(hw_id)
if data is None:
return {'error': "Can't find headset with that id."}
else:
return data
def get_headset_details_db(hw_id):
headsets = query("""
SELECT * FROM `Headset` WHERE `hw_id`=%(hw_id)s;
""", {'hw_id': hw_id})
if len(headsets) == 0:
return None
room = get_room_details_db(headsets[0]['current_room'])
return {'user': headsets[0], 'room': room}
@router.post('/set_headset_details/{hw_id}')
def set_headset_details_generic(hw_id: str, data: dict):
print(data)
allowed_keys = [
'current_room',
'pairing_code',
'user_color',
'user_name',
'avatar_url',
'user_details',
]
for key in data:
if key in allowed_keys:
if key == 'current_room':
create_room(data['current_room'])
insert("UPDATE `Headset` SET " + key +
"=%(value)s, modified_by=%(sender_id)s WHERE `hw_id`=%(hw_id)s;", {'value': data[key], 'hw_id': hw_id, 'sender_id': data['sender_id']})
return {'success': True}
@router.post('/set_room_details/{room_id}')
def set_room_details_generic(room_id: str, data: dict):
print(data)
allowed_keys = [
'modified_by',
'whitelist',
'tv_url',
'carpet_color',
'room_details',
]
for key in data:
if key in allowed_keys:
insert("UPDATE `Room` SET " + key +
"=%(value)s, modified_by=%(sender_id)s WHERE `room_id`=%(room_id)s;", {'value': data[key], 'room_id': room_id, 'sender_id': data['sender_id']})
return {'success': True}
@router.get('/get_room_details/{room_id}')
def get_room_details(room_id: str):
return get_room_details_db(room_id)
def get_room_details_db(room_id):
values = query("""
SELECT * FROM `Room` WHERE room_id=%(room_id)s;
""", {'room_id': room_id})
if len(values) == 1:
return values[0]
else:
return None
def create_room(room_id):
insert("""
INSERT IGNORE INTO `Room`(room_id)
VALUES(
%(room_id)s
);
""", {'room_id': room_id})
return {'room_id': room_id}
@router.post('/update_user_count', tags=["User Count"])
def update_user_count(data: dict):
insert("""
REPLACE INTO `UserCount`
VALUES(
CURRENT_TIMESTAMP,
%(hw_id)s,
%(room_id)s,
%(total_users)s,
%(room_users)s,
%(version)s,
%(platform)s
);
""", data)
return {'success': True}
@router.get('/get_user_count', tags=["User Count"])
def get_user_count(hours: float = 24):
values = query("""
SELECT timestamp, total_users
FROM `UserCount`
WHERE TIMESTAMP > DATE_SUB(NOW(), INTERVAL """ + str(hours) + """ HOUR);
""")
return values

View File

@ -1,57 +0,0 @@
from functools import wraps
import inspect
from flask import abort, request, make_response, jsonify
from velconnect.db import connectToDB
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
def require_api_key(level):
def decorator(view_function):
def wrapper(*args, **kwargs):
return view_function(*args, **kwargs)
key = request.headers.get('x-api-key')
conn, curr = connectToDB()
query = """
SELECT * FROM `APIKey` WHERE `key`=%(key)s;
"""
curr.execute(query, {'key': key})
values = [dict(row) for row in curr.fetchall()]
if len(values) > 0 and values['auth_level'] < level:
return view_function(*args, **kwargs)
else:
abort(401)
wrapper.__name__ = view_function.__name__
return wrapper
return decorator
def required_args(f):
@wraps(f)
def decorated_function(*args, **kwargs):
""" Decorator that makes sure the view arguments are in the request args, otherwise 400 error """
sig = inspect.signature(f)
data = request.args
for arg in sig.parameters.values():
# Check if the argument is passed from the url
if arg.name in kwargs:
continue
# check if the argument is in the json data
if data and data.get(arg.name) is not None:
kwargs[arg.name] = data.get(arg.name)
# else check if it has been given a default
elif arg.default is not arg.empty:
kwargs[arg.name] = arg.default
missing_args = [arg for arg in sig.parameters.keys()
if arg not in kwargs.keys()]
if missing_args:
return 'Did not receive args for: {}'.format(', '.join(missing_args)), 400
return f(*args, **kwargs)
return decorated_function

View File

@ -1,3 +0,0 @@
# Flask-Caching related configs
CACHE_TYPE = "SimpleCache"
CACHE_DEFAULT_TIMEOUT = 3600

View File

@ -1,7 +1,7 @@
# from config import * # from config import *
import pymysql import pymysql
from pymysql import converters from pymysql import converters
from velconnect.config_mysql import * from config_mysql import *
def connectToDB(): def connectToDB():
@ -19,3 +19,29 @@ def connectToDB():
curr = conn.cursor() curr = conn.cursor()
return conn, curr return conn, curr
def query(query: str, data: dict = None) -> list:
try:
conn, curr = connectToDB()
curr.execute(query, data)
values = [dict(row) for row in curr.fetchall()]
curr.close()
return values
except Exception:
print(curr._last_executed)
curr.close()
raise
def insert(query: str, data: dict = None) -> bool:
try:
conn, curr = connectToDB()
curr.execute(query, data)
conn.commit()
curr.close()
return True
except Exception:
print(curr._last_executed)
curr.close()
raise

View File

@ -1,7 +0,0 @@
import logging
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler('app.log', maxBytes=100000000, backupCount=1500)
logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)
logger.addHandler(handler)

29
velconnect/main.py Normal file
View File

@ -0,0 +1,29 @@
from imp import reload
import uvicorn
from fastapi.middleware.cors import CORSMiddleware
from fastapi import FastAPI
from api import router as api_router
app = FastAPI()
origins = [
"http://velconnect.ugavel.com",
"https://velconnect.ugavel.com",
"http://localhost",
"http://localhost:8080",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router)
if __name__ == '__main__':
uvicorn.run("main:app", host='127.0.0.1', port=8005,
log_level="info", reload=True)
print("running")

View File

@ -0,0 +1,4 @@
fastapi
autopep8
uvicorn
pymysql

View File

@ -1,274 +0,0 @@
from flask.helpers import send_from_directory
from velconnect.auth import require_api_key
from velconnect.db import connectToDB
from velconnect.logger import logger
from flask import Blueprint, request, jsonify, render_template, url_for
import time
import simplejson as json
from random import random
bp = Blueprint('api', __name__)
@bp.route('/', methods=['GET'])
def index():
return render_template('api.html')
@bp.route('/api_spec.json', methods=['GET'])
@require_api_key(0)
def api_spec():
response = send_from_directory('static', 'api_spec.json')
response.headers.add('Access-Control-Allow-Origin', '*')
return response
@bp.route('/get_all_headsets', methods=['GET'])
@require_api_key(0)
def get_all_headsets():
conn, curr = connectToDB()
query = """
SELECT * FROM `Headset`;
"""
curr.execute(query, None)
values = [dict(row) for row in curr.fetchall()]
curr.close()
response = jsonify(values)
response.headers.add('Access-Control-Allow-Origin', '*')
return response
@bp.route('/pair_headset/<pairing_code>', methods=['GET'])
@require_api_key(0)
def pair_headset(pairing_code):
conn, curr = connectToDB()
query = """
SELECT * FROM `Headset` WHERE `pairing_code`=%(pairing_code)s;
"""
curr.execute(query, {'pairing_code': pairing_code})
values = [dict(row) for row in curr.fetchall()]
curr.close()
if len(values) == 1:
print(values[0]['hw_id'])
response = jsonify({'hw_id': values[0]['hw_id']})
response.headers.add('Access-Control-Allow-Origin', '*')
return response
return 'Not found', 400
# This also creates a headset if it doesn't exist
@bp.route('/update_pairing_code', methods=['POST'])
@require_api_key(0)
def update_paring_code():
data = request.json
if 'hw_id' not in data:
return 'Must supply hw_id', 400
if 'pairing_code' not in data:
return 'Must supply pairing_code', 400
conn, curr = connectToDB()
query = """
INSERT INTO `Headset`(
`hw_id`,
`pairing_code`,
`last_used`
) VALUES (
%(hw_id)s,
%(pairing_code)s,
CURRENT_TIMESTAMP
)
ON DUPLICATE KEY UPDATE
pairing_code=%(pairing_code)s,
last_used=CURRENT_TIMESTAMP;
"""
curr.execute(query, data)
conn.commit()
curr.close()
response = jsonify({'success': True})
response.headers.add('Access-Control-Allow-Origin', '*')
return response
@bp.route('/get_state/<hw_id>', methods=['GET'])
@require_api_key(10)
def get_headset_details(hw_id):
data = get_headset_details_db(hw_id)
if data is None:
response = jsonify({'error': "Can't find headset with that id."})
response.headers.add('Access-Control-Allow-Origin', '*')
return response
else:
response = jsonify(data)
response.headers.add('Access-Control-Allow-Origin', '*')
return response
def get_headset_details_db(hw_id):
conn, curr = connectToDB()
query = """
SELECT * FROM `Headset` WHERE `hw_id`=%(hw_id)s;
"""
curr.execute(query, {'hw_id': hw_id})
headsets = [dict(row) for row in curr.fetchall()]
curr.close()
if len(headsets) == 0:
return None
room = get_room_details_db(headsets[0]['current_room'])
return {'user': headsets[0], 'room': room}
@bp.route('/set_headset_details/<hw_id>', methods=['POST'])
@require_api_key(10)
def set_headset_details_generic(hw_id):
data = request.json
logger.error(data)
conn, curr = connectToDB()
allowed_keys = [
'current_room',
'pairing_code',
'user_color',
'user_name',
'avatar_url',
'user_details',
]
try:
for key in data:
if key in allowed_keys:
if key == 'current_room':
create_room(data['current_room'])
query = "UPDATE `Headset` SET " + key + \
"=%(value)s, modified_by=%(sender_id)s WHERE `hw_id`=%(hw_id)s;"
curr.execute(
query, {'value': data[key], 'hw_id': hw_id, 'sender_id': data['sender_id']})
conn.commit()
except Exception as e:
logger.error(curr._last_executed)
curr.close()
logger.error(e)
return 'Error', 400
curr.close()
response = jsonify({'success': True})
response.headers.add('Access-Control-Allow-Origin', '*')
return response
@bp.route('/set_room_details/<room_id>', methods=['POST'])
@require_api_key(10)
def set_room_details_generic(room_id):
data = request.json
logger.error(data)
conn, curr = connectToDB()
allowed_keys = [
'modified_by',
'whitelist',
'tv_url',
'carpet_color',
'room_details',
]
try:
for key in data:
if key in allowed_keys:
query = "UPDATE `Room` SET " + key + \
"=%(value)s, modified_by=%(sender_id)s WHERE `room_id`=%(room_id)s;"
curr.execute(
query, {'value': data[key], 'room_id': room_id, 'sender_id': data['sender_id']})
conn.commit()
except Exception as e:
logger.error(curr._last_executed)
curr.close()
logger.error(e)
return 'Error', 400
curr.close()
response = jsonify({'success': True})
response.headers.add('Access-Control-Allow-Origin', '*')
return response
@bp.route('/get_room_details/<room_id>', methods=['GET'])
@require_api_key(10)
def get_room_details(room_id):
response = jsonify(get_room_details_db(room_id))
response.headers.add('Access-Control-Allow-Origin', '*')
return response
def get_room_details_db(room_id):
conn, curr = connectToDB()
query = """
SELECT * FROM `Room` WHERE room_id=%(room_id)s;
"""
curr.execute(query, {'room_id': room_id})
values = [dict(row) for row in curr.fetchall()]
curr.close()
if len(values) == 1:
return values[0]
else:
return None
def create_room(room_id):
conn, curr = connectToDB()
query = """
INSERT IGNORE INTO `Room`(room_id)
VALUES(
%(room_id)s
);
"""
curr.execute(query, {'room_id': room_id})
conn.commit()
curr.close()
return {'room_id': room_id}
@bp.route('/update_user_count', methods=['POST'])
@require_api_key(10)
def update_user_count():
conn, curr = connectToDB()
query = """
INSERT INTO `UserCount`
VALUES(
CURRENT_TIMESTAMP,
%(hw_id)s,
%(room_id)s,
%(total_users)s,
%(room_users)s,
%(version)s,
%(platform)s
);
"""
data = request.json
curr.execute(query, data)
conn.commit()
curr.close()
response = jsonify({'success': True})
response.headers.add('Access-Control-Allow-Origin', '*')
return response
@bp.route('/get_user_count', methods=['GET'])
def get_user_count():
hours = request.args.get('hours', 24)
conn, curr = connectToDB()
query = """
SELECT timestamp, total_users
FROM `UserCount`
WHERE TIMESTAMP > DATE_SUB(NOW(), INTERVAL """ + str(hours) + """ HOUR);
"""
data = request.json
curr.execute(query, data)
values = [dict(row) for row in curr.fetchall()]
curr.close()
response = jsonify(values)
response.headers.add('Access-Control-Allow-Origin', '*')
return response

View File

@ -1,27 +0,0 @@
from velconnect.db import connectToDB
from flask import Blueprint, request, jsonify, render_template
from velconnect.logger import logger
import time
import simplejson as json
bp = Blueprint('website', __name__, template_folder='templates')
@bp.route('/', methods=['GET'])
def index():
return render_template('index.html')
@bp.route('/pair', methods=['GET'])
def pair():
return render_template('pair.html')
@bp.route('/success', methods=['GET'])
def success():
return render_template('success.html')
@bp.route('/failure', methods=['GET'])
def failure():
return render_template('failure.html')

View File

@ -1,165 +0,0 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0",
"title": "VEL Connect API",
"description": "This API provides an interface with the database for pairing and controlling headsets while within a VEL app."
},
"servers": [
{
"url": "http://velconnect.ugavel.com/api/"
},
{
"url": "http://localhost:5000/api/"
}
],
"tags": [
{
"name": "Read"
},
{
"name": "Write"
}
],
"paths": {
"/get_all_headsets": {
"get": {
"summary": "Get all headset data",
"description" : "Gets a list of all headset data that has been stored",
"tags": ["Read"],
"parameters": [
]
}
},
"/pair_headset/{pairing_code}": {
"get": {
"summary": "Pair a headset by code",
"description" : "Tries to pair to a headset with a supplied pairing code. Returns the hw_id if successfully paired. Used by the website for pairing.",
"tags": ["Write"],
"parameters": [
{
"name": "pairing_code",
"example": "1234",
"in": "path",
"description": "Pairing Code",
"required": true,
"schema": {
"type": "integer"
}
}
]
}
},
"/update_pairing_code": {
"post": {
"summary": "Update Pairing Code",
"description": "This is called by the VR application on login or when the pairing code is reset. If this is the first time this headset is seen, it also creates this headset in the database. A JSON body must be submitted with hw_id and pairing_code defined.",
"tags": ["Write"],
"parameters": [
]
}
},
"/get_headset_details/{hw_id}": {
"get": {
"summary": "Get Headset Details",
"description" : "Gets the info associated with a specific hardware id.",
"tags": ["Read"],
"parameters": [
{
"name": "hw_id",
"example": "123456789",
"in": "path",
"description": "Hardware id of the device",
"required": true,
"schema": {
"type": "string"
}
}
]
}
},
"/get_room_details/{room_id}": {
"get": {
"summary": "Get Room Details",
"description" : "Gets the info associated with a specific room id. Used by the website to show initial data, and polled often by the headset.",
"tags": ["Read"],
"parameters": [
{
"name": "room_id",
"example": "1234",
"in": "path",
"description": "Room id",
"required": true,
"schema": {
"type": "string"
}
}
]
}
},
"/update_room/{room_id}": {
"post": {
"summary": "Update Room Details",
"description" : "Sets the info associated with a specific room id. The room id is specified in the url and the data is specified in the JSON body. Used by both the website and headset.",
"tags": ["Write"],
"parameters": [
{
"name": "room_id",
"example": "1234",
"in": "path",
"description": "Room id",
"required": true,
"schema": {
"type": "string"
}
}
]
}
},
"/update_user_count": {
"post": {
"summary": "Update User Count",
"description" : "Updates the current user count for a room and globally.",
"tags": ["User Count"],
"parameters": [
]
}
},
"/get_user_count": {
"get": {
"summary": "Get User Count",
"description" : "Gets a list of the recent user counts.",
"tags": ["User Count"],
"parameters": [
{
"name": "hours",
"example": "24",
"in": "path",
"description": "Number of hours to get user counts for",
"required": false,
"schema": {
"type": "string"
}
}
]
}
}
},
"components": {
"schemas": {
},
"securitySchemes": {
"api_key": {
"type": "apiKey",
"name": "x-api-key",
"in": "query"
}
}
},
"security": [
{
"BasicAuth": []
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#b91d47</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,20 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="300.000000pt" height="300.000000pt" viewBox="0 0 300.000000 300.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,300.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M605 1500 l600 -600 97 0 98 0 0 600 0 600 -100 0 -100 0 0 -452 0
-453 -453 453 -452 452 -145 0 -145 0 600 -600z"/>
<path d="M1500 2000 l0 -100 300 0 300 0 0 100 0 100 -300 0 -300 0 0 -100z"/>
<path d="M2200 1500 l0 -600 398 2 397 3 3 98 3 97 -301 0 -300 0 -2 498 -3
497 -97 3 -98 3 0 -601z"/>
<path d="M1500 1500 l0 -100 300 0 300 0 0 100 0 100 -300 0 -300 0 0 -100z"/>
<path d="M1500 1000 l0 -100 300 0 300 0 0 100 0 100 -300 0 -300 0 0 -100z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 977 B

View File

@ -1,19 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,155 +0,0 @@
function httpGetAsync(theUrl, callback, failCallback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status == 200) {
callback(xmlHttp.responseText);
} else {
failCallback(xmlHttp.status);
}
}
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
}
function httpPostAsync(theUrl, data, callback, failCallback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status == 200) {
callback(xmlHttp.responseText);
} else {
failCallback(xmlHttp.status);
}
}
}
xmlHttp.open("POST", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader('Content-type', 'application/json');
xmlHttp.send(JSON.stringify(data));
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function writeClass(className, data) {
if (data == undefined || data == null || data.toString() == 'undefined') {
data = "";
}
let elements = document.getElementsByClassName(className);
Array.from(elements).forEach(e => {
e.innerHTML = data;
});
}
function writeId(idName, data) {
if (data == undefined || data == null || data.toString() == 'undefined') {
data = "";
}
document.getElementById(idName).innerHTML = data;
}
function writeValue(className, data) {
if (data == undefined || data == null || data.toString() == 'undefined') {
data = "";
}
let elements = document.getElementsByClassName(className);
Array.from(elements).forEach(e => {
e.value = data;
});
}
function writeSrc(className, data) {
if (data == undefined || data == null || data.toString() == 'undefined') {
data = "";
}
let elements = document.getElementsByClassName(className);
Array.from(elements).forEach(e => {
e.src = data;
});
}
function timeSince(date) {
let seconds = Math.floor((new Date() - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
}
function timeSinceString(date) {
date = Date.parse(date);
let seconds = Math.floor((new Date() - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
}

View File

@ -1,25 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 charecters -->
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
<title>API Reference</title>
</head>
<body>
<rapi-doc
render-style = "read"
primary-color = "#bc1f2d"
show-header = "false"
show-info = "true"
spec-url = "{% include 'api_url.html' %}/api/api_spec.json"
default-schema-tab = 'example'
>
<div slot="nav-logo" style="display: flex; align-items: center; justify-content: center;">
<img src = "/static/favicons/android-chrome-256x256.png" style="width:10em; margin: auto;" />
</div>
</rapi-doc>
</body>
</html>

View File

@ -1,23 +0,0 @@
{% extends 'single.html' %}
{% block content %}
<style>
body {
background-color: #333;
font-size: 1.2em;
}
.centered {
margin: 8em auto;
width: max-content;
font-family: arial, sans-serif;
color: #ddd;
}
</style>
<div class="centered">
🤮 FAIL 🤡
</div>
{% endblock %}

View File

@ -1,264 +0,0 @@
{% extends 'single.html' %}
{% block content %}
<style>
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
.centered {
margin: auto;
}
</style>
<div id="loading"><br><br>
<div class="loading loading-lg"></div>
</div>
<div id="failure" style="display: none;"><br><br><br>☹️</div>
<div id="headset_details" style="display: none;">
<div class="panel card">
<div class="card-image">
<img class="img-responsive" src="/static/img/mini_landscape.png" alt="conVRged Logo">
</div>
<div class="panel-header text-center">
<!-- <img src="/static/favicons/android-chrome-192x192.png" alt="Avatar" style="height:5em;" /> -->
<div class="panel-title h5 mt-10">Headset Info</div>
<code class="panel-subtitle hw_id">---</code>
<br>
<br>
<a href="/pair"><button class="btn btn-primary btn-lg tooltip tooltip-bottom" data-tooltip="Enter a new pairing code">Pair New</button></a>
</div>
<div class="panel-body">
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Current Room</div>
<input class="btn current_room" type="text" id="current_room" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_room_id"
data-tooltip="Set Room ID">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">First Seen</div>
<div class="tile-subtitle date_created">---</div>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Last Login</div>
<div class="tile-subtitle last_used">---</div>
</div>
</div>
<br>
<div class="divider text-center" data-content="User Settings"></div>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">User Name</div>
<input class="btn user_name" type="text" id="user_name" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_user_name"
data-tooltip="Update in VR">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Avatar URL</div>
<div class="tile-subtitle"><a href="https://convrged.readyplayer.me" target="blank">Create New Avatar</a></div>
<input class="btn avatar_url" type="text" id="avatar_url" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_avatar_url"
data-tooltip="Update in VR">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">User Color</div>
<input class="btn user_color coloris" type="text" id="user_color" placeholder="#ffffff">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_user_color"
data-tooltip="Set User Color">Set</button>
</div>
</div>
<br>
<div class="divider text-center" data-content="Room Settings"></div>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">TV URL</div>
<input class="btn tv_url" type="text" id="tv_url" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_tv_url"
data-tooltip="Update in VR">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Carpet Color</div>
<input class="btn carpet_color coloris" type="text" id="carpet_color" placeholder="#ffffff">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_carpet_color"
data-tooltip="Set Carpet Color">Set</button>
</div>
</div>
<br>
<br>
</div>
</div>
</div>
<script type="text/javascript" src="/static/js/coloris.min.js"></script>
<script>
let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code');
let loading = document.getElementById('loading');
let enter_pairing_id = document.getElementById('enter_pairing_id');
let headset_details = document.getElementById('headset_details');
let hw_id_field = document.getElementById('hw_id');
let failure = document.getElementById('failure');
let current_room = document.getElementById('current_room');
let set_room_id = document.getElementById('set_room_id');
let user_color = document.getElementById('user_color');
let carpet_color = document.getElementById('carpet_color');
// check cookie
let hw_id = getCookie('hw_id');
if (hw_id != "") {
httpGetAsync('{% include "api_url.html" %}/api/get_state/' + hw_id, (resp) => {
console.log(resp);
let respData = JSON.parse(resp);
writeClass('hw_id', respData['user']['hw_id']);
writeValue('current_room', respData['user']['current_room']);
writeClass('date_created', respData['user']['date_created'] + "<br>" + timeSinceString(respData['user']['date_created']) + " ago");
writeClass('last_used', respData['user']['last_used'] + "<br>" + timeSinceString(respData['user']['last_used']) + " ago");
writeValue('user_color', respData['user']['user_color']);
user_color.parentElement.style.color = respData['user']['user_color'];
writeValue('user_name', respData['user']['user_name']);
writeValue('avatar_url', respData['user']['avatar_url']);
if (respData['room']) {
writeValue('tv_url', respData['room']['tv_url']);
writeValue('carpet_color', respData['room']['carpet_color']);
carpet_color.parentElement.style.color = respData['room']['carpet_color'];
}
Coloris({
el: '.coloris',
swatches: [
'#264653',
'#2a9d8f',
'#e9c46a',
'#f4a261',
'#e76f51',
'#d62828',
'#023e8a',
'#0077b6',
'#0096c7',
'#00b4d8',
'#48cae4',
]
});
loading.style.display = "none";
headset_details.style.display = "block";
}, (status) => {
loading.style.display = "none";
failure.style.display = "block";
window.location.href = "/pair";
});
function setUserData(data) {
data["sender_id"] = Math.floor(Math.random()*10000000);
httpPostAsync('{% include "api_url.html" %}/api/set_headset_details/' + hw_id,
data,
(resp) => { console.log('success'); },
(status) => { console.log('fail'); }
);
}
function setRoomData(data) {
data["sender_id"] = Math.floor(Math.random()*10000000);
httpPostAsync('{% include "api_url.html" %}/api/set_room_details/' + current_room.value,
data,
(resp) => { console.log('success'); },
(status) => { console.log('fail'); }
);
}
set_room_id.addEventListener('click', () => {
setUserData({ "current_room": current_room.value });
});
document.getElementById('set_user_color').addEventListener('click', () => {
setUserData({ "user_color": document.getElementById('user_color').value });
});
document.getElementById('set_user_name').addEventListener('click', () => {
setUserData({ "user_name": document.getElementById('user_name').value });
});
document.getElementById('set_tv_url').addEventListener('click', () => {
setRoomData({ "tv_url": document.getElementById('tv_url').value });
});
document.getElementById('set_carpet_color').addEventListener('click', () => {
setRoomData({ "carpet_color": document.getElementById('carpet_color').value });
});
document.getElementById('set_avatar_url').addEventListener('click', () => {
setUserData({ "avatar_url": document.getElementById('avatar_url').value });
});
} else {
window.location.href = "/pair";
}
Coloris({
el: '.coloris',
swatches: [
'#264653',
'#2a9d8f',
'#e9c46a',
'#f4a261',
'#e76f51',
'#d62828',
'#023e8a',
'#0077b6',
'#0096c7',
'#00b4d8',
'#48cae4',
]
});
</script>
{% endblock %}

View File

@ -1,69 +0,0 @@
{% extends 'single.html' %}
{% block content %}
<style>
:root {
--primary-color: #bc1f2d;
}
#pair_code {
max-width: 4em;
}
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
.centered {
margin: auto;
}
</style>
<div class="card">
<div class="card-image">
<img src="/static/img/pair_code_screenshot.png" class="img-responsive">
</div>
<div class="card-header">
<div class="card-title h5">Enter Pairing Code</div>
<div class="card-subtitle text-gray"></div>
</div>
<div class="card-body">
You can find the code in the bottom left of your menu tablet in conVRged.
</div>
<div class="card-footer centered">
<input class="btn" type="text" id="pair_code" placeholder="0000">
<button class="btn btn-primary" id="submit_pairing_code">Submit</button>
</div>
</div>
</div>
<script>
let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code');
submit_button.addEventListener('click', () => {
httpGetAsync("{% include 'api_url.html' %}/api/pair_headset/" + pair_code_input.value, (resp) => {
console.log(resp);
let respData = JSON.parse(resp);
if (respData['hw_id'] != '') {
setCookie('hw_id', respData['hw_id'], 60);
window.location.href = "/";
}
}, (status) => {
window.location.href = "/failure";
});
});
</script>
{% endblock %}

View File

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VEL Connect</title>
<link rel="stylesheet" type="text/css" href="/static/css/spectre.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/spectre-exp.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/spectre-icons.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/coloris.min.css">
<script src="/static/js/util.js"></script>
<style>
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
.centered {
margin: auto;
}
</style>
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@ -1,23 +0,0 @@
{% extends 'single.html' %}
{% block content %}
<style>
body {
background-color: #333;
font-size: 1.2em;
}
.centered {
margin: 8em auto;
width: max-content;
font-family: arial, sans-serif;
color: #ddd;
}
</style>
<div class="centered">
🎉 SUCCESS 🎉
</div>
{% endblock %}