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
|
||||||
|
|
||||||
96
README.md
|
|
@ -1,73 +1,69 @@
|
||||||
## 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
|
```sh
|
||||||
cd velconnect
|
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
|
```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
|
or
|
||||||
|
|
||||||
```sh
|
```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
|
## Set up systemctl service:
|
||||||
docker build --tag velconnect .
|
|
||||||
docker rm web
|
```ini
|
||||||
docker run -p 80:80 --name web velconnect
|
[Unit]
|
||||||
|
Description = velconnect
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
or run `./rebuild.sh`
|
- Enter the above in `/etc/systemd/system/velconnect.service`
|
||||||
|
- `sudo systemctl enable velconnect`
|
||||||
## Option 4: Run Python locally (WSL or native Linux)
|
- `sudo systemctl start velconnect`
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=VELConnect API
|
|
||||||
Requires=network.target
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[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
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
- Enter the above in `/etc/systemd/system/velconnect.service`
|
|
||||||
- `sudo systemctl enable velconnect`
|
|
||||||
- `sudo systemctl start velconnect`
|
|
||||||
|
|
|
||||||
|
|
@ -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.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Networking;
|
using UnityEngine.Networking;
|
||||||
|
|
@ -13,6 +14,7 @@ using VelNet;
|
||||||
|
|
||||||
namespace VELConnect
|
namespace VELConnect
|
||||||
{
|
{
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
public class VELConnectManager : MonoBehaviour
|
public class VELConnectManager : MonoBehaviour
|
||||||
{
|
{
|
||||||
public string velConnectUrl = "http://localhost";
|
public string velConnectUrl = "http://localhost";
|
||||||
|
|
@ -33,15 +35,17 @@ namespace VELConnect
|
||||||
|
|
||||||
public class Device
|
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 os_info;
|
||||||
public string friendly_name;
|
public string friendly_name;
|
||||||
public string modified_by;
|
public string modified_by;
|
||||||
public string current_app;
|
public string current_app;
|
||||||
public string current_room;
|
public string current_room;
|
||||||
public int pairing_code;
|
public string pairing_code;
|
||||||
public string date_created;
|
public DateTime last_online;
|
||||||
public string last_modified;
|
|
||||||
public Dictionary<string, string> data;
|
public Dictionary<string, string> data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -56,13 +60,14 @@ namespace VELConnect
|
||||||
|
|
||||||
public class RoomState
|
public class RoomState
|
||||||
{
|
{
|
||||||
public string error;
|
public readonly string id;
|
||||||
public 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 category;
|
||||||
public string date_created;
|
|
||||||
public string modified_by;
|
public string modified_by;
|
||||||
public string last_modified;
|
|
||||||
public string last_accessed;
|
|
||||||
public Dictionary<string, string> data;
|
public Dictionary<string, string> data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -75,17 +80,32 @@ namespace VELConnect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public User user;
|
public User user;
|
||||||
public Device device;
|
public Device device;
|
||||||
public RoomState room;
|
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 State lastState;
|
||||||
|
|
||||||
public static Action<State> OnInitialState;
|
public static Action<State> OnInitialState;
|
||||||
public static Action<string, string> OnDeviceFieldChanged;
|
public static Action<string, string> OnDeviceFieldChanged;
|
||||||
public static Action<string, string> OnDeviceDataChanged;
|
public static Action<string, object> OnDeviceDataChanged;
|
||||||
public static Action<string, string> OnRoomDataChanged;
|
public static Action<string, object> OnRoomDataChanged;
|
||||||
|
|
||||||
private static readonly Dictionary<string, List<CallbackListener>> deviceFieldCallbacks =
|
private static readonly Dictionary<string, List<CallbackListener>> deviceFieldCallbacks =
|
||||||
new Dictionary<string, List<CallbackListener>>();
|
new Dictionary<string, List<CallbackListener>>();
|
||||||
|
|
@ -111,7 +131,7 @@ namespace VELConnect
|
||||||
public bool sendInitialState;
|
public bool sendInitialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int PairingCode
|
public static string PairingCode
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|
@ -120,7 +140,7 @@ namespace VELConnect
|
||||||
// change once a day
|
// change once a day
|
||||||
hash.Append(DateTime.UtcNow.DayOfYear);
|
hash.Append(DateTime.UtcNow.DayOfYear);
|
||||||
// between 1000 and 9999 inclusive (any 4 digit number)
|
// 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
|
// Start is called before the first frame update
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
SetDeviceField(new Dictionary<string, object>
|
SetDeviceField(new State.Device
|
||||||
{
|
{
|
||||||
{ "current_app", Application.productName },
|
os_info = SystemInfo.operatingSystem,
|
||||||
{ "pairing_code", PairingCode },
|
friendly_name = SystemInfo.deviceName,
|
||||||
{ "friendly_name", SystemInfo.deviceName },
|
current_app = Application.productName,
|
||||||
|
pairing_code = PairingCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
UpdateUserCount();
|
UpdateUserCount();
|
||||||
|
|
||||||
|
|
||||||
StartCoroutine(SlowLoop());
|
StartCoroutine(SlowLoop());
|
||||||
|
|
||||||
VelNetManager.OnJoinedRoom += room =>
|
VelNetManager.OnJoinedRoom += room =>
|
||||||
{
|
{
|
||||||
SetDeviceField(new Dictionary<string, object>
|
SetDeviceField(new State.Device
|
||||||
{
|
{
|
||||||
{ "current_app", Application.productName },
|
current_app = Application.productName,
|
||||||
{ "current_room", room },
|
current_room = room,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -176,17 +196,20 @@ namespace VELConnect
|
||||||
|
|
||||||
VelNetManager.GetRooms(rooms =>
|
VelNetManager.GetRooms(rooms =>
|
||||||
{
|
{
|
||||||
Dictionary<string, object> postData = new Dictionary<string, object>
|
UserCount postData = new UserCount
|
||||||
{
|
{
|
||||||
{ "hw_id", DeviceId },
|
device_id = DeviceId,
|
||||||
{ "app_id", Application.productName },
|
app_id = Application.productName,
|
||||||
{ "room_id", VelNetManager.Room ?? "" },
|
room_id = VelNetManager.Room ?? "",
|
||||||
{ "total_users", rooms.rooms.Sum(r => r.numUsers) - (leaving ? 1 : 0) },
|
total_users = rooms.rooms.Sum(r => r.numUsers) - (leaving ? 1 : 0),
|
||||||
{ "room_users", VelNetManager.PlayerCount - (leaving ? 1 : 0) },
|
room_users = VelNetManager.PlayerCount - (leaving ? 1 : 0),
|
||||||
{ "version", Application.version },
|
version = Application.version,
|
||||||
{ "platform", SystemInfo.operatingSystem },
|
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
|
try
|
||||||
{
|
{
|
||||||
GetRequestCallback(velConnectUrl + "/api/device/get_data/" + DeviceId, json =>
|
GetRequestCallback(velConnectUrl + "/state/device/" + DeviceId, json =>
|
||||||
{
|
{
|
||||||
State state = JsonConvert.DeserializeObject<State>(json);
|
State state = JsonConvert.DeserializeObject<State>(json);
|
||||||
if (state == null) return;
|
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();
|
FieldInfo[] fields = state.device.GetType().GetFields();
|
||||||
|
|
||||||
|
// loop through all the fields in the device
|
||||||
foreach (FieldInfo fieldInfo in fields)
|
foreach (FieldInfo fieldInfo in fields)
|
||||||
{
|
{
|
||||||
string newValue = fieldInfo.GetValue(state.device) as string;
|
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)
|
foreach (KeyValuePair<string, string> elem in state.room.data)
|
||||||
{
|
{
|
||||||
|
|
@ -488,13 +513,30 @@ namespace VELConnect
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets data on the device keys themselves
|
/// Sets data on the device keys themselves
|
||||||
|
/// These are fixed fields defined for every application
|
||||||
/// </summary>
|
/// </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(
|
PostRequestCallback(
|
||||||
_instance.velConnectUrl + "/api/device/set_data/" + DeviceId,
|
_instance.velConnectUrl + "/device/" + DeviceId,
|
||||||
JsonConvert.SerializeObject(device),
|
JsonConvert.SerializeObject(device, Formatting.None, new JsonSerializerSettings
|
||||||
new Dictionary<string, string> { { "modified_by", DeviceId } }
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -503,13 +545,35 @@ namespace VELConnect
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SetDeviceData(Dictionary<string, string> data)
|
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(
|
PostRequestCallback(
|
||||||
_instance.velConnectUrl + "/api/device/set_data/" + DeviceId,
|
_instance.velConnectUrl + "/device/" + DeviceId,
|
||||||
JsonConvert.SerializeObject(new Dictionary<string, object> { { "data", data } }),
|
JsonConvert.SerializeObject(device, Formatting.None, new JsonSerializerSettings
|
||||||
new Dictionary<string, string> { { "modified_by", DeviceId } }
|
{
|
||||||
|
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)
|
public static void SetRoomData(Dictionary<string, string> data)
|
||||||
{
|
{
|
||||||
if (!VelNetManager.InRoom)
|
if (!VelNetManager.InRoom)
|
||||||
|
|
@ -518,34 +582,53 @@ namespace VELConnect
|
||||||
return;
|
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(
|
PostRequestCallback(
|
||||||
_instance.velConnectUrl + "/api/set_data/" + Application.productName + "_" + VelNetManager.Room,
|
_instance.velConnectUrl + "/data_block/" + Application.productName + "_" + VelNetManager.Room,
|
||||||
JsonConvert.SerializeObject(data),
|
JsonConvert.SerializeObject(room, Formatting.None, new JsonSerializerSettings
|
||||||
new Dictionary<string, string> { { "modified_by", DeviceId } }
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
public static void UploadFile(string fileName, byte[] fileData, Action<string> successCallback = null)
|
public static void UploadFile(string fileName, byte[] fileData, Action<string> successCallback = null)
|
||||||
{
|
{
|
||||||
MultipartFormDataContent requestContent = new MultipartFormDataContent();
|
// MultipartFormDataContent requestContent = new MultipartFormDataContent();
|
||||||
ByteArrayContent fileContent = new ByteArrayContent(fileData);
|
// ByteArrayContent fileContent = new ByteArrayContent(fileData);
|
||||||
|
//
|
||||||
requestContent.Add(fileContent, "file", fileName);
|
// requestContent.Add(fileContent, "file", fileName);
|
||||||
|
//
|
||||||
Task.Run(async () =>
|
// Task.Run(async () =>
|
||||||
{
|
// {
|
||||||
HttpResponseMessage r =
|
// HttpResponseMessage r =
|
||||||
await new HttpClient().PostAsync(_instance.velConnectUrl + "/api/upload_file", requestContent);
|
// await new HttpClient().PostAsync(_instance.velConnectUrl + "/api/upload_file", requestContent);
|
||||||
string resp = await r.Content.ReadAsStringAsync();
|
// string resp = await r.Content.ReadAsStringAsync();
|
||||||
Dictionary<string, string> dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(resp);
|
// Dictionary<string, string> dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(resp);
|
||||||
successCallback?.Invoke(dict["key"]);
|
// successCallback?.Invoke(dict["key"]);
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
public static void DownloadFile(string key, Action<byte[]> successCallback = null)
|
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)
|
private IEnumerator DownloadFileCo(string key, Action<byte[]> successCallback = null)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "edu.uga.engr.vel.vel-connect",
|
"name": "edu.uga.engr.vel.vel-connect",
|
||||||
"displayName": "VEL-Connect",
|
"displayName": "VEL-Connect",
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"unity": "2019.1",
|
"unity": "2019.1",
|
||||||
"description": "Web-based configuration for VR applications",
|
"description": "Web-based configuration for VR applications",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
},
|
},
|
||||||
"samples": [],
|
"samples": [],
|
||||||
"dependencies": {
|
"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": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: FastAPI",
|
"name": "Launch file",
|
||||||
"type": "python",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"mode": "debug",
|
||||||
"args": [
|
"program": "main.go",
|
||||||
"main:app",
|
"args": ["serve"]
|
||||||
"--reload"
|
|
||||||
],
|
|
||||||
"jinja": true,
|
|
||||||
"justMyCode": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
# syntax=docker/dockerfile:1
|
||||||
FROM python:3.10
|
FROM golang:1.18 as build
|
||||||
WORKDIR /usr/src/velconnect
|
WORKDIR /src
|
||||||
COPY ./requirements.txt /usr/src/requirements.txt
|
COPY go.mod go.sum ./
|
||||||
RUN pip install --no-cache-dir --upgrade -r /usr/src/requirements.txt
|
RUN go mod download
|
||||||
COPY . /usr/src/velconnect
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
|
|
||||||
|
|
||||||
|
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:
|
services:
|
||||||
web:
|
server:
|
||||||
build: .
|
build: .
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8046:80"
|
- "8090:8090"
|
||||||
volumes:
|
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>
|
|
||||||