big things are happening to velconnect
|
|
@ -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
|
||||
|
||||
76
README.md
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Astro Starter Kit: Minimal
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template minimal
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
|
||||
[](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).
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 815 B After Width: | Height: | Size: 815 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 977 B After Width: | Height: | Size: 977 B |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 437 KiB After Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 437 KiB After Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="astro/client" />
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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`)
|
||||
);
|
||||
|
|
@ -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"]
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
MYSQL_DATABASE_DB = ''
|
||||
MYSQL_DATABASE_USER = ''
|
||||
MYSQL_DATABASE_HOST = ''
|
||||
MYSQL_DATABASE_PASSWORD = ''
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
docker build --tag velconnect .
|
||||
docker rm web
|
||||
docker run -p 8081:80 --name web velconnect
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
docker build --tag velconnect .
|
||||
docker rm web
|
||||
docker run -p 8081:80 --name web velconnect
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
docker build --tag velconnect .
|
||||
docker rm web
|
||||
docker run -p 8081:80 --name web velconnect
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fastapi
|
||||
autopep8
|
||||
uvicorn
|
||||
pymysql
|
||||
pyppeteer
|
||||
jinja2
|
||||
python-multipart
|
||||
aiofiles
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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})
|
||||
|
|
@ -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";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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="----">-->
|
||||
<!-- <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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||