add velconnect unity package, lots of api changes to make it more generic, added dockerfile

dev
Anton Franzluebbers 2022-07-03 18:51:51 -04:00
parent 4997679aae
commit 4502662a53
21 changed files with 899 additions and 178 deletions

19
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: CI
on:
push:
branches:
- main
jobs:
split-upm:
name: split upm branch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: split upm branch
run: |
git subtree split -P "$PKG_ROOT" -b upm
git push -u origin upm
env:
PKG_ROOT: unity_package

0
unity_package/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0ce56caac2a2b78479e2bebd4f84101f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5b08ac62f2f2a9642bc42da52d43f5de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
{
"name": "VEL-Connect",
"rootNamespace": "VEL-Connect",
"references": [
"GUID:1e55e2c4387020247a1ae212bbcbd381",
"GUID:343deaaf83e0cee4ca978e7df0b80d21",
"GUID:2bafac87e7f4b9b418d9448d219b01ab"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 907e38b516b504b408923c7bed1662f6
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,349 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Networking;
using VelNet;
namespace VELConnect
{
public class VELConnectManager : MonoBehaviour
{
public string velConnectUrl = "http://localhost";
public static VELConnectManager instance;
public class State
{
public class Device
{
public string hw_id;
public string os_info;
public string friendly_name;
public string modified_by;
public string current_app;
public string current_room;
public int pairing_code;
public string date_created;
public string last_modified;
public Dictionary<string, string> data;
/// <summary>
/// Returns the value if it exists, otherwise null
/// </summary>
public string TryGetData(string key)
{
return data?.TryGetValue(key, out string val) == true ? val : null;
}
}
public class RoomState
{
public string error;
public string id;
public string category;
public string date_created;
public string modified_by;
public string last_modified;
public string last_accessed;
public Dictionary<string, string> data;
}
public Device device;
public RoomState room;
}
public State lastState;
public static Action<State> OnInitialState;
public static Action<string, string> OnDeviceFieldChanged;
public static Action<string, string> OnDeviceDataChanged;
public static Action<string, string> OnRoomDataChanged;
public static int PairingCode
{
get
{
Hash128 hash = new Hash128();
hash.Append(DeviceId);
// change once a day
hash.Append(DateTime.UtcNow.DayOfYear);
// between 1000 and 9999 inclusive (any 4 digit number)
return Math.Abs(hash.GetHashCode()) % 9000 + 1000;
}
}
private static string DeviceId
{
get
{
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
// allows running multiple builds on the same computer
// return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath);
return SystemInfo.deviceUniqueIdentifier + "_BUILD";
#else
return SystemInfo.deviceUniqueIdentifier;
#endif
}
}
private void Awake()
{
if (instance != null) Debug.LogError("VELConnectManager instance already exists", this);
instance = this;
}
// Start is called before the first frame update
private void Start()
{
SetDeviceBaseData(new Dictionary<string, object>
{
{ "current_app", Application.productName },
{ "pairing_code", PairingCode }
});
UpdateUserCount();
StartCoroutine(SlowLoop());
VelNetManager.OnJoinedRoom += room =>
{
SetDeviceBaseData(new Dictionary<string, object>
{
{ "current_app", Application.productName },
{ "current_room", room },
});
};
}
private void UpdateUserCount(bool leaving = false)
{
if (!VelNetManager.InRoom) return;
VelNetManager.GetRooms(rooms =>
{
Dictionary<string, object> postData = new Dictionary<string, object>
{
{ "hw_id", DeviceId },
{ "app_id", Application.productName },
{ "room_id", VelNetManager.Room ?? "" },
{ "total_users", rooms.rooms.Sum(r => r.numUsers) - (leaving ? 1 : 0) },
{ "room_users", VelNetManager.PlayerCount - (leaving ? 1 : 0) },
{ "version", Application.version },
{ "platform", SystemInfo.operatingSystem },
};
PostRequestCallback(velConnectUrl + "/api/v2/update_user_count", JsonConvert.SerializeObject(postData));
});
}
private IEnumerator SlowLoop()
{
while (true)
{
try
{
GetRequestCallback(velConnectUrl + "/api/v2/get_state/" + DeviceId, json =>
{
State state = JsonConvert.DeserializeObject<State>(json);
if (state == null) return;
// first load stuff
if (lastState == null)
{
try
{
OnInitialState?.Invoke(state);
}
catch (Exception e)
{
Debug.LogError(e);
}
lastState = state;
return;
}
if (state.device.modified_by != DeviceId)
{
FieldInfo[] fields = state.device.GetType().GetFields();
foreach (FieldInfo fieldInfo in fields)
{
string newValue = fieldInfo.GetValue(state.device) as string;
string oldValue = fieldInfo.GetValue(lastState.device) as string;
if (newValue != oldValue)
{
try
{
OnDeviceFieldChanged?.Invoke(fieldInfo.Name, newValue);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}
foreach (KeyValuePair<string, string> elem in state.device.data)
{
lastState.device.data.TryGetValue(elem.Key, out string oldValue);
if (elem.Value != oldValue)
{
try
{
OnDeviceDataChanged?.Invoke(elem.Key, elem.Value);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}
}
if (state.room.modified_by != DeviceId)
{
foreach (KeyValuePair<string, string> elem in state.room.data)
{
lastState.room.data.TryGetValue(elem.Key, out string oldValue);
if (elem.Value != oldValue)
{
try
{
OnRoomDataChanged?.Invoke(elem.Key, elem.Value);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}
}
lastState = state;
});
}
catch (Exception e)
{
Debug.LogError(e);
// this make sure the coroutine never quits
}
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Sets data on the device keys themselves
/// </summary>
public static void SetDeviceBaseData(Dictionary<string, object> data)
{
data["modified_by"] = DeviceId;
instance.PostRequestCallback(
instance.velConnectUrl + "/api/v2/device/set_data/" + DeviceId,
JsonConvert.SerializeObject(data)
);
}
/// <summary>
/// Sets the 'data' object of the Device table
/// </summary>
public static void SetDeviceData(Dictionary<string, string> data)
{
instance.PostRequestCallback(
instance.velConnectUrl + "/api/v2/device/set_data/" + DeviceId,
JsonConvert.SerializeObject(new Dictionary<string, object>
{
{ "modified_by", DeviceId },
{ "data", data }
})
);
}
public static void SetRoomData(Dictionary<string, string> data)
{
if (!VelNetManager.InRoom)
{
Debug.LogError("Can't set data for a room if you're not in a room.");
return;
}
data["modified_by"] = DeviceId;
instance.PostRequestCallback(
instance.velConnectUrl + "/api/v2/set_data/" + Application.productName + "_" + VelNetManager.Room,
JsonConvert.SerializeObject(data)
);
}
public void GetRequestCallback(string url, Action<string> successCallback = null, Action<string> failureCallback = null)
{
StartCoroutine(GetRequestCallbackCo(url, successCallback, failureCallback));
}
private IEnumerator GetRequestCallbackCo(string url, Action<string> successCallback = null, Action<string> failureCallback = null)
{
using UnityWebRequest webRequest = UnityWebRequest.Get(url);
// Request and wait for the desired page.
yield return webRequest.SendWebRequest();
switch (webRequest.result)
{
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError:
Debug.LogError(url + ": Error: " + webRequest.error);
failureCallback?.Invoke(webRequest.error);
break;
case UnityWebRequest.Result.Success:
successCallback?.Invoke(webRequest.downloadHandler.text);
break;
}
}
public void PostRequestCallback(string url, string postData, Action<string> successCallback = null,
Action<string> failureCallback = null)
{
StartCoroutine(PostRequestCallbackCo(url, postData, successCallback, failureCallback));
}
private static IEnumerator PostRequestCallbackCo(string url, string postData, Action<string> successCallback = null,
Action<string> failureCallback = null)
{
UnityWebRequest webRequest = new UnityWebRequest(url, "POST");
byte[] bodyRaw = Encoding.UTF8.GetBytes(postData);
UploadHandlerRaw uploadHandler = new UploadHandlerRaw(bodyRaw);
webRequest.uploadHandler = uploadHandler;
webRequest.SetRequestHeader("Content-Type", "application/json");
yield return webRequest.SendWebRequest();
switch (webRequest.result)
{
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError:
Debug.LogError(url + ": Error: " + webRequest.error);
failureCallback?.Invoke(webRequest.error);
break;
case UnityWebRequest.Result.Success:
successCallback?.Invoke(webRequest.downloadHandler.text);
break;
}
uploadHandler.Dispose();
webRequest.Dispose();
}
private void OnApplicationFocus(bool focus)
{
UpdateUserCount(!focus);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a55631aea982e21409a42ae6a1bd5814
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
{
"name": "edu.uga.engr.vel.vel-connect",
"displayName": "VEL-Connect",
"version": "1.0.0",
"unity": "2019.1",
"description": "Web-based configuration for VR applications",
"keywords": [],
"author": {
"name": "Virtual Experiences Laboratory",
"email": "velaboratory@gmail.com",
"url": "https://vel.engr.uga.edu/"
},
"samples": [],
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.0.0"
}
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3e4b6bbeec5c39a428ab34a5e59d56ba
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,41 +1,3 @@
DROP TABLE IF EXISTS `Room`;
CREATE TABLE `Room` (
`room_id` VARCHAR(64) NOT NULL PRIMARY KEY,
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`last_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Can be null if no owner
`owner` VARCHAR(64),
-- The last source to change this object
`modified_by` VARCHAR(64),
-- array of hw_ids of users allowed. Always includes the owner. Null for public
`whitelist` JSON,
`tv_url` VARCHAR(1024),
`carpet_color` VARCHAR(9),
`room_details` JSON
);
DROP TABLE IF EXISTS `Headset`;
CREATE TABLE `Headset` (
`hw_id` VARCHAR(64) NOT NULL PRIMARY KEY,
-- The last source to change this object
`modified_by` VARCHAR(64),
-- The room_id of the owned room
`owned_room` VARCHAR(64),
-- The room_id of the current room. Can be null if room not specified
`current_room` VARCHAR(64) DEFAULT "0",
-- changes relatively often. Generated by the headset
`pairing_code` INT,
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- the last time this headset was actually seen
`last_used` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`user_color` VARCHAR(9),
`user_name` VARCHAR(64),
`avatar_url` VARCHAR(128),
-- Stuff like player color, nickname, whiteboard state
`user_details` JSON,
`streamer_stream_id` VARCHAR(64),
`streamer_control_id` VARCHAR(64)
);
DROP TABLE IF EXISTS `APIKey`;
CREATE TABLE `APIKey` (
`key` VARCHAR(64) NOT NULL PRIMARY KEY,
-- 0 is all access, higher is less
@ -43,16 +5,54 @@ CREATE TABLE `APIKey` (
`auth_level` INT,
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`last_used` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Incremented every time this key is used
`uses` INT DEFAULT 0
);
DROP TABLE IF EXISTS `UserCount`;
CREATE TABLE `UserCount` (
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`hw_id` VARCHAR(64) NOT NULL,
`app_id` VARCHAR(64) NOT NULL,
`room_id` VARCHAR(64) NOT NULL,
`total_users` INT NOT NULL DEFAULT 0,
`room_users` INT NOT NULL DEFAULT 0,
`version` VARCHAR(32) DEFAULT "0",
`platform` VARCHAR(64) DEFAULT "none",
`version` VARCHAR(32),
`platform` VARCHAR(64),
PRIMARY KEY (`timestamp`, `hw_id`)
);
CREATE TABLE `Device` (
-- Unique identifier for this device
`hw_id` TEXT NOT NULL PRIMARY KEY,
-- info about the hardware. Would specify Quest or Windows for example
`os_info` TEXT,
-- A human-readable name for this device. Could be a username
`friendly_name` TEXT,
-- The last source to change this object. Generally this is the device id
`modified_by` TEXT,
-- The app_id of the current app. Can be null if app left cleanly
`current_app` TEXT,
-- The room_id of the current room. Can be null if room not specified. Could be some other sub-app identifier
`current_room` TEXT,
-- changes relatively often. Generated by the headset
`pairing_code` INT,
-- the first time this device was seen
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- the last time this device data was modified
`last_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- JSON containing arbitrary data
`data` TEXT
);
CREATE TABLE `DataBlock` (
-- Could be randomly generated. For room data, this is 'appId_roomName'
`id` TEXT NOT NULL PRIMARY KEY,
-- This is an indexable field to filter out different types of datablocks
`category` TEXT,
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- The last source to change this object. Generally this is the device id
`modified_by` TEXT,
`last_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- the last time this data was fetched individually
`last_accessed` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- JSON containing arbitrary data
`data` TEXT
);

8
velconnect/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM python:3.10
WORKDIR /usr/src/velconnect
COPY ./requirements.txt /usr/src/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /usr/src/requirements.txt
COPY . /usr/src/velconnect
EXPOSE 80
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

View File

@ -3,48 +3,49 @@ import os
import traceback
def create_or_connect():
db_name = 'velconnect.db'
create = False
if not os.path.exists(db_name, ):
create = True
class DB:
def __init__(self, db_name):
self.db_name = db_name
conn = sqlite3.connect(db_name)
conn.row_factory = sqlite3.Row
curr = conn.cursor()
if create:
# create the db
with open('CreateDB.sql', 'r') as f:
curr.executescript(f.read())
def create_or_connect(self):
create = False
if not os.path.exists(self.db_name, ):
create = True
conn.set_trace_callback(print)
return conn, curr
conn = sqlite3.connect(self.db_name)
conn.row_factory = sqlite3.Row
curr = conn.cursor()
if create:
# create the db
with open('CreateDB.sql', 'r') as f:
curr.executescript(f.read())
conn.set_trace_callback(print)
return conn, curr
def query(query: str, data: dict = None) -> list:
try:
conn, curr = create_or_connect()
if data is not None:
curr.execute(query, data)
else:
curr.execute(query)
values = curr.fetchall()
conn.close()
return values
except:
print(traceback.print_exc())
conn.close()
raise
def query(self, query_string: str, data: dict = None) -> list:
try:
conn, curr = self.create_or_connect()
if data is not None:
curr.execute(query_string, data)
else:
curr.execute(query_string)
values = curr.fetchall()
conn.close()
return values
except:
print(traceback.print_exc())
conn.close()
raise
def insert(query: str, data: dict = None) -> bool:
try:
conn, curr = create_or_connect()
curr.execute(query, data)
conn.commit()
conn.close()
return True
except:
print(traceback.print_exc())
conn.close()
raise
def insert(self, query_string: str, data: dict = None) -> bool:
try:
conn, curr = self.create_or_connect()
curr.execute(query_string, data)
conn.commit()
conn.close()
return True
except:
print(traceback.print_exc())
conn.close()
raise

View File

@ -1,11 +1,14 @@
from imp import reload
import uvicorn
from fastapi.middleware.cors import CORSMiddleware
from fastapi import FastAPI
from api import router as api_router
from website import router as website_router
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from routes.api import router as api_router
from routes.api_v2 import router as api_v2_router
from routes.user_count import router as user_count_router
from routes.oculus_api import router as oculus_api_router
from routes.website import router as website_router
app = FastAPI()
origins = [
@ -26,6 +29,9 @@ app.add_middleware(
app.mount("/static", StaticFiles(directory="static"), name="static")
app.include_router(api_router)
app.include_router(api_v2_router)
app.include_router(user_count_router)
app.include_router(oculus_api_router)
app.include_router(website_router)
if __name__ == '__main__':

4
velconnect/rebuild.sh Normal file
View File

@ -0,0 +1,4 @@
docker build -t velconnect .
docker rm web
docker run -p 8081:80 --name web velconnect

View File

@ -1,16 +1,12 @@
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 import Depends, HTTPException, status
from fastapi.responses import HTMLResponse
from fastapi.security import OAuth2PasswordBearer
from db import query, insert
from pydantic import BaseModel
from typing import Union
from pyppeteer import launch
from enum import Enum
import db
db = db.DB("velconnect.db")
# APIRouter creates path operations for user module
router = APIRouter(
@ -25,7 +21,7 @@ oauth2_scheme = OAuth2PasswordBearer(
def api_key_auth(api_key: str = Depends(oauth2_scheme)):
return True
values = query(
values = db.query(
"SELECT * FROM `APIKey` WHERE `key`=:key;", {'key': api_key})
if not (len(values) > 0 and values['auth_level'] < 0):
raise HTTPException(
@ -65,17 +61,16 @@ async def read_root():
@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`;")
values = db.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;",
values = db.query("SELECT * FROM `Headset` WHERE `pairing_code`=:pairing_code;",
{'pairing_code': pairing_code})
if len(values) == 1:
print(values[0]['hw_id'])
return {'hw_id': values[0]['hw_id']}
return values[0]
return {'error': 'Not found'}, 400
@ -93,7 +88,7 @@ def update_pairing_code(data: UpdatePairingCode):
create_headset(data.hw_id)
insert("""
db.insert("""
UPDATE `Headset`
SET `pairing_code`=:pairing_code, `last_used`=CURRENT_TIMESTAMP
WHERE `hw_id`=:hw_id;
@ -103,8 +98,8 @@ def update_pairing_code(data: UpdatePairingCode):
def create_headset(hw_id: str):
insert("""
INSERT OR IGNORE INTO Headset(hw_id) VALUES (:hw_id);
db.insert("""
db.insert IGNORE INTO Headset(hw_id) VALUES (:hw_id);
""", {'hw_id': hw_id})
@ -118,7 +113,7 @@ def get_headset_details(hw_id: str):
def get_headset_details_db(hw_id):
headsets = query("""
headsets = db.query("""
SELECT * FROM `Headset` WHERE `hw_id`=:hw_id;
""", {'hw_id': hw_id})
if len(headsets) == 0:
@ -150,8 +145,8 @@ def set_headset_details_generic(hw_id: str, data: dict):
if key in allowed_keys:
if key == 'current_room':
create_room(data['current_room'])
insert(f"UPDATE `Headset` SET {key}=:value, modified_by=:sender_id WHERE `hw_id`=:hw_id;", {
'value': data[key], 'hw_id': hw_id, 'sender_id': data['sender_id']})
db.insert(f"UPDATE `Headset` SET {key}=:value, modified_by=:sender_id WHERE `hw_id`=:hw_id;", {
'value': data[key], 'hw_id': hw_id, 'sender_id': data['sender_id']})
return {'success': True}
@ -168,8 +163,9 @@ def set_room_details_generic(room_id: str, data: dict):
for key in data:
if key in allowed_keys:
insert("UPDATE `Room` SET " + key +
"=:value, modified_by=:sender_id WHERE `room_id`=:room_id;", {'value': data[key], 'room_id': room_id, 'sender_id': data['sender_id']})
db.insert("UPDATE `Room` SET " + key +
"=:value, modified_by=:sender_id WHERE `room_id`=:room_id;",
{'value': data[key], 'room_id': room_id, 'sender_id': data['sender_id']})
return {'success': True}
@ -179,7 +175,7 @@ def get_room_details(room_id: str):
def get_room_details_db(room_id):
values = query("""
values = db.query("""
SELECT * FROM `Room` WHERE room_id=:room_id;
""", {'room_id': room_id})
if len(values) == 1:
@ -189,77 +185,11 @@ def get_room_details_db(room_id):
def create_room(room_id):
insert("""
INSERT OR IGNORE INTO `Room`(room_id)
db.insert("""
db.insert IGNORE INTO `Room`(room_id)
VALUES(
:room_id
);
""", {'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,
:room_id,
:total_users,
:room_users,
:version,
:platform
);
""", 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
class QuestRift(str, Enum):
quest = "quest"
rift = "rift"
@router.get('/get_store_details/{quest_rift}/{app_id}', tags=["Oculus API"])
async def get_version_nums(quest_rift: QuestRift, app_id: int):
browser = await launch(headless=True, options={'args': ['--no-sandbox']})
page = await browser.newPage()
await page.goto(f'https://www.oculus.com/experiences/{quest_rift}/{app_id}')
ret = {}
# title
title = await page.querySelector(".app-description__title")
ret["title"] = await page.evaluate("e => e.textContent", title)
# description
desc = await page.querySelector(".clamped-description__content")
ret["description"] = await page.evaluate("e => e.textContent", desc)
# versions
await page.evaluate("document.querySelector('.app-details-version-info-row__version').nextElementSibling.firstChild.click();")
elements = await page.querySelectorAll('.sky-dropdown__link.link.link--clickable')
versions = []
for e in elements:
v = await page.evaluate('(element) => element.textContent', e)
versions.append({
'channel': v.split(':')[0],
'version': v.split(':')[1]
})
ret["versions"] = versions
await browser.close()
return ret

208
velconnect/routes/api_v2.py Normal file
View File

@ -0,0 +1,208 @@
import secrets
import json
import string
import fastapi
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import db
db = db.DB("velconnect_v2.db")
# APIRouter creates path operations for user module
router = fastapi.APIRouter(
prefix="/api/v2",
tags=["API V2"],
responses={404: {"description": "Not found"}},
)
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
async def read_root():
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<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_devices')
def get_all_devices():
"""Returns a list of all devices and details associated with them."""
values = db.query("SELECT * FROM `Device`;")
return values
@router.get('/get_device_by_pairing_code/{pairing_code}')
def get_device_by_pairing_code(pairing_code: str):
values = db.query("SELECT * FROM `Device` WHERE `pairing_code`=:pairing_code;",
{'pairing_code': pairing_code})
if len(values) == 1:
return values[0]
return {'error': 'Not found'}, 400
def create_device(hw_id: str):
db.insert("""
INSERT IGNORE INTO `Device`(hw_id) VALUES (:hw_id);
""", {'hw_id': hw_id})
@router.get('/device/get_data/{hw_id}')
def get_state(hw_id: str):
"""Gets the device state"""
devices = db.query("""
SELECT * FROM `Device` WHERE `hw_id`=:hw_id;
""", {'hw_id': hw_id})
if len(devices) == 0:
return {'error': "Can't find device with that id."}
room_data = get_data(f"{devices[0]['current_app']}_{devices[0]['current_room']}")
return {'device': devices[0], 'room': room_data}
@router.post('/device/set_data/{hw_id}')
def set_state(hw_id: str, data: dict, request: fastapi.Request):
"""Sets the device state"""
create_device(hw_id)
# add the client's IP address if no sender specified
if 'modified_by' not in data:
data['modified_by'] = str(request.client) + "_" + str(request.headers)
allowed_keys: list[str] = [
'os_info',
'friendly_name',
'modified_by',
'current_app',
'current_room',
'pairing_code',
]
for key in data:
if key in allowed_keys:
db.insert(f"""
UPDATE `Device`
SET {key}=:value,
last_modified=CURRENT_TIMESTAMP
WHERE `hw_id`=:hw_id;
""",
{
'value': data[key],
'hw_id': hw_id,
'sender_id': data['sender_id']
})
if key == "data":
# get the old json values and merge the data
old_data_query = db.query("""
SELECT data
FROM `Device`
WHERE hw_id=:hw_id
""", {"hw_id": hw_id})
if len(old_data_query) == 1:
old_data: dict = json.loads(old_data_query[0]["data"])
data = {**old_data, **data}
# add the data to the db
db.insert("""
UPDATE `Device`
SET data=:data,
last_modified=CURRENT_TIMESTAMP
WHERE hw_id=:hw_id;
""", {"hw_id": hw_id, "data": json.dumps(data)})
return {'success': True}
def generate_id(length: int = 4) -> str:
return ''.join(
secrets.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for i in range(length))
@router.post('/set_data')
def store_data_with_random_key(request: fastapi.Request, data: dict, category: str = None) -> dict:
"""Creates a little storage bucket for arbitrary data with a random key"""
return store_data(request, data, None, category)
@router.post('/set_data/{key}')
def store_data(request: fastapi.Request, data: dict, key: str = None, modified_by: str = None, category: str = None) -> dict:
"""Creates a little storage bucket for arbitrary data"""
# add the client's IP address if no sender specified
if modified_by is None:
modified_by = str(request.client) + "_" + str(request.headers)
# generates a key if none was supplied
if key is None:
key = generate_id()
# regenerate if necessary
while len(db.query("SELECT id FROM `DataBlock` WHERE id=:id;", {"id": key})) > 0:
key = generate_id()
# get the old json values and merge the data
old_data_query = db.query("""
SELECT data
FROM `DataBlock`
WHERE id=:id
""", {"id": key})
if len(old_data_query) == 1:
old_data: dict = json.loads(old_data_query[0]["data"])
data = {**old_data, **data}
# add the data to the db
db.insert("""
REPLACE INTO `DataBlock` (id, category, modified_by, data, last_modified)
VALUES(:id, :category, :modified_by, :data, CURRENT_TIMESTAMP);
""", {"id": key, "category": category, "modified_by": modified_by, "data": json.dumps(data)})
return {'key': key}
@router.get('/get_data/{key}')
def get_data(key: str) -> dict:
"""Gets data from a storage bucket for arbitrary data"""
data = db.query("""
SELECT data
FROM `DataBlock`
WHERE id=:id
""", {"id": key})
db.insert("""
UPDATE `DataBlock`
SET last_accessed = CURRENT_TIMESTAMP
WHERE id=:id;
""", {"id": key})
try:
if len(data) == 1:
return json.loads(data[0])
return {'error': 'Not found'}
except:
return {'error': 'Unknown. Maybe no data at this key.'}

View File

@ -0,0 +1,52 @@
from enum import Enum
import fastapi
from pyppeteer import launch
# APIRouter creates path operations for user module
router = fastapi.APIRouter(
prefix="/api",
tags=["Oculus API"],
responses={404: {"description": "Not found"}},
)
class QuestRift(str, Enum):
quest = "quest"
rift = "rift"
@router.get('/get_store_details/{quest_rift}/{app_id}')
async def get_version_nums(quest_rift: QuestRift, app_id: int):
browser = await launch(headless=True, options={'args': ['--no-sandbox']})
page = await browser.newPage()
await page.goto(f'https://www.oculus.com/experiences/{quest_rift}/{app_id}')
ret = {}
# title
title = await page.querySelector(".app-description__title")
ret["title"] = await page.evaluate("e => e.textContent", title)
# description
desc = await page.querySelector(".clamped-description__content")
ret["description"] = await page.evaluate("e => e.textContent", desc)
# versions
await page.evaluate(
"document.querySelector('.app-details-version-info-row__version').nextElementSibling.firstChild.click();")
elements = await page.querySelectorAll('.sky-dropdown__link.link.link--clickable')
versions = []
for e in elements:
v = await page.evaluate('(element) => element.textContent', e)
versions.append({
'channel': v.split(':')[0],
'version': v.split(':')[1]
})
ret["versions"] = versions
await browser.close()
return ret

View File

@ -0,0 +1,68 @@
import fastapi
import db
db = db.DB("velconnect.db")
# APIRouter creates path operations for user module
router = fastapi.APIRouter(
prefix="/api",
tags=["User Count"],
responses={404: {"description": "Not found"}},
)
post_user_count_example = {
"default": {
"summary": "Example insert for user count",
"value": {
"hw_id": "1234",
"app_id": "example",
"room_id": "0",
"total_users": 1,
"room_users": 1,
"version": "0.1",
"platform": "Windows"
}
}
}
@router.post('/update_user_count')
def update_user_count(data: dict = fastapi.Body(..., examples=post_user_count_example)) -> dict:
if 'app_id' not in data:
data['app_id'] = ""
db.insert("""
REPLACE INTO `UserCount` (
timestamp,
hw_id,
app_id,
room_id,
total_users,
room_users,
version,
platform
)
VALUES(
CURRENT_TIMESTAMP,
:hw_id,
:app_id,
:room_id,
:total_users,
:room_users,
:version,
:platform
);
""", data)
return {'success': True}
@router.get('/get_user_count')
def get_user_count(app_id: str = None, hours: float = 24) -> list:
values = db.query("""
SELECT timestamp, total_users
FROM `UserCount`
WHERE app_id = :app_id AND
timestamp > datetime('now', '-""" + str(hours) + """ Hour');
""", {"app_id": app_id})
return values

View File

@ -1,11 +1,11 @@
from fastapi import APIRouter
from fastapi.responses import FileResponse
# APIRouter creates path operations for user module
router = APIRouter(
prefix="",
tags=["Website"],
include_in_schema=False
)

View File

@ -186,7 +186,7 @@
</div>
<br>
<div>
<p>To share your screen, download <a href="https://github.com/velaboratory/VEL-Share">VEL-Share
<p>To share your screen, download <a href="https://github.com/velaboratory/VEL-Share/releases/latest" target="_blank">VEL-Share
<svg style="width:1em;height:1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"/>