add velconnect unity package, lots of api changes to make it more generic, added dockerfile
parent
4997679aae
commit
4502662a53
|
|
@ -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,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0ce56caac2a2b78479e2bebd4f84101f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5b08ac62f2f2a9642bc42da52d43f5de
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 907e38b516b504b408923c7bed1662f6
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a55631aea982e21409a42ae6a1bd5814
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3e4b6bbeec5c39a428ab34a5e59d56ba
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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__':
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
docker build -t velconnect .
|
||||
docker rm web
|
||||
docker run -p 8081:80 --name web velconnect
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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.'}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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"/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue