big things are happening to velconnect

dev
Anton Franzluebbers 2023-07-07 00:35:35 -04:00
parent c1f4721f41
commit 862a9d5e58
71 changed files with 13926 additions and 2191 deletions

21
.github/workflows/deploy_oracle_pb.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Deploy to VelNet Oracle server (Pocketbase edition)
on:
push:
branches: ["feature/pocketbase-server"]
paths: ["velconnect/**"]
jobs:
run_pull:
name: Pull new version
runs-on: ubuntu-latest
steps:
- name: install ssh keys
run: |
install -m 600 -D /dev/null ~/.ssh/id_rsa
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} > ~/.ssh/known_hosts
- name: connect and pull
run: ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd ${{ secrets.SSH_WORK_DIR }}/../VEL-Connect-PB && git pull && docker compose up -d --build && exit"
- name: cleanup
run: rm -rf ~/.ssh

View File

@ -1,69 +1,65 @@
## VELConnect API Setup
## VEL-Connect Server Setup
## Option 1: Build and run using Docker Compose
### Option 1: Download the latest binary from releases
Then run with:
- Windows: `velconnect.exe serve`
- Linux: `./velconnect serve`
Run `./velconnect help` for help
### Option 1: Build and run using Docker Compose
```sh
cd velconnect
docker compose up -d
docker compose up -d --build
```
and visit http://localhost:8046 in your browser.
and visit http://localhost:8090/\_/ in your browser.
This will set up autorestart of the docker image. To pull updates, just run `docker compose up -d` again.
This will set up autorestart of the docker image. To pull updates, just run `docker compose up -d --build` again.
## Option 2: Pull from Docker Hub:
### Option 2: Pull from Docker Hub:
```sh
docker run -p 80:80 velaboratory/velconnect
docker run -p 80:8090 velaboratory/velconnect
```
and visit http://localhost in your browser.
and visit http://localhost/\_/ in your browser.
or
```sh
docker run -p 8000:80 --name web velaboratory/velconnect
docker run -p 8080:8090 --name velconnect velaboratory/velconnect
```
to access from http://localhost:8000 in your browser and name the container "web".
to access from http://localhost:8080/\_/ in your browser and name the container "velconnect".
## Option 3: Build Docker Image:
### Option 3: Run Go locally
Make sure you're in the `velconnect/` folder.
1. Make sure to install [Go](https://go.dev/) on your machine
2. `cd velconnect`
3. To run: `go run main.go serve`
4. To build: `go build`
- Then run the executable e.g. `velconnect.exe serve`
```sh
docker build --tag velconnect .
docker rm web
docker run -p 80:80 --name web velconnect
```
or run `./rebuild.sh`
## Option 4: Run Python locally (WSL or native Linux)
1. `cd velconnect`
2. Create pip env: `python3 -m venv env`
3. Activate the env `. env/bin/activate`
4. Install packages `pip install -r requirements.txt`
5. Add `config_mysql.py`
- Get from some old server
- Or copy and fill out the values from `config_mysql_template.py`
6. Run `./run_server.sh`
- Or set up systemctl service:
## Set up systemctl service:
```ini
[Unit]
Description=VELConnect API
Requires=network.target
After=network.target
Description = velconnect
[Service]
User=ubuntu
Group=ubuntu
Environment="PATH=/home/ubuntu/VEL-Connect/velconnect/env/bin"
WorkingDirectory=/home/ubuntu/VEL-Connect/velconnect
ExecStart=/home/ubuntu/VEL-Connect/velconnect/env/bin/uvicorn --port 8005 main:app
Type = simple
User = root
Group = root
LimitNOFILE = 4096
Restart = always
RestartSec = 5s
StandardOutput = append:/home/ubuntu/VEL-Connect/velconnect/errors.log
StandardError = append:/home/ubuntu/VEL-Connect/velconnect/errors.log
ExecStart = /home/ubuntu/VEL-Connect/velconnect/velconnect serve
[Install]
WantedBy = multi-user.target
```

21
example_control_panel/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```
npm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:3000` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

@ -0,0 +1,4 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});

9383
example_control_panel/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
{
"name": "",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^2.8.0"
}
}

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 815 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 977 B

After

Width:  |  Height:  |  Size: 977 B

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 224 KiB

View File

Before

Width:  |  Height:  |  Size: 437 KiB

After

Width:  |  Height:  |  Size: 437 KiB

View File

Before

Width:  |  Height:  |  Size: 437 KiB

After

Width:  |  Height:  |  Size: 437 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

1
example_control_panel/src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="astro/client" />

View File

@ -0,0 +1,15 @@
---
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
</body>
</html>

View File

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}

View File

@ -6,6 +6,7 @@ using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Networking;
@ -13,6 +14,7 @@ using VelNet;
namespace VELConnect
{
// ReSharper disable once InconsistentNaming
public class VELConnectManager : MonoBehaviour
{
public string velConnectUrl = "http://localhost";
@ -33,15 +35,17 @@ namespace VELConnect
public class Device
{
public string hw_id;
public readonly string id;
public readonly DateTime created;
public readonly DateTime updated;
public string device_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 string pairing_code;
public DateTime last_online;
public Dictionary<string, string> data;
/// <summary>
@ -56,13 +60,14 @@ namespace VELConnect
public class RoomState
{
public string error;
public string id;
public readonly string id;
public readonly DateTime created;
public readonly DateTime updated;
public string block_id;
public string owner_id;
public string visibility;
public string category;
public string date_created;
public string modified_by;
public string last_modified;
public string last_accessed;
public Dictionary<string, string> data;
/// <summary>
@ -75,17 +80,32 @@ namespace VELConnect
}
}
public User user;
public Device device;
public RoomState room;
}
public class UserCount
{
public readonly string id;
public readonly DateTime created;
public readonly DateTime updated;
public string device_id;
public string app_id;
public string room_id;
public int total_users;
public int room_users;
public string version;
public string platform;
}
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 Action<string, object> OnDeviceDataChanged;
public static Action<string, object> OnRoomDataChanged;
private static readonly Dictionary<string, List<CallbackListener>> deviceFieldCallbacks =
new Dictionary<string, List<CallbackListener>>();
@ -111,7 +131,7 @@ namespace VELConnect
public bool sendInitialState;
}
public static int PairingCode
public static string PairingCode
{
get
{
@ -120,7 +140,7 @@ namespace VELConnect
// 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;
return (Math.Abs(hash.GetHashCode()) % 9000 + 1000).ToString();
}
}
@ -147,24 +167,24 @@ namespace VELConnect
// Start is called before the first frame update
private void Start()
{
SetDeviceField(new Dictionary<string, object>
SetDeviceField(new State.Device
{
{ "current_app", Application.productName },
{ "pairing_code", PairingCode },
{ "friendly_name", SystemInfo.deviceName },
os_info = SystemInfo.operatingSystem,
friendly_name = SystemInfo.deviceName,
current_app = Application.productName,
pairing_code = PairingCode,
});
UpdateUserCount();
StartCoroutine(SlowLoop());
VelNetManager.OnJoinedRoom += room =>
{
SetDeviceField(new Dictionary<string, object>
SetDeviceField(new State.Device
{
{ "current_app", Application.productName },
{ "current_room", room },
current_app = Application.productName,
current_room = room,
});
};
}
@ -176,17 +196,20 @@ namespace VELConnect
VelNetManager.GetRooms(rooms =>
{
Dictionary<string, object> postData = new Dictionary<string, object>
UserCount postData = new UserCount
{
{ "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 },
device_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/update_user_count", JsonConvert.SerializeObject(postData));
PostRequestCallback(velConnectUrl + "/api/collections/UserCount/records", JsonConvert.SerializeObject(postData, Formatting.None, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
}));
});
}
@ -196,7 +219,7 @@ namespace VELConnect
{
try
{
GetRequestCallback(velConnectUrl + "/api/device/get_data/" + DeviceId, json =>
GetRequestCallback(velConnectUrl + "/state/device/" + DeviceId, json =>
{
State state = JsonConvert.DeserializeObject<State>(json);
if (state == null) return;
@ -221,10 +244,11 @@ namespace VELConnect
}
if (state.device.modified_by != DeviceId)
// if (state.device.modified_by != DeviceId)
{
FieldInfo[] fields = state.device.GetType().GetFields();
// loop through all the fields in the device
foreach (FieldInfo fieldInfo in fields)
{
string newValue = fieldInfo.GetValue(state.device) as string;
@ -311,7 +335,8 @@ namespace VELConnect
}
}
if (state.room.modified_by != DeviceId && state.room.data != null)
// if (state.room.modified_by != DeviceId && state.room.data != null)
if (state.room?.data != null)
{
foreach (KeyValuePair<string, string> elem in state.room.data)
{
@ -488,13 +513,30 @@ namespace VELConnect
/// <summary>
/// Sets data on the device keys themselves
/// These are fixed fields defined for every application
/// </summary>
public static void SetDeviceField(Dictionary<string, object> device)
public static void SetDeviceField(State.Device device)
{
device.last_online = DateTime.UtcNow;
// update our local state, so we don't get change events on our own updates
if (_instance.lastState?.device != null)
{
FieldInfo[] fields = device.GetType().GetFields();
// loop through all the fields in the device
foreach (FieldInfo fieldInfo in fields)
{
fieldInfo.SetValue(_instance.lastState.device, fieldInfo.GetValue(device));
}
}
PostRequestCallback(
_instance.velConnectUrl + "/api/device/set_data/" + DeviceId,
JsonConvert.SerializeObject(device),
new Dictionary<string, string> { { "modified_by", DeviceId } }
_instance.velConnectUrl + "/device/" + DeviceId,
JsonConvert.SerializeObject(device, Formatting.None, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
})
);
}
@ -503,13 +545,35 @@ namespace VELConnect
/// </summary>
public static void SetDeviceData(Dictionary<string, string> data)
{
State.Device device = new State.Device
{
last_online = DateTime.UtcNow,
data = data,
};
// update our local state, so we don't get change events on our own updates
if (_instance.lastState?.device != null)
{
foreach (KeyValuePair<string, string> kvp in data)
{
_instance.lastState.device.data[kvp.Key] = kvp.Value;
}
}
PostRequestCallback(
_instance.velConnectUrl + "/api/device/set_data/" + DeviceId,
JsonConvert.SerializeObject(new Dictionary<string, object> { { "data", data } }),
new Dictionary<string, string> { { "modified_by", DeviceId } }
_instance.velConnectUrl + "/device/" + DeviceId,
JsonConvert.SerializeObject(device, Formatting.None, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
})
);
}
public static void SetRoomData(string key, string value)
{
SetRoomData(new Dictionary<string, string> { { key, value } });
}
public static void SetRoomData(Dictionary<string, string> data)
{
if (!VelNetManager.InRoom)
@ -518,34 +582,53 @@ namespace VELConnect
return;
}
State.RoomState room = new State.RoomState
{
category = "room",
visibility = "public",
data = data
};
// update our local state, so we don't get change events on our own updates
if (_instance.lastState?.room != null)
{
foreach (KeyValuePair<string, string> kvp in data)
{
_instance.lastState.room.data[kvp.Key] = kvp.Value;
}
}
PostRequestCallback(
_instance.velConnectUrl + "/api/set_data/" + Application.productName + "_" + VelNetManager.Room,
JsonConvert.SerializeObject(data),
new Dictionary<string, string> { { "modified_by", DeviceId } }
_instance.velConnectUrl + "/data_block/" + Application.productName + "_" + VelNetManager.Room,
JsonConvert.SerializeObject(room, Formatting.None, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
})
);
}
// TODO
public static void UploadFile(string fileName, byte[] fileData, Action<string> successCallback = null)
{
MultipartFormDataContent requestContent = new MultipartFormDataContent();
ByteArrayContent fileContent = new ByteArrayContent(fileData);
requestContent.Add(fileContent, "file", fileName);
Task.Run(async () =>
{
HttpResponseMessage r =
await new HttpClient().PostAsync(_instance.velConnectUrl + "/api/upload_file", requestContent);
string resp = await r.Content.ReadAsStringAsync();
Dictionary<string, string> dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(resp);
successCallback?.Invoke(dict["key"]);
});
// MultipartFormDataContent requestContent = new MultipartFormDataContent();
// ByteArrayContent fileContent = new ByteArrayContent(fileData);
//
// requestContent.Add(fileContent, "file", fileName);
//
// Task.Run(async () =>
// {
// HttpResponseMessage r =
// await new HttpClient().PostAsync(_instance.velConnectUrl + "/api/upload_file", requestContent);
// string resp = await r.Content.ReadAsStringAsync();
// Dictionary<string, string> dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(resp);
// successCallback?.Invoke(dict["key"]);
// });
}
// TODO
public static void DownloadFile(string key, Action<byte[]> successCallback = null)
{
_instance.StartCoroutine(_instance.DownloadFileCo(key, successCallback));
// _instance.StartCoroutine(_instance.DownloadFileCo(key, successCallback));
}
private IEnumerator DownloadFileCo(string key, Action<byte[]> successCallback = null)

View File

@ -1,7 +1,7 @@
{
"name": "edu.uga.engr.vel.vel-connect",
"displayName": "VEL-Connect",
"version": "1.0.2",
"version": "2.0.0",
"unity": "2019.1",
"description": "Web-based configuration for VR applications",
"keywords": [],
@ -12,6 +12,7 @@
},
"samples": [],
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.0.0"
"com.unity.nuget.newtonsoft-json": "3.0.0",
"edu.uga.engr.vel.velnet": "1.1.8"
}
}

24
velconnect/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
errors.log
pb_data/
velconnect-pb

View File

@ -1,20 +1,12 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: FastAPI",
"type": "python",
"name": "Launch file",
"type": "go",
"request": "launch",
"module": "uvicorn",
"args": [
"main:app",
"--reload"
],
"jinja": true,
"justMyCode": true
"mode": "debug",
"program": "main.go",
"args": ["serve"]
}
]
}

View File

@ -1,88 +0,0 @@
CREATE TABLE `APIKey` (
`key` VARCHAR(64) NOT NULL PRIMARY KEY,
-- 0 is all access, higher is less
-- 10 is for headset clients
`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
);
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),
`platform` VARCHAR(64),
PRIMARY KEY (`timestamp`, `hw_id`)
);
CREATE TABLE `User` (
-- user is defined by uuid, to which an email can be added without having to migrate.
-- then the data that is coming from a user vs device is constant
-- UUID
`id` TEXT NOT NULL PRIMARY KEY,
-- the user's email
`email` TEXT,
`username` TEXT,
-- 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 `UserDevice` (
-- the user account's uuid
`user_id` TEXT NOT NULL,
-- identifier for the device
-- This is unique because a device can have only one owner
`hw_id` TEXT NOT NULL UNIQUE,
-- when this connection was created
`date_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`, `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. Not a username for the game
`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,
-- id of the owner of this file. Ownership is not transferable because ids may collide,
-- but the owner could be null for global scope
`owner_id` TEXT DEFAULT 'none',
`visibility` TEXT CHECK( `visibility` IN ('public','private','unlisted') ) NOT NULL DEFAULT 'public',
-- 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,
PRIMARY KEY (`id`, `owner_id`)
);

View File

@ -1,9 +1,20 @@
# syntax=docker/dockerfile:1
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"]
FROM golang:1.18 as build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
COPY migrations/ ./migrations
# Build
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o ./velconnect
# RUN ./velconnect-pb migrate up
FROM alpine:3.18.2
COPY --from=build /src/velconnect /velconnect
EXPOSE 8090
# Run
ENTRYPOINT ["./velconnect", "serve", "--http=0.0.0.0:8090"]

View File

View File

@ -1,4 +0,0 @@
MYSQL_DATABASE_DB = ''
MYSQL_DATABASE_USER = ''
MYSQL_DATABASE_HOST = ''
MYSQL_DATABASE_PASSWORD = ''

View File

@ -1,55 +0,0 @@
import sqlite3
import os
import traceback
class DB:
def __init__(self, db_name):
self.db_name = db_name
def create_or_connect(self):
create = False
if not os.path.exists(self.db_name):
create = True
os.makedirs(os.path.dirname(self.db_name))
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.commit()
conn.set_trace_callback(print)
return conn, curr
def query(self, query_string: str, data: dict = None) -> list:
conn = None
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())
if conn is not None:
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,9 +1,9 @@
version: "1.0"
version: "2.0"
services:
web:
server:
build: .
restart: always
ports:
- "8046:80"
- "8090:8090"
volumes:
- ./db:/usr/src/velconnect/db
- ./pb_data:/pb_data

88
velconnect/go.mod Normal file
View File

@ -0,0 +1,88 @@
module velaboratory/velconnect
go 1.18
require (
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
github.com/pocketbase/pocketbase v0.16.7
)
require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.44.289 // indirect
github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/ganigeorgiev/fexpr v0.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/pocketbase/dbx v1.10.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.opencensus.io v0.24.0 // indirect
gocloud.dev v0.30.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/image v0.8.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/oauth2 v0.9.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.128.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.56.1 // indirect
google.golang.org/protobuf v1.30.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.14 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.23.1 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
)

3335
velconnect/go.sum Normal file

File diff suppressed because it is too large Load Diff

211
velconnect/main.go Normal file
View File

@ -0,0 +1,211 @@
package main
import (
"encoding/json"
"log"
"net/http"
_ "velaboratory/velconnect/migrations"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
)
func main() {
app := pocketbase.New()
// loosely check if it was executed using "go run"
// isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
// enable auto creation of migration files when making collection changes
// (the isGoRun check is to enable it only during development)
Automigrate: true,
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// or you can also use the shorter e.Router.GET("/articles/:slug", handler, middlewares...)
e.Router.POST("/data_block/:block_id", func(c echo.Context) error {
dao := app.Dao()
requestData := apis.RequestData(c)
log.Println(requestData)
// get the old value to do a merge
record, err := dao.FindFirstRecordByData("DataBlock", "block_id", c.PathParam("block_id"))
if err == nil {
mergeDataBlock(requestData, record)
} else {
collection, err := dao.FindCollectionByNameOrId("DataBlock")
if err != nil {
return err
}
record = models.NewRecord(collection)
// we don't have an existing data, so just set the new values
if val, ok := requestData.Data["data"]; ok {
record.Set("data", val)
}
}
// add the new values
record.Set("block_id", c.PathParam("block_id"))
fields := []string{
"owner_id",
"visibility",
"category",
}
for _, v := range fields {
if val, ok := requestData.Data[v]; ok {
record.Set(v, val)
}
}
// apply to the db
if err := dao.SaveRecord(record); err != nil {
return err
}
log.Println(record)
return c.JSON(http.StatusOK, record)
},
apis.ActivityLogger(app),
)
e.Router.GET("/data_block/:block_id", func(c echo.Context) error {
record, err := app.Dao().FindFirstRecordByData("DataBlock", "block_id", c.PathParam("block_id"))
if err != nil {
return apis.NewNotFoundError("The data block does not exist.", err)
}
// enable ?expand query param support
apis.EnrichRecord(c, app.Dao(), record)
return c.JSON(http.StatusOK, record)
},
apis.ActivityLogger(app),
)
e.Router.POST("/device/:device_id", func(c echo.Context) error {
dao := app.Dao()
requestData := apis.RequestData(c)
// get the old value to do a merge
record, err := dao.FindFirstRecordByData("Device", "device_id", c.PathParam("device_id"))
if err == nil {
mergeDataBlock(requestData, record)
} else {
collection, err := dao.FindCollectionByNameOrId("Device")
if err != nil {
return err
}
record = models.NewRecord(collection)
// we don't have an existing data, so just set the new values
if val, ok := requestData.Data["data"]; ok {
record.Set("data", val)
}
}
// add the new values
record.Set("device_id", c.PathParam("device_id"))
fields := []string{
"os_info",
"friendly_name",
"modified_by",
"current_app",
"current_room",
"pairing_code",
"last_online",
}
for _, v := range fields {
if val, ok := requestData.Data[v]; ok {
record.Set(v, val)
}
}
// apply to the db
if err := dao.SaveRecord(record); err != nil {
return err
}
return c.JSON(http.StatusOK, record)
},
apis.ActivityLogger(app),
)
e.Router.GET("/device/:device_id", func(c echo.Context) error {
record, err := app.Dao().FindFirstRecordByData("Device", "device_id", c.PathParam("device_id"))
if err != nil {
return apis.NewNotFoundError("The device does not exist.", err)
}
// enable ?expand query param support
apis.EnrichRecord(c, app.Dao(), record)
return c.JSON(http.StatusOK, record)
},
apis.ActivityLogger(app),
)
// gets all relevant tables for this device id
e.Router.GET("/state/device/:device_id", func(c echo.Context) error {
record, err := app.Dao().FindFirstRecordByData("Device", "device_id", c.PathParam("device_id"))
if err != nil {
return apis.NewNotFoundError("The device does not exist.", err)
}
room, _ := app.Dao().FindFirstRecordByData("DataBlock", "block_id", record.GetString("current_room"))
output := map[string]interface{}{
"device": record,
"room": room,
}
return c.JSON(http.StatusOK, output)
},
apis.ActivityLogger(app),
)
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
func mergeDataBlock(requestData *models.RequestData, record *models.Record) {
// get the new data
newData, hasNewData := requestData.Data["data"]
if hasNewData {
// convert the existing data to a map
var newDataMap = map[string]interface{}{}
for k, v := range newData.(map[string]interface{}) {
newDataMap[k] = v
}
// get the existing data
existingDataString := record.GetString("data")
existingDataMap := map[string]interface{}{}
json.Unmarshal([]byte(existingDataString), &existingDataMap)
// merge the new keys
// this is only single-level
for k, v := range newDataMap {
existingDataMap[k] = v
}
record.Set("data", existingDataMap)
}
}

View File

@ -1,44 +0,0 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from routes.api import router as api_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 = [
"http://velconnect.ugavel.com",
"https://velconnect.ugavel.com",
"http://localhost",
"http://localhost:8080",
"http://localhost:8000",
"http://localhost:8005",
"http://localhost:5173",
"https://convrged.ugavel.com",
"http://convrged.ugavel.com",
"https://healxr.ugavel.com"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
app.mount("/static", StaticFiles(directory="static"), name="static")
app.include_router(api_router)
app.include_router(user_count_router)
app.include_router(oculus_api_router)
app.include_router(website_router)
if __name__ == '__main__':
uvicorn.run("main:app", host='127.0.0.1', port=8005,
log_level="info", reload=True)
print("running")

View File

@ -0,0 +1,30 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(db dbx.Builder) error {
// set default app settings
dao := daos.New(db)
settings, _ := dao.FindSettings()
settings.Meta.AppName = "VEL-Connect"
settings.Smtp.Enabled = false
settings.Logs.MaxDays = 60
settings.Backups.Cron = "0 0 * * 0"
settings.Backups.CronMaxKeep = 10
if err := dao.SaveSettings(settings); err != nil {
return err
}
return nil
}, func(db dbx.Builder) error {
// add down queries...
return nil
})
}

View File

@ -0,0 +1,423 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "_pb_users_auth_",
"created": "2023-07-06 22:29:02.843Z",
"updated": "2023-07-06 22:29:02.844Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
},
{
"id": "ve85cwsj7syqvxu",
"created": "2023-07-06 23:08:13.962Z",
"updated": "2023-07-06 23:08:13.962Z",
"name": "UserCount",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "pnhtdbcx",
"name": "app_id",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "wkf3zyyb",
"name": "room_id",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "f7k9hdoc",
"name": "total_users",
"type": "number",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
},
{
"system": false,
"id": "uevek8os",
"name": "room_users",
"type": "number",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
},
{
"system": false,
"id": "coilxuep",
"name": "version",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "zee0a2yb",
"name": "platform",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "fupstz47c55s69f",
"created": "2023-07-06 23:10:31.321Z",
"updated": "2023-07-06 23:12:35.555Z",
"name": "Device",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "1tkrnxqf",
"name": "os_info",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "knspamfx",
"name": "friendly_name",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "qfalwg3c",
"name": "modified_by",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "x0zlup7v",
"name": "current_app",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vpzen2th",
"name": "current_room",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "d0ckgjhm",
"name": "pairing_code",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "hglbl7da",
"name": "last_online",
"type": "date",
"required": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "pphfrekz",
"name": "data",
"type": "json",
"required": false,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "3qwwkz4wb0lyi78",
"created": "2023-07-06 23:12:11.113Z",
"updated": "2023-07-06 23:12:11.113Z",
"name": "DataBlock",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "2j8ydmzp",
"name": "owner_id",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "s12duaes",
"name": "visibility",
"type": "select",
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"public",
"private",
"unlisted"
]
}
},
{
"system": false,
"id": "wbifl8pv",
"name": "category",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "5a3nwg7m",
"name": "modified_by",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "mkzyfsng",
"name": "data",
"type": "json",
"required": false,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "ejjwc6vs7mfpyck",
"created": "2023-07-06 23:16:00.484Z",
"updated": "2023-07-06 23:16:00.484Z",
"name": "UserDevice",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "7qflf3o6",
"name": "user_id",
"type": "relation",
"required": true,
"unique": false,
"options": {
"collectionId": "_pb_users_auth_",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": []
}
},
{
"system": false,
"id": "l7xsjmop",
"name": "device_id",
"type": "relation",
"required": true,
"unique": false,
"options": {
"collectionId": "fupstz47c55s69f",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": []
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

View File

@ -0,0 +1,58 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("fupstz47c55s69f")
if err != nil {
return err
}
json.Unmarshal([]byte(`[
"CREATE UNIQUE INDEX ` + "`" + `idx_jgyX3xA` + "`" + ` ON ` + "`" + `Device` + "`" + ` (` + "`" + `device_id` + "`" + `)"
]`), &collection.Indexes)
// add
new_device_id := &schema.SchemaField{}
json.Unmarshal([]byte(`{
"system": false,
"id": "vjzi0uvv",
"name": "device_id",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}`), new_device_id)
collection.Schema.AddField(new_device_id)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("fupstz47c55s69f")
if err != nil {
return err
}
json.Unmarshal([]byte(`[]`), &collection.Indexes)
// remove
collection.Schema.RemoveField("vjzi0uvv")
return dao.SaveCollection(collection)
})
}

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
fastapi
autopep8
uvicorn
pymysql
pyppeteer
jinja2
python-multipart
aiofiles

View File

@ -1,519 +0,0 @@
import os
import secrets
import json
import string
import aiofiles
import uuid
import fastapi
from fastapi.responses import HTMLResponse, FileResponse
from fastapi import FastAPI, File, UploadFile, Response, Request, status
from enum import Enum
import db
db = db.DB("db/velconnect.db")
# APIRouter creates path operations for user module
router = fastapi.APIRouter(
prefix="/api",
tags=["API"],
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 = "/static/img/velconnect_logo_1.png" style="width:10em; margin: 2em auto;" />
</div>
</rapi-doc>
</body>
</html>
"""
def parse_data(device: dict):
if "data" in device and device["data"] is not None and len(device["data"]) > 0:
device["data"] = json.loads(device["data"])
@router.get("/get_all_users")
def get_all_users():
"""Returns a list of all devices and details associated with them."""
values = db.query("SELECT * FROM `User`;")
values = [dict(v) for v in values]
for v in values:
parse_data(v)
return values
@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`;")
values = [dict(v) for v in values]
for device in values:
parse_data(device)
return values
@router.get("/get_user_by_pairing_code/{pairing_code}")
def get_user_by_pairing_code(pairing_code: str, response: Response):
device = get_device_by_pairing_code_dict(pairing_code)
if device is not None:
return device
response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "User not found"}
@router.get("/get_device_by_pairing_code/{pairing_code}")
def get_device_by_pairing_code(pairing_code: str, response: Response):
device = get_device_by_pairing_code_dict(pairing_code)
if device is not None:
return device
response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "Device not found"}
def get_device_by_pairing_code_dict(pairing_code: str) -> dict | None:
values = db.query(
"SELECT * FROM `Device` WHERE `pairing_code`=:pairing_code;",
{"pairing_code": pairing_code},
)
if len(values) == 1:
device = dict(values[0])
parse_data(device)
return device
return None
def get_user_for_device(hw_id: str) -> dict:
values = db.query(
"""SELECT * FROM `UserDevice` WHERE `hw_id`=:hw_id;""", {"hw_id": hw_id}
)
if len(values) == 1:
user_id = dict(values[0])["user_id"]
user = get_user_dict(user_id=user_id)
else:
# create new user instead
user = create_user(hw_id)
parse_data(user)
return user
# creates a user with a device autoattached
def create_user(hw_id: str) -> dict | None:
user_id = str(uuid.uuid4())
if not db.insert(
"""INSERT INTO `User`(id) VALUES (:user_id);""", {"user_id": user_id}
):
return None
if not db.insert(
"""INSERT INTO `UserDevice`(user_id, hw_id) VALUES (:user_id, :hw_id); """,
{"user_id": user_id, "hw_id": hw_id},
):
return None
return get_user_for_device(hw_id)
def create_device(hw_id: str):
db.insert(
"""
INSERT OR IGNORE INTO `Device`(hw_id) VALUES (:hw_id);
""",
{"hw_id": hw_id},
)
@router.get("/device/get_data/{hw_id}")
def get_device_data(request: Request, response: Response, 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:
response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "Can't find device with that id."}
block = dict(devices[0])
if "data" in block and block["data"] is not None:
block["data"] = json.loads(block["data"])
user = get_user_for_device(hw_id)
room_key: str = f"{devices[0]['current_app']}_{devices[0]['current_room']}"
room_data = get_data(response, key=room_key, user_id=user["id"])
if "error" in room_data:
response.status_code = (
None # this really isn't an error, so we reset the status code
)
set_data(request, data={}, key=room_key, modified_by=None, category="room")
room_data = get_data(response, key=room_key, user_id=user["id"])
return {"device": block, "room": room_data, "user": user}
@router.post("/device/set_data/{hw_id}")
def set_device_data(
request: fastapi.Request, hw_id: str, data: dict, modified_by: str = None
):
"""Sets the device state"""
create_device(hw_id)
# add the client's IP address if no sender specified
if "modified_by" in data:
modified_by = data["modified_by"]
if modified_by is None:
modified_by: str = str(request.client) + "_" + str(request.headers)
allowed_keys: list[str] = [
"os_info",
"friendly_name",
"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,
modified_by=:modified_by
WHERE `hw_id`=:hw_id;
""",
{"value": data[key], "hw_id": hw_id, "modified_by": modified_by},
)
if key == "data":
new_data = data["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 = {}
if old_data_query[0]["data"] is not None:
old_data = json.loads(old_data_query[0]["data"])
new_data = {**old_data, **new_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(new_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)
)
class Visibility(str, Enum):
public = "public"
private = "private"
unlisted = "unlisted"
@router.post("/set_data")
def set_data_with_random_key(
request: fastapi.Request,
data: dict,
owner: str,
modified_by: str = None,
category: str = None,
visibility: Visibility = Visibility.public,
) -> dict:
"""Creates a little storage bucket for arbitrary data with a random key"""
return set_data(request, data, None, owner, modified_by, category, visibility)
@router.post("/set_data/{key}")
def set_data(
request: fastapi.Request,
data: dict,
key: str = None,
owner: str = "none",
modified_by: str = None,
category: str = None,
visibility: Visibility = Visibility.public,
) -> dict:
"""Creates a little storage bucket for arbitrary data"""
# sqlite composite key isn't necessarily unique if a value is null
if owner == None:
owner = "none"
# add the client's IP address if no sender specified
if "modified_by" in data:
modified_by = data["modified_by"]
if modified_by is None:
modified_by: str = 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(
"""
UPDATE `DataBlock` SET
category = :category,
modified_by = :modified_by,
data = :data,
last_modified = CURRENT_TIMESTAMP
WHERE id=:id AND owner_id = :owner_id;
""",
{
"id": key,
"category": category,
"modified_by": modified_by,
"owner_id": owner,
"data": json.dumps(data),
},
)
else:
# add the data to the db
db.insert(
"""
INSERT INTO `DataBlock` (id, owner_id, category, modified_by, data, last_modified)
VALUES(:id, :owner_id, :category, :modified_by, :data, CURRENT_TIMESTAMP);
""",
{
"id": key,
"owner_id": owner,
"category": category,
"modified_by": modified_by,
"data": json.dumps(data),
},
)
return {"key": key}
@router.get("/get_data/{key}")
def get_data(response: Response, key: str, user_id: str = None) -> dict:
"""Gets data from a storage bucket for arbitrary data"""
data = db.query(
"""
SELECT *
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:
block = dict(data[0])
if "data" in block and block["data"] is not None:
block["data"] = json.loads(block["data"])
if not has_permission(block, user_id):
response.status_code = status.HTTP_401_UNAUTHORIZED
return {"error": "Not authorized to see that data."}
replace_userid_with_name(block)
return block
response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "Not found"}
except Exception as e:
print(e)
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return {"error": "Unknown. Maybe no data at this key."}
def has_permission(data_block: dict, user_uuid: str) -> bool:
# if the data is public by visibility
if (
data_block["visibility"] == Visibility.public
or data_block["visibility"] == Visibility.unlisted
):
return True
# public domain data
elif data_block["owner_id"] is None:
return True
# if we are the owner
elif data_block["owner_id"] == user_uuid:
return True
else:
return False
def replace_userid_with_name(data_block: dict):
if data_block["owner_id"] is not None:
user = get_user_dict(data_block["owner_id"])
if user is not None:
data_block["owner_name"] = user["username"]
del data_block["owner_id"]
@router.get("/user/get_data/{user_id}")
def get_user(response: Response, user_id: str):
user = get_user_dict(user_id)
if user is None:
response.status_code = status.HTTP_404_BAD_REQUEST
return {"error": "User not found"}
return user
def get_user_dict(user_id: str) -> dict | None:
values = db.query("SELECT * FROM `User` WHERE `id`=:user_id;", {"user_id": user_id})
if len(values) == 1:
user = dict(values[0])
parse_data(user)
return user
return None
@router.post("/upload_file")
async def upload_file_with_random_key(
request: fastapi.Request, file: UploadFile, modified_by: str = None
):
return await upload_file(request, file, None, modified_by)
@router.post("/upload_file/{key}")
async def upload_file(
request: fastapi.Request, file: UploadFile, key: str | None, modified_by: str = None
):
if not os.path.exists("data"):
os.makedirs("data")
# 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()
async with aiofiles.open("data/" + key, "wb") as out_file:
content = await file.read() # async read
await out_file.write(content) # async write
# add a datablock to link to the file
set_data(
request,
data={"filename": file.filename},
key=key,
category="file",
modified_by=modified_by,
)
return {"filename": file.filename, "key": key}
@router.get("/download_file/{key}")
async def download_file(response: Response, key: str):
# get the relevant datablock
data = get_data(response, key)
print(data)
if response.status_code == status.HTTP_404_NOT_FOUND:
return "Not found"
if data["category"] != "file":
response.status_code = status.HTTP_400_BAD_REQUEST
return "Not a file"
return FileResponse(path="data/" + key, filename=data["data"]["filename"])
@router.get("/get_all_files")
async def get_all_files():
data = db.query(
"""
SELECT *
FROM `DataBlock`
WHERE visibility='public' AND category='file';
"""
)
data = [dict(f) for f in data]
for f in data:
parse_data(f)
return data
@router.get("/get_all_images")
async def get_all_images():
data = db.query(
"""
SELECT *
FROM `DataBlock`
WHERE visibility='public' AND category='file';
"""
)
data = [dict(f) for f in data]
for f in data:
parse_data(f)
images = []
for f in data:
if f["data"]["filename"].endswith(".png") or f["data"]["filename"].endswith(
".jpg"
):
images.append({"key": f["id"], "filename": f["data"]["filename"]})
return images

View File

@ -1,52 +0,0 @@
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

@ -1,68 +0,0 @@
import fastapi
import db
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,38 +0,0 @@
import fastapi
from fastapi import APIRouter
from fastapi.responses import FileResponse
from fastapi.templating import Jinja2Templates
# APIRouter creates path operations for user module
router = APIRouter(
prefix="",
tags=["Website"],
include_in_schema=False
)
templates = Jinja2Templates(directory="templates")
@router.get('/')
def index():
return FileResponse("templates/index.html")
@router.get('/pair')
def pair():
return FileResponse("templates/pair.html")
@router.get('/success')
def success():
return FileResponse("templates/success.html")
@router.get('/failure')
def failure(request: fastapi.Request, code: int = 0):
return templates.TemplateResponse("failure.html", {"request": request, "code": code})
@router.get('/join/{app_id}/{link}')
def join(request: fastapi.Request, app_id: str, link: str):
return templates.TemplateResponse("join.html", {"request": request, "app_id": app_id, "link": link})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,34 +0,0 @@
{
// check cookie
let hw_id = getCookie('hw_id');
if (hw_id !== "" && hw_id !== undefined && hw_id !== "undefined") {
httpGetAsync('/api/device/get_data/' + hw_id, (resp) => {
console.log(resp);
let respData = JSON.parse(resp);
if ("error" in respData) {
window.location.href = "/pair";
}
writeClass('hw_id', respData['device']['hw_id']);
writeClass('pairing_code', respData['device']['pairing_code']);
writeValue('current_app', respData['device']['current_app']);
writeValue('current_room', respData['device']['current_room']);
writeClass('date_created', respData['device']['date_created'] + "<br>" + timeSinceString(respData['device']['date_created']) + " ago");
writeClass('last_modified', respData['device']['last_modified'] + "<br>" + timeSinceString(respData['device']['last_modified']) + " ago");
writeValue('user_name', respData['device']['friendly_name']);
writeValue('avatar_url', respData['device']['data']?.['avatar_url']);
writeValue('tv_url', respData['room']?.['data']?.['tv_url']);
writeValue('carpet_color', respData['room']?.['data']?.['carpet_color']);
if (carpet_color) carpet_color.parentElement.style.color = "" + respData['room']?.['data']?.['carpet_color'];
loading.style.display = "none";
headset_details.style.display = "block";
}, (status) => {
loading.style.display = "none";
failure.style.display = "block";
});
}
}

View File

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

View File

@ -1,48 +0,0 @@
function setDeviceField(device) {
let hw_id = getCookie('hw_id');
fetch('/api/device/set_data/' + hw_id, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(device)
})
.then(_ => console.log('success'))
.catch(_ => console.log('fail'));
}
function setDeviceData(data, successCallback, failureCallback) {
let hw_id = getCookie('hw_id');
fetch('/api/device/set_data/' + hw_id, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({"data": data})
})
.then(_ => {
console.log('success');
successCallback?.();
})
.catch(_ => {
console.log('fail');
failureCallback?.();
});
}
function setRoomData(data) {
fetch('/api/set_data/' + current_app.value + "_" + current_room.value, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(_ => console.log('success'))
.catch(_ => console.log('fail'));
}

View File

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

View File

@ -1,45 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Failure</title>
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<style>
body {
background-color: #333;
font-size: 1.2em;
}
.centered {
margin: 8em auto;
width: max-content;
font-family: arial, sans-serif;
color: #ddd;
}
</style>
</head>
<body>
<div class="centered">
🤮 FAIL 🤡
</div>
<div class="centered">
{% if code==1 %}
<p>Pairing code not recognized. Go back and try again.</p>
{% else %}
<p>Unknown error</p>
{% endif %}
</div>
</body>
</html>

View File

@ -1,523 +0,0 @@
<html lang="en">
<head>
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<title>VEL Connect</title>
<link rel="stylesheet" type="text/css" href="/static/css/spectre.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/spectre-exp.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/spectre-icons.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/coloris.min.css">
<script src="/static/js/util.js"></script>
<style>
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
.centered {
margin: auto;
}
hr {
color: #0004;
}
</style>
</head>
<body>
<div class="container">
<div id="loading"><br><br>
<div class="loading loading-lg"></div>
</div>
<div id="failure" style="display: none;"><br><br><br>☹️</div>
<div id="headset_details" style="display: none;">
<div class="panel card">
<div class="card-image">
<img id="cover_image" class="img-responsive" src="/static/img/cover_default.png">
</div>
<div class="panel-header text-center" style="overflow:hidden">
<figure class="avatar avatar-lg" style="background: none;"><img
src="/static/img/velconnect_logo_1_square.webp" alt="Avatar"></figure>
<div class="panel-title h5 mt-10">Headset ID:</div>
<code class="panel-subtitle hw_id">---</code>
<br>
<br>
<div class="container">
<div class="columns">
<div class="col-6">
<div class="panel-title h5 mt-10">Pairing Code:</div>
<code class="panel-subtitle pairing_code">---</code>
</div>
<div class="col-6">
<a href="/pair">
<button class="btn btn-primary btn-lg tooltip tooltip-right" id="pair_new"
data-tooltip="Clear this headset and pair a new headset">
Pair New
</button>
</a>
</div>
</div>
</div>
<hr>
</div>
<div class="panel-body">
<div class="container">
<div class="columns">
<div class="tile tile-centered col-6">
<div class="tile-content">
<div class="tile-title text-bold">First Seen</div>
<div class="tile-subtitle date_created">---</div>
</div>
</div>
<div class="tile tile-centered col-6">
<div class="tile-content">
<div class="tile-title text-bold">Last Modified</div>
<div class="tile-subtitle last_modified">---</div>
</div>
</div>
</div>
</div>
<br>
<div class="divider text-center" data-content="User Settings"></div>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">User Name</div>
<input class="btn user_name" type="text" id="user_name" placeholder="----">
<button class="btn btn-primary btn-lg tooltip tooltip-right" id="set_user_name"
data-tooltip="Set Username">Set
</button>
</div>
</div>
<!-- <br>-->
<!-- <div class="tile tile-centered">-->
<!-- <div class="tile-content">-->
<!-- <div class="tile-title text-bold">TV URL</div>-->
<!-- <input class="btn tv_url" type="text" id="tv_url" placeholder="&#45;&#45;&#45;&#45;">-->
<!-- <button class="btn btn-primary btn-lg tooltip tooltip-right" id="set_tv_url"-->
<!-- data-tooltip="">Set</button>-->
<!-- </div>-->
<!-- <div class="tile-action">-->
<!-- </div>-->
<!-- </div>-->
<!-- <br>-->
<!-- <div class="tile tile-centered">-->
<!-- <div class="tile-content">-->
<!-- <div class="tile-title text-bold">User Color</div>-->
<!-- <input class="btn user_color coloris" type="text" id="user_color" placeholder="#ffffff">-->
<!-- <button class="btn btn-primary btn-lg tooltip tooltip-right" id="set_user_color"-->
<!-- data-tooltip="Set User Color">Set</button>-->
<!-- </div>-->
<!-- <div class="tile-action">-->
<!-- </div>-->
<!-- </div>-->
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Avatar URL</div>
<div class="tile-subtitle">
<a href="https://convrged.readyplayer.me" target="blank">
Create New Avatar
<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"/>
</svg>
</a>
</div>
<input class="btn avatar_url" type="text" id="avatar_url" placeholder="----">
<button class="btn btn-primary btn-lg tooltip tooltip-right" id="set_avatar_url"
data-tooltip="Set Avatar URL">Set
</button>
</div>
<div class="tile-action">
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Upload File</div>
<input class="btn upload_file" type="file" id="upload_file">
<button class="btn btn-primary btn-lg" id="upload_file_button">Upload
</button>
</div>
<div class="tile-action">
</div>
</div>
<br>
<br>
<div class="divider text-center" data-content="Room Settings"></div>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Current Room</div>
<div class="tile-subtitle">
<a id="shareable_link" href="" target="blank">
Shareable Link
<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"/>
</svg>
</a>
</div>
<input class="btn current_room" type="text" id="current_room" placeholder="----">
<input style="display: none;" class="btn current_app" type="text" id="current_app"
placeholder="----">
<button class="btn btn-primary btn-lg tooltip tooltip-right" id="set_room_id"
data-tooltip="Set Room ID">Set
</button>
</div>
</div>
<br>
<!-- <div class="tile tile-centered">-->
<!-- <div class="tile-content">-->
<!-- <div class="tile-title text-bold">Carpet Color</div>-->
<!-- <input class="btn carpet_color coloris" type="text" id="carpet_color" placeholder="#ffffff">-->
<!-- <button class="btn btn-primary btn-lg tooltip tooltip-right" id="set_carpet_color"-->
<!-- data-tooltip="Set Carpet Color">Set-->
<!-- </button>-->
<!-- </div>-->
<!-- <div class="tile-action">-->
<!-- </div>-->
<!-- </div>-->
<!-- <br>-->
<div class="divider text-center" data-content="Screen Sharing"></div>
<div>
<div>
<div class="flex flex-col h-screen relative">
<header class="flex h-16 justify-center items-center text-xl bg-black text-white">
<div class="columns">
<button id="bnt_pubcam" class="btn btn-secondary btn-lg col-6"
onclick="start(true)">Publish Camera
</button>
<button id="bnt_pubscreen" class="btn btn-secondary btn-lg col-6"
onclick="start(false)">Publish Screen
</button>
</div>
</header>
<video style="width: 100%;" id="pub_video" class="bg-black" controls></video>
</div>
<script src="https://unpkg.com/ion-sdk-js@1.5.5/dist/ion-sdk.min.js"></script>
<script src="https://unpkg.com/ion-sdk-js@1.5.5/dist/json-rpc.min.js"></script>
</div>
<p>For more screen-sharing options and remote control, 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"/>
</svg>
</a></p>
</div>
<br>
</div>
</div>
</div>
<script type="text/javascript" src="/static/js/coloris.min.js"></script>
<script>
let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code');
let loading = document.getElementById('loading');
let enter_pairing_id = document.getElementById('enter_pairing_id');
let headset_details = document.getElementById('headset_details');
let hw_id_field = document.getElementById('hw_id');
let failure = document.getElementById('failure');
let current_app = document.getElementById('current_app');
let current_room = document.getElementById('current_room');
let set_room_id = document.getElementById('set_room_id');
let set_user_color = document.getElementById('set_user_color');
let user_color = document.getElementById('user_color');
let carpet_color = document.getElementById('carpet_color');
let set_user_name = document.getElementById('set_user_name');
let set_tv_url = document.getElementById('set_tv_url');
let set_carpet_color = document.getElementById('set_carpet_color');
let set_avatar_url = document.getElementById('set_avatar_url');
let upload_file_button = document.getElementById('upload_file_button');
// check cookie
let hw_id = getCookie('hw_id');
if (hw_id !== "" && hw_id !== undefined && hw_id !== "undefined") {
httpGetAsync('/api/device/get_data/' + hw_id, (resp) => {
console.log(resp);
let respData = JSON.parse(resp);
if ("error" in respData) {
window.location.href = "/pair";
}
writeClass('hw_id', respData['device']['hw_id']);
writeClass('pairing_code', respData['device']['pairing_code']);
writeValue('current_app', respData['device']['current_app']);
writeValue('current_room', respData['device']['current_room']);
writeClass('date_created', respData['device']['date_created'] + "<br>" + timeSinceString(respData['device']['date_created']) + " ago");
writeClass('last_modified', respData['device']['last_modified'] + "<br>" + timeSinceString(respData['device']['last_modified']) + " ago");
writeValue('user_name', respData['device']['friendly_name']);
writeValue('avatar_url', respData['device']['data']?.['avatar_url']);
writeValue('tv_url', respData['room']?.['data']?.['tv_url']);
writeValue('carpet_color', respData['room']?.['data']?.['carpet_color']);
if (carpet_color) carpet_color.parentElement.style.color = "" + respData['room']?.['data']?.['carpet_color'];
Coloris({
el: '.coloris',
swatches: [
'#264653',
'#2a9d8f',
'#e9c46a',
'#f4a261',
'#e76f51',
'#d62828',
'#023e8a',
'#0077b6',
'#0096c7',
'#00b4d8',
'#48cae4',
]
});
if (respData['device']['current_app']) {
document.getElementById('cover_image').src = `/static/img/cover_${respData['device']['current_app']}.png`
document.getElementById('shareable_link').href = `/join/${respData['device']['current_app']}/${respData['device']['current_room']}`
}
loading.style.display = "none";
headset_details.style.display = "block";
}, (status) => {
loading.style.display = "none";
failure.style.display = "block";
window.location.href = "/pair";
});
function setDeviceField(data) {
fetch('/api/device/set_data/' + hw_id, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(_ => console.log('success'))
.catch(_ => console.log('fail'));
}
function setDeviceData(data) {
fetch('/api/device/set_data/' + hw_id, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({"data": data})
})
.then(_ => console.log('success'))
.catch(_ => console.log('fail'));
}
function setRoomData(data) {
fetch('/api/set_data/' + current_app.value + "_" + current_room.value, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(_ => console.log('success'))
.catch(_ => console.log('fail'));
}
function uploadFile(file) {
let formData = new FormData();
formData.append("file", file);
fetch('/api/upload_file', {
method: 'POST',
body: formData
})
.then(_ => console.log('success'))
.catch(_ => console.log('fail'));
}
if (set_room_id) {
set_room_id.addEventListener('click', () => {
setDeviceField({"current_room": current_room.value});
});
}
if (set_user_color) {
set_user_color.addEventListener('click', () => {
setDeviceData({"user_color": document.getElementById('user_color').value});
});
}
if (set_user_name) {
set_user_name.addEventListener('click', () => {
setDeviceField({"friendly_name": document.getElementById('user_name').value});
});
}
if (set_tv_url) {
set_tv_url.addEventListener('click', () => {
setRoomData({"tv_url": document.getElementById('tv_url').value});
});
}
if (set_carpet_color) {
set_carpet_color.addEventListener('click', () => {
setRoomData({"carpet_color": document.getElementById('carpet_color').value});
});
}
if (set_avatar_url) {
set_avatar_url.addEventListener('click', () => {
setDeviceData({"avatar_url": document.getElementById('avatar_url').value});
});
}
if (upload_file_button) {
upload_file_button.addEventListener('click', () => {
uploadFile(document.getElementById('upload_file').files[0]);
});
}
} else {
window.location.href = "/pair";
}
Coloris({
el: '.coloris',
swatches: [
'#264653',
'#2a9d8f',
'#e9c46a',
'#f4a261',
'#e76f51',
'#d62828',
'#023e8a',
'#0077b6',
'#0096c7',
'#00b4d8',
'#48cae4',
]
});
const roomName = (Math.random() + 1).toString(36).substring(7);
const pubVideo = document.getElementById("pub_video");
const subVideo = document.getElementById("sub_video");
const bntPubCam = document.getElementById("bnt_pubcam");
const bntPubScreen = document.getElementById("bnt_pubscreen");
// setDeviceData({"streamer_stream_id": roomName});
const serverURL = "wss://velnet.ugavel.com/ws";
const config = {
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
};
const signalLocal = new Signal.IonSFUJSONRPCSignal(serverURL);
const clientLocal = new IonSDK.Client(signalLocal, config);
signalLocal.onopen = () => clientLocal.join(roomName);
const start = (type) => {
if (type) {
IonSDK.LocalStream.getUserMedia({
resolution: "vga",
audio: true,
video: true,
codec: "vp8",
}).then((media) => {
pubVideo.srcObject = media;
pubVideo.autoplay = true;
pubVideo.controls = true;
pubVideo.muted = true;
bntPubCam.disabled = true;
bntPubScreen.disabled = true;
clientLocal.publish(media);
}).catch(console.error);
} else {
IonSDK.LocalStream.getDisplayMedia({
audio: true,
video: true,
codec: "vp8",
}).then((media) => {
pubVideo.srcObject = media;
pubVideo.autoplay = true;
pubVideo.controls = true;
pubVideo.muted = true;
bntPubCam.disabled = true;
bntPubScreen.disabled = true;
clientLocal.publish(media);
}).catch(console.error);
}
}
clientLocal.ontrack = (track, stream) => {
console.log("got track: ", track.id, "for stream: ", stream.id);
stream.mute();
stream.unmute();
if (track.kind === "video") {
subVideo.srcObject = stream;
subVideo.play();
}
//track.onunmute = () => {
//subVideo.srcObject = stream;
//subVideo.autoplay = true;
//subVideo.muted = true;
//subVideo.play();
//stream.onremovetrack = () => {
//subVideo.srcObject = null;
//}
//}
}
</script>
</div>
</body>
</html>

View File

@ -1,170 +0,0 @@
<html>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<title>VEL Connect</title>
<link rel="stylesheet" type="text/css" href="/static/css/spectre.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/spectre-exp.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/spectre-icons.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/coloris.min.css">
<script type="application/javascript" src="/static/js/util.js"></script>
<style>
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
.centered {
margin: auto;
}
hr {
color: #0004;
}
</style>
</head>
<body>
<div class="container">
<div id="loading"><br><br>
<div class="loading loading-lg"></div>
</div>
<div id="failure" style="display: none;"><br><br><br>☹️</div>
<div id="headset_details" style="display: none;">
<div class="panel card">
<div class="card-image">
<img id="cover_image" class="img-responsive" src="/static/img/cover_default.png">
</div>
<div class="panel-header text-center">
<figure class="avatar avatar-lg" style="background: none;"><img
src="/static/favicons/android-chrome-192x192.png" alt="Avatar"></figure>
<div class="panel-title h5 mt-10">Headset ID:</div>
<code class="panel-subtitle hw_id">---</code>
<br>
<br>
<div class="container">
<div class="columns">
<div class="col-6">
<div class="panel-title h5 mt-10">Pairing Code:</div>
<code class="panel-subtitle pairing_code">---</code>
</div>
<div class="col-6">
<a href="/pair">
<button class="btn btn-primary btn-lg tooltip tooltip-right" id="pair_new"
data-tooltip="Clear this headset and pair a new headset">Pair New
</button>
</a>
</div>
</div>
</div>
<hr>
</div>
<div class="panel-body">
<div class="container">
<div class="columns">
<div class="tile tile-centered col-6">
<div class="tile-content">
<div class="tile-title text-bold">First Seen</div>
<div class="tile-subtitle date_created">---</div>
</div>
</div>
<div class="tile tile-centered col-6">
<div class="tile-content">
<div class="tile-title text-bold">Last Modified</div>
<div class="tile-subtitle last_modified">---</div>
</div>
</div>
</div>
</div>
<br>
<div class="divider text-center" data-content="Join Link"></div>
<div style="display: none;" class="text-center" id="join_success">
<h2>Success!</h2>
<p>Your device will now join the room <strong>{{ link }}</strong> when you launch {{ app_id }}.</p>
</div>
<div style="display: none;" class="text-center" id="join_failure">
<h2>FAIL!</h2>
<p>Something went wrong sending the join request to your device.</p>
</div>
<br>
</div>
</div>
</div>
<script type="application/javascript" src="/static/js/coloris.min.js"></script>
<script type="application/javascript" src="/static/js/device_details.js"></script>
<script type="application/javascript" src="/static/js/velconnect_util.js"></script>
<script>
let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code');
let loading = document.getElementById('loading');
let enter_pairing_id = document.getElementById('enter_pairing_id');
let headset_details = document.getElementById('headset_details');
let hw_id_field = document.getElementById('hw_id');
let failure = document.getElementById('failure');
let current_app = document.getElementById('current_app');
let current_room = document.getElementById('current_room');
let set_room_id = document.getElementById('set_room_id');
let set_user_color = document.getElementById('set_user_color');
let user_color = document.getElementById('user_color');
let carpet_color = document.getElementById('carpet_color');
let set_user_name = document.getElementById('set_user_name');
let set_tv_url = document.getElementById('set_tv_url');
let set_carpet_color = document.getElementById('set_carpet_color');
let set_avatar_url = document.getElementById('set_avatar_url');
// check cookie
let hw_id = getCookie('hw_id');
if (hw_id !== "" && hw_id !== undefined && hw_id !== "undefined") {
document.getElementById('cover_image').src = `/static/img/cover_{{app_id}}.png`
setDeviceData({
"join_room_request_{{app_id}}": "{{link}}"
}, () => {
document.getElementById("join_success").style.display = "block";
}, () => {
document.getElementById("join_failure").style.display = "block";
});
} else {
window.location.href = "/pair";
}
</script>
</div>
</body>
</html>

View File

@ -1,110 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<title>VEL Connect | Pair</title>
<link rel="stylesheet" href="/static/css/spectre.min.css">
<script src="/static/js/util.js"></script>
<style>
:root {
--primary-color: #bc1f2d;
}
#pair_code {
max-width: 4em;
}
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
#submit_pairing_code {
cursor: pointer;
user-select: unset;
}
.centered {
margin: auto;
}
</style>
</head>
<body>
<div class="container">
<!-- <div class="hero bg-gray">
<div class="hero-body">
<h1>Pair your headset.</h1>
</div>
</div> -->
<div class="card">
<div class="card-image">
<img src="/static/img/pair_code_screenshot.png" class="img-responsive" alt="Pairing code location">
</div>
<div class="card-header">
<div class="card-title h5">Enter Pairing Code</div>
<div class="card-subtitle text-gray"></div>
</div>
<div class="card-body">
You can find the code in the bottom left of your menu tablet in conVRged.
</div>
<div class="card-footer centered">
<form id="pair_form">
<label for="pair_code"></label><input class="btn" type="text" id="pair_code" placeholder="0000">
<input class="btn btn-primary" id="submit_pairing_code" type="submit" value="Submit"/>
</form>
</div>
</div>
</div>
<script>
const pair_code_input = document.getElementById('pair_code');
document.getElementById('pair_form').addEventListener('submit', submitCode, true);
function submitCode(event) {
fetch('/api/get_device_by_pairing_code/' + pair_code_input.value)
.then(resp => resp.json())
.then(resp => {
if (resp.length === 2 && resp[1] === 400) {
window.location.href = "/failure?code=1";
console.error(resp);
} else {
if (resp['hw_id'] !== '') {
setCookie('hw_id', resp['hw_id'], 60);
window.location.href = "/";
}
}
}).catch(e => {
window.location.href = "/failure";
console.error(e);
});
event.preventDefault();
return false;
}
</script>
</body>
</html>

View File

@ -1,35 +0,0 @@
<html>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/static/favicon-16x16.png">
<link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<style>
body {
background-color: #333;
font-size: 1.2em;
}
.centered {
margin: 8em auto;
width: max-content;
font-family: arial, sans-serif;
color: #ddd;
}
</style>
</head>
<body>
<div class="centered">
🎉 SUCCESS 🎉
</div>
</body>
</html>