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` (
|
CREATE TABLE `APIKey` (
|
||||||
`key` VARCHAR(64) NOT NULL PRIMARY KEY,
|
`key` VARCHAR(64) NOT NULL PRIMARY KEY,
|
||||||
-- 0 is all access, higher is less
|
-- 0 is all access, higher is less
|
||||||
|
|
@ -43,16 +5,54 @@ CREATE TABLE `APIKey` (
|
||||||
`auth_level` INT,
|
`auth_level` INT,
|
||||||
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
`last_used` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
`last_used` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
-- Incremented every time this key is used
|
||||||
`uses` INT DEFAULT 0
|
`uses` INT DEFAULT 0
|
||||||
);
|
);
|
||||||
DROP TABLE IF EXISTS `UserCount`;
|
|
||||||
CREATE TABLE `UserCount` (
|
CREATE TABLE `UserCount` (
|
||||||
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`hw_id` VARCHAR(64) NOT NULL,
|
`hw_id` VARCHAR(64) NOT NULL,
|
||||||
|
`app_id` VARCHAR(64) NOT NULL,
|
||||||
`room_id` VARCHAR(64) NOT NULL,
|
`room_id` VARCHAR(64) NOT NULL,
|
||||||
`total_users` INT NOT NULL DEFAULT 0,
|
`total_users` INT NOT NULL DEFAULT 0,
|
||||||
`room_users` INT NOT NULL DEFAULT 0,
|
`room_users` INT NOT NULL DEFAULT 0,
|
||||||
`version` VARCHAR(32) DEFAULT "0",
|
`version` VARCHAR(32),
|
||||||
`platform` VARCHAR(64) DEFAULT "none",
|
`platform` VARCHAR(64),
|
||||||
PRIMARY KEY (`timestamp`, `hw_id`)
|
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
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
def create_or_connect():
|
class DB:
|
||||||
db_name = 'velconnect.db'
|
def __init__(self, db_name):
|
||||||
create = False
|
self.db_name = db_name
|
||||||
if not os.path.exists(db_name, ):
|
|
||||||
create = True
|
|
||||||
|
|
||||||
conn = sqlite3.connect(db_name)
|
def create_or_connect(self):
|
||||||
conn.row_factory = sqlite3.Row
|
create = False
|
||||||
curr = conn.cursor()
|
if not os.path.exists(self.db_name, ):
|
||||||
if create:
|
create = True
|
||||||
# create the db
|
|
||||||
with open('CreateDB.sql', 'r') as f:
|
|
||||||
curr.executescript(f.read())
|
|
||||||
|
|
||||||
conn.set_trace_callback(print)
|
conn = sqlite3.connect(self.db_name)
|
||||||
return conn, curr
|
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:
|
def query(self, query_string: str, data: dict = None) -> list:
|
||||||
try:
|
try:
|
||||||
conn, curr = create_or_connect()
|
conn, curr = self.create_or_connect()
|
||||||
if data is not None:
|
if data is not None:
|
||||||
curr.execute(query, data)
|
curr.execute(query_string, data)
|
||||||
else:
|
else:
|
||||||
curr.execute(query)
|
curr.execute(query_string)
|
||||||
values = curr.fetchall()
|
values = curr.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return values
|
return values
|
||||||
except:
|
except:
|
||||||
print(traceback.print_exc())
|
print(traceback.print_exc())
|
||||||
conn.close()
|
conn.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def insert(self, query_string: str, data: dict = None) -> bool:
|
||||||
def insert(query: str, data: dict = None) -> bool:
|
try:
|
||||||
try:
|
conn, curr = self.create_or_connect()
|
||||||
conn, curr = create_or_connect()
|
curr.execute(query_string, data)
|
||||||
curr.execute(query, data)
|
conn.commit()
|
||||||
conn.commit()
|
conn.close()
|
||||||
conn.close()
|
return True
|
||||||
return True
|
except:
|
||||||
except:
|
print(traceback.print_exc())
|
||||||
print(traceback.print_exc())
|
conn.close()
|
||||||
conn.close()
|
raise
|
||||||
raise
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
from imp import reload
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from api import router as api_router
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from website import router as website_router
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
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()
|
app = FastAPI()
|
||||||
|
|
||||||
origins = [
|
origins = [
|
||||||
|
|
@ -26,6 +29,9 @@ app.add_middleware(
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
app.include_router(api_router)
|
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)
|
app.include_router(website_router)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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 APIRouter
|
||||||
from fastapi import Query
|
from fastapi import Depends, HTTPException, status
|
||||||
from typing import Optional
|
from fastapi.responses import HTMLResponse
|
||||||
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 fastapi.security import OAuth2PasswordBearer
|
||||||
from db import query, insert
|
|
||||||
from pydantic import BaseModel
|
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
|
# APIRouter creates path operations for user module
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
|
|
@ -25,7 +21,7 @@ oauth2_scheme = OAuth2PasswordBearer(
|
||||||
|
|
||||||
def api_key_auth(api_key: str = Depends(oauth2_scheme)):
|
def api_key_auth(api_key: str = Depends(oauth2_scheme)):
|
||||||
return True
|
return True
|
||||||
values = query(
|
values = db.query(
|
||||||
"SELECT * FROM `APIKey` WHERE `key`=:key;", {'key': api_key})
|
"SELECT * FROM `APIKey` WHERE `key`=:key;", {'key': api_key})
|
||||||
if not (len(values) > 0 and values['auth_level'] < 0):
|
if not (len(values) > 0 and values['auth_level'] < 0):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -65,17 +61,16 @@ async def read_root():
|
||||||
@router.get('/get_all_headsets')
|
@router.get('/get_all_headsets')
|
||||||
def get_all_headsets():
|
def get_all_headsets():
|
||||||
"""Returns a list of all headsets and details associated with them."""
|
"""Returns a list of all headsets and details associated with them."""
|
||||||
values = query("SELECT * FROM `Headset`;")
|
values = db.query("SELECT * FROM `Headset`;")
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
@router.get('/pair_headset/{pairing_code}')
|
@router.get('/pair_headset/{pairing_code}')
|
||||||
def pair_headset(pairing_code: str):
|
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})
|
{'pairing_code': pairing_code})
|
||||||
if len(values) == 1:
|
if len(values) == 1:
|
||||||
print(values[0]['hw_id'])
|
return values[0]
|
||||||
return {'hw_id': values[0]['hw_id']}
|
|
||||||
return {'error': 'Not found'}, 400
|
return {'error': 'Not found'}, 400
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -93,7 +88,7 @@ def update_pairing_code(data: UpdatePairingCode):
|
||||||
|
|
||||||
create_headset(data.hw_id)
|
create_headset(data.hw_id)
|
||||||
|
|
||||||
insert("""
|
db.insert("""
|
||||||
UPDATE `Headset`
|
UPDATE `Headset`
|
||||||
SET `pairing_code`=:pairing_code, `last_used`=CURRENT_TIMESTAMP
|
SET `pairing_code`=:pairing_code, `last_used`=CURRENT_TIMESTAMP
|
||||||
WHERE `hw_id`=:hw_id;
|
WHERE `hw_id`=:hw_id;
|
||||||
|
|
@ -103,8 +98,8 @@ def update_pairing_code(data: UpdatePairingCode):
|
||||||
|
|
||||||
|
|
||||||
def create_headset(hw_id: str):
|
def create_headset(hw_id: str):
|
||||||
insert("""
|
db.insert("""
|
||||||
INSERT OR IGNORE INTO Headset(hw_id) VALUES (:hw_id);
|
db.insert IGNORE INTO Headset(hw_id) VALUES (:hw_id);
|
||||||
""", {'hw_id': hw_id})
|
""", {'hw_id': hw_id})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -118,7 +113,7 @@ def get_headset_details(hw_id: str):
|
||||||
|
|
||||||
|
|
||||||
def get_headset_details_db(hw_id):
|
def get_headset_details_db(hw_id):
|
||||||
headsets = query("""
|
headsets = db.query("""
|
||||||
SELECT * FROM `Headset` WHERE `hw_id`=:hw_id;
|
SELECT * FROM `Headset` WHERE `hw_id`=:hw_id;
|
||||||
""", {'hw_id': hw_id})
|
""", {'hw_id': hw_id})
|
||||||
if len(headsets) == 0:
|
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 in allowed_keys:
|
||||||
if key == 'current_room':
|
if key == 'current_room':
|
||||||
create_room(data['current_room'])
|
create_room(data['current_room'])
|
||||||
insert(f"UPDATE `Headset` SET {key}=:value, modified_by=:sender_id WHERE `hw_id`=:hw_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']})
|
'value': data[key], 'hw_id': hw_id, 'sender_id': data['sender_id']})
|
||||||
return {'success': True}
|
return {'success': True}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -168,8 +163,9 @@ def set_room_details_generic(room_id: str, data: dict):
|
||||||
|
|
||||||
for key in data:
|
for key in data:
|
||||||
if key in allowed_keys:
|
if key in allowed_keys:
|
||||||
insert("UPDATE `Room` SET " + key +
|
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']})
|
"=: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}
|
return {'success': True}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -179,7 +175,7 @@ def get_room_details(room_id: str):
|
||||||
|
|
||||||
|
|
||||||
def get_room_details_db(room_id):
|
def get_room_details_db(room_id):
|
||||||
values = query("""
|
values = db.query("""
|
||||||
SELECT * FROM `Room` WHERE room_id=:room_id;
|
SELECT * FROM `Room` WHERE room_id=:room_id;
|
||||||
""", {'room_id': room_id})
|
""", {'room_id': room_id})
|
||||||
if len(values) == 1:
|
if len(values) == 1:
|
||||||
|
|
@ -189,77 +185,11 @@ def get_room_details_db(room_id):
|
||||||
|
|
||||||
|
|
||||||
def create_room(room_id):
|
def create_room(room_id):
|
||||||
insert("""
|
db.insert("""
|
||||||
INSERT OR IGNORE INTO `Room`(room_id)
|
db.insert IGNORE INTO `Room`(room_id)
|
||||||
VALUES(
|
VALUES(
|
||||||
:room_id
|
:room_id
|
||||||
);
|
);
|
||||||
""", {'room_id': room_id})
|
""", {'room_id': room_id})
|
||||||
return {'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 import APIRouter
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
|
|
||||||
# APIRouter creates path operations for user module
|
# APIRouter creates path operations for user module
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="",
|
prefix="",
|
||||||
tags=["Website"],
|
tags=["Website"],
|
||||||
|
include_in_schema=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<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">
|
<svg style="width:1em;height:1em" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor"
|
<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"/>
|
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