mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-11 00:05:36 +02:00
Merge branch 'main' into feat/play
This commit is contained in:
2
.github/workflows/runner.yml
vendored
2
.github/workflows/runner.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- "containers/runner.Containerfile"
|
||||
- "packages/scripts/**"
|
||||
- "packages/server/**"
|
||||
- ".github/workflows/runner.yml"
|
||||
schedule:
|
||||
- cron: 7 0 * * 1,3,6 # Regularly to keep that build cache warm
|
||||
@@ -16,6 +17,7 @@ on:
|
||||
- "containers/runner.Containerfile"
|
||||
- ".github/workflows/runner.yml"
|
||||
- "packages/scripts/**"
|
||||
- "packages/server/**"
|
||||
tags:
|
||||
- v*.*.*
|
||||
release:
|
||||
|
||||
@@ -35,8 +35,10 @@
|
||||
"@builder.io/qwik": "^1.8.0",
|
||||
"@builder.io/qwik-city": "^1.8.0",
|
||||
"@builder.io/qwik-react": "0.5.0",
|
||||
"@fontsource-variable/bricolage-grotesque": "^5.1.1",
|
||||
"@fontsource-variable/mona-sans": "^5.0.1",
|
||||
"@fontsource/bricolage-grotesque": "^5.0.7",
|
||||
"@fontsource/geist-mono": "^5.1.0",
|
||||
"@fontsource/geist-sans": "^5.1.0",
|
||||
"@fontsource/mona-sans": "^5.0.1",
|
||||
"@modular-forms/qwik": "^0.29.0",
|
||||
"@nestri/input": "*",
|
||||
"@nestri/libmoq": "*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
const dbProject = new neon.Project("Nestri", {
|
||||
historyRetentionSeconds: 86400,
|
||||
// name:"Nestri"
|
||||
name:"Nestri"
|
||||
})
|
||||
|
||||
const dbBranchId = $app.stage !== "production" ?
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { authFingerprintKey } from "./auth";
|
||||
import { domain } from "./dns";
|
||||
import { secret } from "./secrets"
|
||||
// import { party } from "./party"
|
||||
import { gpuTaskDefinition, ecsCluster } from "./cluster";
|
||||
|
||||
export const urls = new sst.Linkable("Urls", {
|
||||
properties: {
|
||||
api: "https://api." + domain,
|
||||
auth: "https://auth." + domain,
|
||||
},
|
||||
});
|
||||
|
||||
export const kv = new sst.cloudflare.Kv("CloudflareAuthKV")
|
||||
|
||||
export const auth = new sst.cloudflare.Worker("Auth", {
|
||||
link: [
|
||||
kv,
|
||||
urls,
|
||||
authFingerprintKey,
|
||||
secret.InstantAdminToken,
|
||||
secret.InstantAppId,
|
||||
secret.LoopsApiKey,
|
||||
secret.GithubClientID,
|
||||
secret.GithubClientSecret,
|
||||
secret.DiscordClientID,
|
||||
secret.DiscordClientSecret,
|
||||
],
|
||||
handler: "./packages/functions/src/auth.ts",
|
||||
url: true,
|
||||
domain: "auth." + domain
|
||||
});
|
||||
|
||||
export const api = new sst.cloudflare.Worker("Api", {
|
||||
link: [
|
||||
urls,
|
||||
ecsCluster,
|
||||
gpuTaskDefinition,
|
||||
authFingerprintKey,
|
||||
secret.LoopsApiKey,
|
||||
secret.InstantAppId,
|
||||
secret.AwsAccessKey,
|
||||
secret.AwsSecretKey,
|
||||
secret.InstantAdminToken,
|
||||
],
|
||||
url: true,
|
||||
handler: "./packages/functions/src/api/index.ts",
|
||||
domain: "api." + domain
|
||||
})
|
||||
|
||||
export const outputs = {
|
||||
auth: auth.url,
|
||||
api: api.url
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export const authFingerprintKey = new random.RandomString(
|
||||
"AuthFingerprintKey",
|
||||
{
|
||||
length: 32,
|
||||
},
|
||||
);
|
||||
|
||||
sst.Linkable.wrap(random.RandomString, (resource) => ({
|
||||
properties: {
|
||||
value: resource.result,
|
||||
},
|
||||
}));
|
||||
@@ -1,155 +0,0 @@
|
||||
import { sshKey } from "./ssh";
|
||||
import { authFingerprintKey } from "./auth";
|
||||
|
||||
export const ecsCluster = new aws.ecs.Cluster("NestriGPUCluster", {
|
||||
name: "NestriGPUCluster",
|
||||
});
|
||||
|
||||
const ecsInstanceRole = new aws.iam.Role("NestriGPUInstanceRole", {
|
||||
name: "GPUAssumeRoleProd",
|
||||
assumeRolePolicy: JSON.stringify({
|
||||
Version: "2012-10-17",
|
||||
Statement: [{
|
||||
Action: "sts:AssumeRole",
|
||||
Principal: {
|
||||
Service: "ec2.amazonaws.com",
|
||||
},
|
||||
Effect: "Allow",
|
||||
Sid: "",
|
||||
}],
|
||||
}),
|
||||
});
|
||||
|
||||
new aws.iam.RolePolicyAttachment("NestriGPUInstancePolicyAttachment", {
|
||||
role: ecsInstanceRole.name,
|
||||
policyArn: "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role",
|
||||
});
|
||||
|
||||
const ecsInstanceProfile = new aws.iam.InstanceProfile("NestriGPUInstanceProfile", {
|
||||
role: ecsInstanceRole.name,
|
||||
});
|
||||
|
||||
// const server = new aws.ec2.Instance("NestriGPU", {
|
||||
// instanceType: aws.ec2.InstanceType.G4dn_XLarge,
|
||||
// ami: "ami-046a6af96ef510bb6",//Fedora cloud
|
||||
// keyName: sshKey.keyName,
|
||||
// instanceMarketOptions: {
|
||||
// marketType: "spot",
|
||||
// spotOptions: {
|
||||
// maxPrice: "0.2",
|
||||
// spotInstanceType: "persistent",
|
||||
// instanceInterruptionBehavior: "stop"
|
||||
// },
|
||||
// },
|
||||
// iamInstanceProfile: ecsInstanceProfile,
|
||||
// });
|
||||
|
||||
const logGroup = new aws.cloudwatch.LogGroup("NestriGPULogGroup", {
|
||||
name: "/ecs/nestri-gpu-prod",
|
||||
retentionInDays: 7,
|
||||
});
|
||||
|
||||
// Create a Task Definition for the ECS service to test it
|
||||
export const gpuTaskDefinition = new aws.ecs.TaskDefinition("NestriGPUTask", {
|
||||
family: "NestriGPUTaskProd",
|
||||
requiresCompatibilities: ["EC2"],
|
||||
volumes: [
|
||||
{
|
||||
name: "host",
|
||||
hostPath: "/mnt/"
|
||||
// efsVolumeConfiguration: {
|
||||
// fileSystemId: storage.id,
|
||||
// authorizationConfig: { accessPointId: storage.accessPoint },
|
||||
// transitEncryption: "ENABLED",
|
||||
// }
|
||||
}
|
||||
],
|
||||
containerDefinitions: authFingerprintKey.result.apply(v => JSON.stringify([{
|
||||
"essential": true,
|
||||
"name": "nestri",
|
||||
"memory": 1024,
|
||||
"cpu": 200,
|
||||
"gpu": 1,
|
||||
"image": "ghcr.io/nestrilabs/nestri/runner:nightly",
|
||||
"environment": [
|
||||
{
|
||||
"name": "RESOLUTION",
|
||||
"value": "1920x1080"
|
||||
},
|
||||
{
|
||||
"name": "AUTH_FINGERPRINT",
|
||||
"value": v
|
||||
},
|
||||
{
|
||||
"name": "FRAMERATE",
|
||||
"value": "60"
|
||||
},
|
||||
{
|
||||
"name": "NESTRI_ROOM",
|
||||
"value": "aws-testing"
|
||||
},
|
||||
{
|
||||
"name": "RELAY_URL",
|
||||
"value": "https://relay.dathorse.com"
|
||||
},
|
||||
{
|
||||
"name": "NESTRI_PARAMS",
|
||||
"value": "--verbose=true --video-codec=h264 --video-bitrate=4000 --video-bitrate-max=6000 --gpu-card-path=/dev/dri/card0"
|
||||
},
|
||||
],
|
||||
"mountPoints": [{ "containerPath": "/home/nestri", "sourceVolume": "host" }],
|
||||
"disableNetworking": false,
|
||||
"linuxParameter": {
|
||||
"sharedMemorySize": 5120
|
||||
},
|
||||
"logConfiguration": {
|
||||
"logDriver": "awslogs",
|
||||
"options": {
|
||||
"awslogs-group": "/ecs/nestri-gpu-prod",
|
||||
"awslogs-region": "us-east-1",
|
||||
"awslogs-stream-prefix": "nestri-gpu-task"
|
||||
}
|
||||
}
|
||||
}]))
|
||||
});
|
||||
|
||||
sst.Linkable.wrap(aws.ecs.TaskDefinition, (resource) => ({
|
||||
properties: {
|
||||
value: resource.arn,
|
||||
},
|
||||
}));
|
||||
|
||||
sst.Linkable.wrap(aws.ecs.Cluster, (resource) => ({
|
||||
properties: {
|
||||
value: resource.arn,
|
||||
},
|
||||
}));
|
||||
|
||||
// userData: $interpolate`#!/bin/bash
|
||||
// sudo rm /etc/sysconfig/docker
|
||||
// echo DAEMON_MAXFILES=1048576 | sudo tee -a /etc/sysconfig/docker
|
||||
// echo DAEMON_PIDFILE_TIMEOUT=10 | sud o tee -a /etc/sysconfig/docker
|
||||
// echo OPTIONS="--default-ulimit nofile=32768:65536" | sudo tee -a /etc/sysconfig/docker
|
||||
// sudo tee "/etc/docker/daemon.json" > /dev/null <<EOF
|
||||
// {
|
||||
// "default-runtime": "nvidia",
|
||||
// "runtimes": {
|
||||
// "nvidia": {
|
||||
// "path": "/usr/bin/nvidia-container-runtime",
|
||||
// "runtimeArgs": []
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// EOF
|
||||
// sudo systemctl restart docker
|
||||
// echo ECS_CLUSTER='${ecsCluster.name}' | sudo tee -a /etc/ecs/ecs.config
|
||||
// echo ECS_ENABLE_GPU_SUPPORT=true | sudo tee -a /etc/ecs/ecs.config
|
||||
// echo ECS_CONTAINER_STOP_TIMEOUT=3h | sudo tee -a /etc/ecs/ecs.config
|
||||
// echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true | sudo tee -a /etc/ecs/ecs.config
|
||||
// `,
|
||||
|
||||
// This is used for requesting a container to be deployed on AWS
|
||||
// const queue = new sst.aws.Queue("PartyQueue", { fifo: true });
|
||||
|
||||
// queue.subscribe({ handler: "packages/functions/src/party/subscriber.handler", permissions:{}, link:[taskF]})
|
||||
// const authRes = $interpolate`${authFingerprintKey.result}`
|
||||
@@ -1,9 +0,0 @@
|
||||
export const domain =
|
||||
{
|
||||
production: "nestri.io",
|
||||
dev: "dev.nestri.io",
|
||||
}[$app.stage] || $app.stage + ".dev.nestri.io";
|
||||
|
||||
export const zone = cloudflare.getZoneOutput({
|
||||
name: "nestri.io",
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
// This is for the websocket/MQTT endpoint that helps the API communicate with the container
|
||||
// [API] <-> party <-websocket-> container
|
||||
// The container is it's own this, and can listen to Websocket connections to start or stop a Steam Game
|
||||
|
||||
// import { authFingerprintKey } from "./auth";
|
||||
// import { ecsCluster, gpuTaskDefinition } from "./cluster";
|
||||
|
||||
// export const party = new sst.aws.Realtime("Party", {
|
||||
// authorizer: "packages/functions/src/party/authorizer.handler"
|
||||
// });
|
||||
|
||||
// export const partyFn = new sst.aws.Function("NestriPartyFn", {
|
||||
// handler: "packages/functions/src/party/create.handler",
|
||||
// // link: [queue],
|
||||
// link: [authFingerprintKey],
|
||||
// environment: {
|
||||
// TASK_DEFINITION: gpuTaskDefinition.arn,
|
||||
// // AUTH_FINGERPRINT: authFingerprintKey.result,
|
||||
// ECS_CLUSTER: ecsCluster.arn,
|
||||
// },
|
||||
// permissions: [
|
||||
// {
|
||||
// effect: "allow",
|
||||
// actions: ["ecs:RunTask"],
|
||||
// resources: [gpuTaskDefinition.arn]
|
||||
// }
|
||||
// ],
|
||||
// url: true,
|
||||
// });
|
||||
|
||||
// export const outputs = {
|
||||
// partyFunction: partyFn.url
|
||||
// }
|
||||
@@ -1,179 +0,0 @@
|
||||
// const vpc = new sst.aws.Vpc("NestriRelayVpc", { az: 2 })
|
||||
// import { subnet1, subnet2, securityGroup } from "./vpc"
|
||||
|
||||
// const taskExecutionRole = new aws.iam.Role('NestriRelayExecutionRole', {
|
||||
// assumeRolePolicy: JSON.stringify({
|
||||
// Version: '2012-10-17',
|
||||
// Statement: [
|
||||
// {
|
||||
// Effect: 'Allow',
|
||||
// Principal: {
|
||||
// Service: 'ecs-tasks.amazonaws.com',
|
||||
// },
|
||||
// Action: 'sts:AssumeRole',
|
||||
// },
|
||||
// ],
|
||||
// }),
|
||||
// });
|
||||
|
||||
// const taskRole = new aws.iam.Role('NestriRelayTaskRole', {
|
||||
// assumeRolePolicy: JSON.stringify({
|
||||
// Version: '2012-10-17',
|
||||
// Statement: [
|
||||
// {
|
||||
// Effect: 'Allow',
|
||||
// Principal: {
|
||||
// Service: 'ecs-tasks.amazonaws.com',
|
||||
// },
|
||||
// Action: 'sts:AssumeRole',
|
||||
// },
|
||||
// ],
|
||||
// }),
|
||||
// });
|
||||
|
||||
// new aws.cloudwatch.LogGroup('NestriRelayLogGroup', {
|
||||
// name: '/ecs/nestri-relay',
|
||||
// retentionInDays: 7,
|
||||
// });
|
||||
|
||||
// new aws.iam.RolePolicyAttachment('NestriRelayExecutionRoleAttachment', {
|
||||
// policyArn: 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy',
|
||||
// role: taskRole,
|
||||
// });
|
||||
|
||||
// const logPolicy = new aws.iam.Policy('NestriRelayLogPolicy', {
|
||||
// policy: JSON.stringify({
|
||||
// Version: '2012-10-17',
|
||||
// Statement: [
|
||||
// {
|
||||
// Effect: 'Allow',
|
||||
// Action: ['logs:CreateLogStream', 'logs:PutLogEvents'],
|
||||
// Resource: 'arn:aws:logs:*:*:*',
|
||||
// },
|
||||
// ],
|
||||
// }),
|
||||
// });
|
||||
|
||||
// new aws.iam.RolePolicyAttachment('NestriRelayTaskRoleAttachment', {
|
||||
// policyArn: logPolicy.arn,
|
||||
// role: taskExecutionRole,
|
||||
// });
|
||||
|
||||
// const taskDefinition = new aws.ecs.TaskDefinition("NestriRelayTask", {
|
||||
// family: "NestriRelay",
|
||||
// cpu: "1024",
|
||||
// memory: "2048",
|
||||
// networkMode: "awsvpc",
|
||||
// taskRoleArn: taskRole.arn,
|
||||
// requiresCompatibilities: ["FARGATE"],
|
||||
// executionRoleArn: taskExecutionRole.arn,
|
||||
// containerDefinitions: JSON.stringify([{
|
||||
// name: "nestri-relay",
|
||||
// essential: true,
|
||||
// memory: 2048,
|
||||
// image: "ghcr.io/nestrilabs/nestri/relay:nightly",
|
||||
// portMappings: [
|
||||
// // HTTP port
|
||||
// {
|
||||
// protocol: "tcp",
|
||||
// hostPort: 80,
|
||||
// containerPort: 80,
|
||||
// },
|
||||
// // UDP port range (1,000 ports)
|
||||
// {
|
||||
// containerPortRange: "10000-11000",
|
||||
// protocol: "udp",
|
||||
// },
|
||||
// ],
|
||||
// "environment": [
|
||||
// {
|
||||
// name: "ENDPOINT_PORT",
|
||||
// value: "80"
|
||||
// },
|
||||
// ],
|
||||
// logConfiguration: {
|
||||
// logDriver: 'awslogs',
|
||||
// options: {
|
||||
// 'awslogs-group': '/ecs/nestri-relay',
|
||||
// 'awslogs-region': 'us-east-1',
|
||||
// 'awslogs-stream-prefix': 'ecs',
|
||||
// },
|
||||
// },
|
||||
// }]),
|
||||
// });
|
||||
|
||||
// const relayCluster = new aws.ecs.Cluster('NestriRelay');
|
||||
|
||||
// new aws.ecs.Service('NestriRelayService', {
|
||||
// name: 'NestriRelayService',
|
||||
// cluster: relayCluster.arn,
|
||||
// desiredCount: 1,
|
||||
// launchType: 'FARGATE',
|
||||
// taskDefinition: taskDefinition.arn,
|
||||
// deploymentCircuitBreaker: {
|
||||
// enable: true,
|
||||
// rollback: true,
|
||||
// },
|
||||
// enableExecuteCommand: true,
|
||||
// networkConfiguration: {
|
||||
// assignPublicIp: true,
|
||||
// subnets: [subnet1.id, subnet2.id],
|
||||
// securityGroups: [securityGroup.id],
|
||||
// },
|
||||
// });
|
||||
|
||||
//FIXME: I cannot create Global Accelerators (Something to do with Quotas - Yet my account is fine)
|
||||
// const usWest2 = new aws.Provider("GlobalAccelerator", { region: aws.Region.USWest2 })
|
||||
|
||||
// const accelerator = new aws.globalaccelerator.Accelerator('Accelerator', {
|
||||
// name: 'NestriRelayAccelerator',
|
||||
// enabled: true,
|
||||
// ipAddressType: 'IPV4',
|
||||
// }, { provider: usWest2 });
|
||||
|
||||
// const httpListener = new aws.globalaccelerator.Listener('TcpListener', {
|
||||
// acceleratorArn: accelerator.id,
|
||||
// clientAffinity: 'SOURCE_IP',
|
||||
// protocol: 'TCP',
|
||||
// portRanges: [{
|
||||
// fromPort: 80,
|
||||
// toPort: 80,
|
||||
// }],
|
||||
// }, { provider: usWest2 });
|
||||
|
||||
// const udpListener = new aws.globalaccelerator.Listener('UdpListener', {
|
||||
// acceleratorArn: accelerator.id,
|
||||
// clientAffinity: 'SOURCE_IP',
|
||||
// protocol: 'UDP',
|
||||
// portRanges: [{
|
||||
// fromPort: 10000,
|
||||
// toPort: 11000,
|
||||
// }],
|
||||
// }, { provider: usWest2 });
|
||||
|
||||
// new aws.globalaccelerator.EndpointGroup('TcpRelay', {
|
||||
// listenerArn: httpListener.id,
|
||||
// // healthCheckPath: '/',
|
||||
// endpointGroupRegion: aws.Region.USEast1,
|
||||
// endpointConfigurations: [{
|
||||
// clientIpPreservationEnabled: true,
|
||||
// endpointId: subnet1.id, //vpc.publicSubnets[0].apply(i => i),
|
||||
// weight: 100,
|
||||
// }],
|
||||
// }, { provider: usWest2 });
|
||||
|
||||
// new aws.globalaccelerator.EndpointGroup('UdpRelay', {
|
||||
// listenerArn: udpListener.id,
|
||||
// // healthCheckPort: 80,
|
||||
// // healthCheckPath: "/",
|
||||
// endpointGroupRegion: aws.Region.USEast1,
|
||||
// endpointConfigurations: [{
|
||||
// clientIpPreservationEnabled: true,
|
||||
// endpointId: subnet1.id,//vpc.publicSubnets[0].apply(i => i),
|
||||
// weight: 100,
|
||||
// }],
|
||||
// }, { provider: usWest2 });
|
||||
|
||||
// export const outputs = {
|
||||
// relay: accelerator.dnsName
|
||||
// }
|
||||
@@ -1,13 +0,0 @@
|
||||
export const secret = {
|
||||
LoopsApiKey: new sst.Secret("LoopsApiKey"),
|
||||
InstantAppId: new sst.Secret("InstantAppId"),
|
||||
AwsSecretKey: new sst.Secret("AwsSecretKey"),
|
||||
AwsAccessKey: new sst.Secret("AwsAccessKey"),
|
||||
GithubClientID: new sst.Secret("GithubClientID"),
|
||||
DiscordClientID: new sst.Secret("DiscordClientID"),
|
||||
GithubClientSecret: new sst.Secret("GithubClientSecret"),
|
||||
InstantAdminToken: new sst.Secret("InstantAdminToken"),
|
||||
DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
|
||||
};
|
||||
|
||||
export const allSecrets = Object.values(secret);
|
||||
@@ -1,19 +0,0 @@
|
||||
import { resolve } from "path";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
export const privateKey = new tls.PrivateKey("NestriGPUPrivateKey", {
|
||||
algorithm: "RSA",
|
||||
rsaBits: 4096,
|
||||
});
|
||||
|
||||
// Just in case you want to SSH
|
||||
export const sshKey = new aws.ec2.KeyPair("NestriGPUKey", {
|
||||
keyName: "NestriGPUKeyProd",
|
||||
publicKey: privateKey.publicKeyOpenssh
|
||||
})
|
||||
|
||||
export const keyPath = privateKey.privateKeyOpenssh.apply((key) => {
|
||||
const path = "key_ssh";
|
||||
writeFileSync(path, key, { mode: 0o600 });
|
||||
return resolve(path);
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export const isPermanentStage =
|
||||
$app.stage === "production" || $app.stage === "dev";
|
||||
@@ -1,4 +0,0 @@
|
||||
// export const vpc = new sst.aws.Vpc("Vpc")
|
||||
|
||||
// export const storage = new sst.aws.Efs("GameStorage",{ vpc })
|
||||
// //
|
||||
103
infra:old/vpc.ts
103
infra:old/vpc.ts
@@ -1,103 +0,0 @@
|
||||
// export const vpc = new aws.ec2.Vpc('NestriVpc', {
|
||||
// cidrBlock: '172.16.0.0/16',
|
||||
// });
|
||||
|
||||
// export const subnet1 = new aws.ec2.Subnet('NestriSubnet1', {
|
||||
// vpcId: vpc.id,
|
||||
// cidrBlock: '172.16.1.0/24',
|
||||
// // cidrBlock: '110.0.12.0/22',
|
||||
// availabilityZone: 'us-east-1a',
|
||||
// });
|
||||
|
||||
// export const subnet2 = new aws.ec2.Subnet('NestriSubnet2', {
|
||||
// vpcId: vpc.id,
|
||||
// cidrBlock: '172.16.2.0/24',
|
||||
// // cidrBlock: '10.0.20.0/22',
|
||||
// availabilityZone: 'us-east-1b',
|
||||
// });
|
||||
|
||||
// const internetGateway = new aws.ec2.InternetGateway('NestriInternetGateway', {
|
||||
// vpcId: vpc.id,
|
||||
// });
|
||||
|
||||
// const routeTable = new aws.ec2.RouteTable('NestriRouteTable', {
|
||||
// vpcId: vpc.id,
|
||||
// routes: [
|
||||
// {
|
||||
// cidrBlock: '0.0.0.0/0',
|
||||
// gatewayId: internetGateway.id,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
|
||||
// new aws.ec2.RouteTableAssociation('NestriSubnet1RouteTable', {
|
||||
// subnetId: subnet1.id,
|
||||
// routeTableId: routeTable.id,
|
||||
// });
|
||||
|
||||
// new aws.ec2.RouteTableAssociation('NestriSubnet2RouteTable', {
|
||||
// subnetId: subnet2.id,
|
||||
// routeTableId: routeTable.id,
|
||||
// });
|
||||
|
||||
// // const vpc = new sst.aws.Vpc("NestriRelayVpc")
|
||||
|
||||
// export const securityGroup = new aws.ec2.SecurityGroup("NestriSecurityGroup", {
|
||||
// vpcId: vpc.id,
|
||||
// description: "Managed thru SST",
|
||||
// ingress: [
|
||||
// {
|
||||
// protocol: "tcp",
|
||||
// fromPort: 80,
|
||||
// toPort: 80,
|
||||
// cidrBlocks: ["0.0.0.0/0"],
|
||||
// },
|
||||
// {
|
||||
// protocol: "udp",
|
||||
// fromPort: 10000,
|
||||
// toPort: 20000,
|
||||
// cidrBlocks: ["0.0.0.0/0"],
|
||||
// },
|
||||
// ],
|
||||
// egress: [
|
||||
// {
|
||||
// protocol: "-1",
|
||||
// cidrBlocks: ["0.0.0.0/0"],
|
||||
// fromPort: 0,
|
||||
// toPort: 0
|
||||
// }
|
||||
// ]
|
||||
// });
|
||||
|
||||
// const loadBalancer = new aws.lb.LoadBalancer('NestriVpcLoadBalancer', {
|
||||
// name: 'NestriVpcLoadBalancer',
|
||||
// internal: false,
|
||||
// securityGroups: [securityGroup.id],
|
||||
// subnets: vpc.publicSubnets
|
||||
// });
|
||||
|
||||
// const targetGroup = new aws.lb.TargetGroup('NestriVpcTargetGroup', {
|
||||
// name: 'NestriVpcTargetGroup',
|
||||
// port: 80,
|
||||
// protocol: 'HTTP',
|
||||
// targetType: 'ip',
|
||||
// vpcId: vpc.id,
|
||||
// healthCheck: {
|
||||
// path: '/',
|
||||
// protocol: 'HTTP',
|
||||
// },
|
||||
// });
|
||||
|
||||
// new aws.lb.Listener('NestriVpcLoadBalancerListener', {
|
||||
// loadBalancerArn: loadBalancer.arn,
|
||||
// port: 80,
|
||||
// protocol: 'HTTP',
|
||||
// defaultActions: [
|
||||
// {
|
||||
// type: 'forward',
|
||||
// targetGroupArn: targetGroup.arn,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
|
||||
// // export const subnets = [subnet1, subnet2]
|
||||
@@ -1,86 +0,0 @@
|
||||
import { createContext } from "../src/context";
|
||||
import { VisibleError } from "./error";
|
||||
|
||||
export interface UserActor {
|
||||
type: "user";
|
||||
properties: {
|
||||
accessToken: string;
|
||||
userID: string;
|
||||
auth?:
|
||||
| {
|
||||
type: "personal";
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "oauth";
|
||||
clientID: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeviceActor {
|
||||
type: "device";
|
||||
properties: {
|
||||
teamSlug: string;
|
||||
hostname: string;
|
||||
auth?:
|
||||
| {
|
||||
type: "personal";
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "oauth";
|
||||
clientID: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface PublicActor {
|
||||
type: "public";
|
||||
properties: {};
|
||||
}
|
||||
|
||||
type Actor = UserActor | PublicActor | DeviceActor;
|
||||
export const ActorContext = createContext<Actor>();
|
||||
|
||||
export function useCurrentUser() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "user") return {
|
||||
id:actor.properties.userID,
|
||||
token: actor.properties.accessToken,
|
||||
};
|
||||
|
||||
throw new VisibleError(
|
||||
"auth",
|
||||
"unauthorized",
|
||||
`You don't have permission to access this resource`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useCurrentDevice() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "device") return {
|
||||
hostname:actor.properties.hostname,
|
||||
teamSlug: actor.properties.teamSlug
|
||||
};
|
||||
throw new VisibleError(
|
||||
"auth",
|
||||
"unauthorized",
|
||||
`You don't have permission to access this resource`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useActor() {
|
||||
try {
|
||||
return ActorContext.use();
|
||||
} catch {
|
||||
return { type: "public", properties: {} } as PublicActor;
|
||||
}
|
||||
}
|
||||
|
||||
export function assertActor<T extends Actor["type"]>(type: T) {
|
||||
const actor = useActor();
|
||||
if (actor.type !== type)
|
||||
throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`);
|
||||
return actor as Extract<Actor, { type: T }>;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { Resource } from "sst";
|
||||
import { doubleFn, fn } from "../utils";
|
||||
import { AwsClient } from "aws4fetch";
|
||||
import { DescribeTasksCommandOutput, StopTaskCommandOutput, type RunTaskCommandOutput } from "@aws-sdk/client-ecs";
|
||||
|
||||
|
||||
export module Aws {
|
||||
export const client = async () => {
|
||||
return new AwsClient({
|
||||
accessKeyId: Resource.AwsAccessKey.value,
|
||||
secretAccessKey: Resource.AwsSecretKey.value,
|
||||
region: "us-east-1",
|
||||
});
|
||||
}
|
||||
|
||||
export const EcsRunTask = fn(z.object({
|
||||
cluster: z.string(),
|
||||
count: z.number(),
|
||||
taskDefinition: z.string(),
|
||||
launchType: z.enum(["EC2", "FARGATE"]),
|
||||
overrides: z.object({
|
||||
containerOverrides: z.object({
|
||||
name: z.string(),
|
||||
environment: z.object({
|
||||
name: z.string(),
|
||||
value: z.string().or(z.number())
|
||||
}).array()
|
||||
}).array()
|
||||
})
|
||||
}), async (body) => {
|
||||
|
||||
const c = await client();
|
||||
|
||||
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
|
||||
|
||||
const res = await c.fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.RunTask",
|
||||
"Content-Type": "application/x-amz-json-1.1",
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
return await res.json() as RunTaskCommandOutput
|
||||
})
|
||||
|
||||
export const EcsDescribeTasks = fn(z.object({ tasks: z.string().array(), cluster: z.string() }), async (body) => {
|
||||
const c = await client();
|
||||
|
||||
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
|
||||
|
||||
const res = await c.fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.DescribeTasks",
|
||||
"Content-Type": "application/x-amz-json-1.1",
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
return await res.json() as DescribeTasksCommandOutput
|
||||
})
|
||||
|
||||
|
||||
export const EcsStopTask = fn(z.object({
|
||||
cluster: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
task: z.string()
|
||||
}), async (body) => {
|
||||
const c = await client();
|
||||
|
||||
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
|
||||
|
||||
const res = await c.fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.StopTask",
|
||||
"Content-Type": "application/x-amz-json-1.1",
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
return await res.json() as StopTaskCommandOutput
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import "zod-openapi/extend";
|
||||
|
||||
export module Common {
|
||||
export const IdDescription = `Unique object identifier.
|
||||
The format and length of IDs may change over time.`;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Resource } from "sst";
|
||||
import { init } from "@instantdb/admin";
|
||||
import schema from "../instant.schema";
|
||||
|
||||
const databaseClient = () => init({
|
||||
appId: Resource.InstantAppId.value,
|
||||
adminToken: Resource.InstantAdminToken.value,
|
||||
schema
|
||||
})
|
||||
|
||||
|
||||
export default databaseClient
|
||||
@@ -1,45 +0,0 @@
|
||||
import { LoopsClient } from "loops";
|
||||
import { Resource } from "sst/resource"
|
||||
export namespace Email {
|
||||
export const Client = () => new LoopsClient(Resource.LoopsApiKey.value);
|
||||
|
||||
export async function send(
|
||||
to: string,
|
||||
body: string,
|
||||
) {
|
||||
|
||||
try {
|
||||
await Client().sendTransactionalEmail(
|
||||
{
|
||||
transactionalId: "cm58pdf8d03upb5ecirnmvrfb",
|
||||
email: to,
|
||||
dataVariables: {
|
||||
logincode: body
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error sending email", error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendWelcome(
|
||||
to: string,
|
||||
name: string,
|
||||
) {
|
||||
|
||||
try {
|
||||
await Client().sendTransactionalEmail(
|
||||
{
|
||||
transactionalId: "cm61jrbbx02twlstfwfcywt5u",
|
||||
email: to,
|
||||
dataVariables: {
|
||||
name
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error sending email", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export class VisibleError extends Error {
|
||||
constructor(
|
||||
public kind: "input" | "auth",
|
||||
public code: string,
|
||||
public message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
export module Examples {
|
||||
|
||||
export const User = {
|
||||
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
|
||||
email: "john@example.com",
|
||||
};
|
||||
|
||||
export const Task = {
|
||||
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
|
||||
taskID: "b8302fca2d224d91ab342a2e4ab926d3",
|
||||
type: "AWS" as const, //or "on-premises",
|
||||
lastStatus: "RUNNING" as const,
|
||||
healthStatus: "UNKNOWN" as const,
|
||||
startedAt: '2025-01-09T01:56:23.902Z',
|
||||
lastUpdated: '2025-01-09T01:56:23.902Z',
|
||||
stoppedAt: '2025-01-09T04:46:23.902Z'
|
||||
}
|
||||
|
||||
export const Profile = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
username: "janedoe47",
|
||||
status: "active" as const,
|
||||
avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png",
|
||||
discriminator: 12, //it needs to be two digits
|
||||
createdAt: '2025-01-04T11:56:23.902Z',
|
||||
updatedAt: '2025-01-09T01:56:23.902Z'
|
||||
}
|
||||
|
||||
export const Subscription = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
checkoutID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
|
||||
// productID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
|
||||
// quantity: 1,
|
||||
// frequency: "monthly" as const,
|
||||
// next: '2025-01-09T01:56:23.902Z',
|
||||
canceledAt: '2025-02-09T01:56:23.902Z'
|
||||
}
|
||||
|
||||
export const Team = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
// owner: true,
|
||||
name: "Jane Doe's Games",
|
||||
slug: "jane-does-games",
|
||||
createdAt: '2025-01-04T11:56:23.902Z',
|
||||
updatedAt: '2025-01-09T01:56:23.902Z'
|
||||
}
|
||||
|
||||
export const Machine = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
hostname: "DESKTOP-EUO8VSF",
|
||||
fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090",
|
||||
createdAt: '2025-01-04T11:56:23.902Z',
|
||||
deletedAt: '2025-01-09T01:56:23.902Z'
|
||||
}
|
||||
|
||||
export const Instance = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
hostname: "a955e059f05d",
|
||||
createdAt: '2025-01-04T11:56:23.902Z',
|
||||
lastActive: '2025-01-09T01:56:23.902Z'
|
||||
}
|
||||
|
||||
export const Game = {
|
||||
id: '0bfcb712-df13-4454-81a8-fbee66eddca4',
|
||||
name: "Control Ultimate Edition",
|
||||
steamID: 870780,
|
||||
}
|
||||
|
||||
export const Session = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
public: true,
|
||||
startedAt: '2025-01-04T11:56:23.902Z',
|
||||
endedAt: '2025-01-04T12:36:23.902Z'
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
// import { z } from "zod"
|
||||
// import { fn } from "../utils";
|
||||
// import { Common } from "../common";
|
||||
// import { Examples } from "../examples";
|
||||
// import databaseClient from "../database"
|
||||
// import { id as createID } from "@instantdb/admin";
|
||||
// import { groupBy, map, pipe, values } from "remeda"
|
||||
// import { useCurrentDevice, useCurrentUser } from "../actor";
|
||||
|
||||
// export module Games {
|
||||
// export const Info = z
|
||||
// .object({
|
||||
// id: z.string().openapi({
|
||||
// description: Common.IdDescription,
|
||||
// example: Examples.Game.id,
|
||||
// }),
|
||||
// name: z.string().openapi({
|
||||
// description: "A human-readable name for the game, used for easy identification.",
|
||||
// example: Examples.Game.name,
|
||||
// }),
|
||||
// steamID: z.number().openapi({
|
||||
// description: "The Steam ID of the game, used to identify it during installation and runtime.",
|
||||
// example: Examples.Game.steamID,
|
||||
// })
|
||||
// })
|
||||
// .openapi({
|
||||
// ref: "Game",
|
||||
// description: "Represents a Steam game that can be installed and played on a machine.",
|
||||
// example: Examples.Game,
|
||||
// });
|
||||
|
||||
// export type Info = z.infer<typeof Info>;
|
||||
|
||||
// export const create = fn(Info.pick({ name: true, steamID: true }), async (input) => {
|
||||
// const id = createID()
|
||||
// const db = databaseClient()
|
||||
// const device = useCurrentDevice()
|
||||
|
||||
// await db.transact(
|
||||
// db.tx.games[id]!.update({
|
||||
// name: input.name,
|
||||
// steamID: input.steamID,
|
||||
// }).link({ machines: device.id })
|
||||
// )
|
||||
// //
|
||||
// return id
|
||||
// })
|
||||
|
||||
// export const list = async () => {
|
||||
// const db = databaseClient()
|
||||
// const user = useCurrentUser()
|
||||
|
||||
// const query = {
|
||||
// $users: {
|
||||
// $: { where: { id: user.id } },
|
||||
// games: {}
|
||||
// },
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
|
||||
// const games = res.$users[0]?.games
|
||||
// if (games && games.length > 0) {
|
||||
// const result = pipe(
|
||||
// games,
|
||||
// groupBy(x => x.id),
|
||||
// values(),
|
||||
// map((group): Info => ({
|
||||
// id: group[0].id,
|
||||
// name: group[0].name,
|
||||
// steamID: group[0].steamID,
|
||||
// }))
|
||||
// )
|
||||
// return result
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
|
||||
// export const fromSteamID = fn(z.number(), async (steamID) => {
|
||||
// const db = databaseClient()
|
||||
|
||||
// const query = {
|
||||
// games: {
|
||||
// $: {
|
||||
// where: {
|
||||
// steamID,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
|
||||
// const games = res.games
|
||||
|
||||
// if (games.length > 0) {
|
||||
// const result = pipe(
|
||||
// games,
|
||||
// groupBy(x => x.id),
|
||||
// values(),
|
||||
// map((group): Info => ({
|
||||
// id: group[0].id,
|
||||
// name: group[0].name,
|
||||
// steamID: group[0].steamID,
|
||||
// }))
|
||||
// )
|
||||
// return result[0]
|
||||
// }
|
||||
|
||||
// return null
|
||||
// })
|
||||
|
||||
// export const linkToCurrentUser = fn(z.string(), async (steamID) => {
|
||||
// const user = useCurrentUser()
|
||||
// const db = databaseClient()
|
||||
|
||||
// await db.transact(db.tx.games[steamID]!.link({ owners: user.id }))
|
||||
|
||||
// return "ok"
|
||||
// })
|
||||
|
||||
// export const unLinkFromCurrentUser = fn(z.number(), async (steamID) => {
|
||||
// const user = useCurrentUser()
|
||||
// const db = databaseClient()
|
||||
|
||||
// const query = {
|
||||
// $users: {
|
||||
// $: { where: { id: user.id } },
|
||||
// games: {
|
||||
// $: {
|
||||
// where: {
|
||||
// steamID,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
// const games = res.$users[0]?.games
|
||||
// if (games && games.length > 0) {
|
||||
// const game = games[0] as Info
|
||||
// await db.transact(db.tx.games[game.id]!.unlink({ owners: user.id }))
|
||||
|
||||
// return "ok"
|
||||
// }
|
||||
|
||||
// return null
|
||||
// })
|
||||
|
||||
// }
|
||||
@@ -1,83 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import databaseClient from "../database"
|
||||
import { id as createID } from "@instantdb/admin";
|
||||
import { groupBy, map, pipe, values } from "remeda"
|
||||
|
||||
export module Instances {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Instance.id,
|
||||
}),
|
||||
hostname: z.string().openapi({
|
||||
description: "The container's hostname",
|
||||
example: Examples.Instance.hostname,
|
||||
}),
|
||||
createdAt: z.string().or(z.number()).openapi({
|
||||
description: "The time this instances was registered on the network",
|
||||
example: Examples.Instance.createdAt,
|
||||
}),
|
||||
lastActive: z.string().or(z.number()).optional().openapi({
|
||||
description: "The time this instance was last seen on the network",
|
||||
example: Examples.Instance.lastActive,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Instance",
|
||||
description: "Represents a running container that is connected to the Nestri network..",
|
||||
example: Examples.Instance,
|
||||
});
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
export const create = fn(z.object({ hostname: z.string(), teamID: z.string() }), async (input) => {
|
||||
const id = createID()
|
||||
const now = new Date().toISOString()
|
||||
const db = databaseClient()
|
||||
await db.transact(
|
||||
db.tx.instances[id]!.update({
|
||||
hostname: input.hostname,
|
||||
createdAt: now,
|
||||
}).link({ owners: input.teamID })
|
||||
)
|
||||
|
||||
return "ok"
|
||||
})
|
||||
|
||||
export const fromTeamID = fn(z.string(), async (teamID) => {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
instances: {
|
||||
$: {
|
||||
where: {
|
||||
owners: teamID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
const data = res.instances
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const result = pipe(
|
||||
data,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
lastActive: group[0].lastActive,
|
||||
hostname: group[0].hostname,
|
||||
createdAt: group[0].createdAt
|
||||
}))
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// import { z } from "zod"
|
||||
// import { fn } from "../utils";
|
||||
// import { Games } from "../game"
|
||||
// import { Common } from "../common";
|
||||
// import { Examples } from "../examples";
|
||||
// import { useCurrentUser } from "../actor";
|
||||
// import databaseClient from "../database"
|
||||
// import { id as createID } from "@instantdb/admin";
|
||||
// import { groupBy, map, pipe, values } from "remeda"
|
||||
// export module Machines {
|
||||
// export const Info = z
|
||||
// .object({
|
||||
// id: z.string().openapi({
|
||||
// description: Common.IdDescription,
|
||||
// example: Examples.Machine.id,
|
||||
// }),
|
||||
// hostname: z.string().openapi({
|
||||
// description: "The Linux hostname that identifies this machine",
|
||||
// example: Examples.Machine.hostname,
|
||||
// }),
|
||||
// fingerprint: z.string().openapi({
|
||||
// description: "A unique identifier derived from the machine's Linux machine ID.",
|
||||
// example: Examples.Machine.fingerprint,
|
||||
// }),
|
||||
// createdAt: z.string().or(z.number()).openapi({
|
||||
// description: "Represents a machine running on the Nestri network, containing its identifying information and metadata.",
|
||||
// example: Examples.Machine.createdAt,
|
||||
// })
|
||||
// })
|
||||
// .openapi({
|
||||
// ref: "Machine",
|
||||
// description: "Represents a physical or virtual machine connected to the Nestri network..",
|
||||
// example: Examples.Machine,
|
||||
// });
|
||||
|
||||
// export type Info = z.infer<typeof Info>;
|
||||
|
||||
// export const create = fn(Info.pick({ fingerprint: true, hostname: true }), async (input) => {
|
||||
// const id = createID()
|
||||
// const now = new Date().toISOString()
|
||||
// const db = databaseClient()
|
||||
// await db.transact(
|
||||
// db.tx.machines[id]!.update({
|
||||
// fingerprint: input.fingerprint,
|
||||
// hostname: input.hostname,
|
||||
// createdAt: now,
|
||||
// //Just in case it had been previously deleted
|
||||
// deletedAt: undefined
|
||||
// })
|
||||
// )
|
||||
|
||||
// return id
|
||||
// })
|
||||
|
||||
// // export const fromID = fn(z.string(), async (id) => {
|
||||
// const db = databaseClient()
|
||||
|
||||
// const query = {
|
||||
// machines: {
|
||||
// $: {
|
||||
// where: {
|
||||
// id: id,
|
||||
// deletedAt: { $isNull: true }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
// const machines = res.machines
|
||||
|
||||
// if (machines && machines.length > 0) {
|
||||
// const result = pipe(
|
||||
// machines,
|
||||
// groupBy(x => x.id),
|
||||
// values(),
|
||||
// map((group): Info => ({
|
||||
// id: group[0].id,
|
||||
// fingerprint: group[0].fingerprint,
|
||||
// hostname: group[0].hostname,
|
||||
// createdAt: group[0].createdAt
|
||||
// }))
|
||||
// )
|
||||
// return result
|
||||
// }
|
||||
|
||||
// return null
|
||||
// })
|
||||
|
||||
// export const installedGames = fn(z.string(), async (id) => {
|
||||
// const db = databaseClient()
|
||||
|
||||
// const query = {
|
||||
// machines: {
|
||||
// $: {
|
||||
// where: {
|
||||
// id: id,
|
||||
// deletedAt: { $isNull: true }
|
||||
// }
|
||||
// },
|
||||
// games: {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
// const machines = res.machines
|
||||
|
||||
// if (machines && machines.length > 0) {
|
||||
// const games = machines[0]?.games as any
|
||||
// if (games.length > 0) {
|
||||
// return games as Games.Info[]
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
|
||||
// return null
|
||||
// })
|
||||
|
||||
// export const fromFingerprint = fn(z.string(), async (input) => {
|
||||
// const db = databaseClient()
|
||||
|
||||
// const query = {
|
||||
// machines: {
|
||||
// $: {
|
||||
// where: {
|
||||
// fingerprint: input,
|
||||
// deletedAt: { $isNull: true }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
|
||||
// const machines = res.machines
|
||||
|
||||
// if (machines.length > 0) {
|
||||
// const result = pipe(
|
||||
// machines,
|
||||
// groupBy(x => x.id),
|
||||
// values(),
|
||||
// map((group): Info => ({
|
||||
// id: group[0].id,
|
||||
// fingerprint: group[0].fingerprint,
|
||||
// hostname: group[0].hostname,
|
||||
// createdAt: group[0].createdAt
|
||||
// }))
|
||||
// )
|
||||
// return result[0]
|
||||
// }
|
||||
|
||||
// return null
|
||||
// })
|
||||
|
||||
// export const list = async () => {
|
||||
// const user = useCurrentUser()
|
||||
// const db = databaseClient()
|
||||
|
||||
// const query = {
|
||||
// $users: {
|
||||
// $: { where: { id: user.id } },
|
||||
// machines: {
|
||||
// $: {
|
||||
// where: {
|
||||
// deletedAt: { $isNull: true }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
|
||||
// const machines = res.$users[0]?.machines
|
||||
// if (machines && machines.length > 0) {
|
||||
// const result = pipe(
|
||||
// machines,
|
||||
// groupBy(x => x.id),
|
||||
// values(),
|
||||
// map((group): Info => ({
|
||||
// id: group[0].id,
|
||||
// fingerprint: group[0].fingerprint,
|
||||
// hostname: group[0].hostname,
|
||||
// createdAt: group[0].createdAt
|
||||
// }))
|
||||
// )
|
||||
// return result
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
|
||||
// export const linkToCurrentUser = fn(z.string(), async (id) => {
|
||||
// const user = useCurrentUser()
|
||||
// const db = databaseClient()
|
||||
|
||||
// await db.transact(db.tx.machines[id]!.link({ owner: user.id }))
|
||||
|
||||
// return "ok"
|
||||
// })
|
||||
|
||||
// export const unLinkFromCurrentUser = fn(z.string(), async (id) => {
|
||||
// const user = useCurrentUser()
|
||||
// const db = databaseClient()
|
||||
// const now = new Date().toISOString()
|
||||
|
||||
// const query = {
|
||||
// $users: {
|
||||
// $: { where: { id: user.id } },
|
||||
// machines: {
|
||||
// $: {
|
||||
// where: {
|
||||
// id,
|
||||
// deletedAt: { $isNull: true }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
|
||||
// const res = await db.query(query)
|
||||
// const machines = res.$users[0]?.machines
|
||||
// if (machines && machines.length > 0) {
|
||||
// const machine = machines[0] as Info
|
||||
// await db.transact(db.tx.machines[machine.id]!.update({ deletedAt: now }))
|
||||
|
||||
// return "ok"
|
||||
// }
|
||||
|
||||
// return null
|
||||
// })
|
||||
|
||||
// }
|
||||
@@ -1,412 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import databaseClient from "../database";
|
||||
import { groupBy, map, pipe, values } from "remeda"
|
||||
import { id as createID, } from "@instantdb/admin";
|
||||
import { useCurrentUser } from "../actor";
|
||||
|
||||
export const userStatus = z.enum([
|
||||
"active", //online and playing a game
|
||||
"idle", //online and not playing
|
||||
"offline",
|
||||
]);
|
||||
|
||||
export module Profiles {
|
||||
const MAX_ATTEMPTS = 50;
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Machine.id,
|
||||
}),
|
||||
username: z.string().openapi({
|
||||
description: "The user's unique username",
|
||||
example: Examples.Profile.username,
|
||||
}),
|
||||
avatarUrl: z.string().or(z.undefined()).openapi({
|
||||
description: "The url to the profile picture.",
|
||||
example: Examples.Profile.username,
|
||||
}),
|
||||
status: userStatus.openapi({
|
||||
description: "Whether the user is active, idle or offline",
|
||||
example: Examples.Profile.status
|
||||
}),
|
||||
discriminator: z.string().or(z.number()).openapi({
|
||||
description: "The number discriminator for each username",
|
||||
example: Examples.Profile.discriminator,
|
||||
}),
|
||||
createdAt: z.string().or(z.number()).openapi({
|
||||
description: "The time when this profile was first created",
|
||||
example: Examples.Profile.createdAt,
|
||||
}),
|
||||
updatedAt: z.string().or(z.number()).openapi({
|
||||
description: "The time when this profile was last edited",
|
||||
example: Examples.Profile.updatedAt,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Profile",
|
||||
description: "Represents a profile of a user on Nestri",
|
||||
example: Examples.Profile,
|
||||
});
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
export type userStatus = z.infer<typeof userStatus>;
|
||||
|
||||
export const sanitizeUsername = (username: string): string => {
|
||||
// Remove spaces and numbers
|
||||
return username.replace(/[\s0-9]/g, '');
|
||||
};
|
||||
|
||||
export const generateDiscriminator = (): string => {
|
||||
return Math.floor(Math.random() * 100).toString().padStart(2, '0');
|
||||
};
|
||||
|
||||
export const isValidDiscriminator = (discriminator: string): boolean => {
|
||||
return /^\d{2}$/.test(discriminator);
|
||||
};
|
||||
|
||||
export const fromUsername = fn(z.string(), async (input) => {
|
||||
const sanitizedUsername = sanitizeUsername(input);
|
||||
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
where: {
|
||||
username: sanitizedUsername,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const profiles = res.profiles
|
||||
|
||||
if (!profiles || profiles.length == 0) {
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return pipe(
|
||||
profiles,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
username: group[0].username,
|
||||
createdAt: group[0].createdAt,
|
||||
discriminator: group[0].discriminator,
|
||||
updatedAt: group[0].updatedAt,
|
||||
status: group[0].status as userStatus
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
export const findAvailableDiscriminator = fn(z.string(), async (input) => {
|
||||
const db = databaseClient()
|
||||
const username = sanitizeUsername(input);
|
||||
|
||||
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
||||
const discriminator = generateDiscriminator();
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
where: {
|
||||
username,
|
||||
discriminator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await db.query(query)
|
||||
const profiles = res.profiles
|
||||
if (profiles.length === 0) {
|
||||
return discriminator;
|
||||
}
|
||||
}
|
||||
return null; // No available discriminators
|
||||
|
||||
})
|
||||
|
||||
export const create = fn(z.object({ username: z.string(), customDiscriminator: z.string().optional(), avatarUrl: z.string().optional(), owner: z.string() }), async (input) => {
|
||||
const username = sanitizeUsername(input.username);
|
||||
|
||||
const db = databaseClient()
|
||||
const id = createID()
|
||||
const now = new Date().toISOString()
|
||||
|
||||
let discriminator: string | null;
|
||||
if (input.customDiscriminator) {
|
||||
if (!isValidDiscriminator(input.customDiscriminator)) {
|
||||
console.error('Invalid discriminator format')
|
||||
return null
|
||||
// throw new Error('Invalid discriminator format');
|
||||
}
|
||||
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
where: {
|
||||
username,
|
||||
discriminator: input.customDiscriminator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
const profiles = res.profiles
|
||||
if (profiles.length != 0) {
|
||||
console.error("Username and discriminator combination already taken ")
|
||||
return null
|
||||
// throw new Error('Username and discriminator combination already taken');
|
||||
}
|
||||
|
||||
discriminator = input.customDiscriminator
|
||||
} else {
|
||||
// Generate a random available discriminator
|
||||
discriminator = await findAvailableDiscriminator(username);
|
||||
|
||||
if (!discriminator) {
|
||||
console.error("No available discriminators for this username ")
|
||||
return null
|
||||
// throw new Error('No available discriminators for this username');
|
||||
}
|
||||
}
|
||||
|
||||
return await db.transact(
|
||||
db.tx.profiles[id]!.update({
|
||||
username,
|
||||
avatarUrl: input.avatarUrl,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
discriminator,
|
||||
status: "idle"
|
||||
}).link({ owner: input.owner })
|
||||
)
|
||||
})
|
||||
|
||||
export const getFullUsername = async (username: string) => {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
where: {
|
||||
username,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await db.query(query)
|
||||
const profiles = res.profiles
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
console.error('User not found')
|
||||
return null
|
||||
// throw new Error('User not found');
|
||||
}
|
||||
|
||||
return `${profiles[0]?.username}#${profiles[0]?.discriminator}`;
|
||||
}
|
||||
|
||||
export const fromOwnerID = async (ownerID: string) => {
|
||||
try {
|
||||
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
where: {
|
||||
owner: ownerID
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
const res = await db.query(query)
|
||||
|
||||
const profiles = res.profiles
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
throw new Error("No profiles were found");
|
||||
}
|
||||
|
||||
const profile = pipe(
|
||||
profiles,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
username: group[0].username,
|
||||
createdAt: group[0].createdAt,
|
||||
updatedAt: group[0].updatedAt,
|
||||
avatarUrl: group[0].avatarUrl,
|
||||
discriminator: group[0].discriminator,
|
||||
status: group[0].status as userStatus
|
||||
}))
|
||||
)
|
||||
|
||||
return profile[0]
|
||||
} catch (error) {
|
||||
console.log("user fromOwnerID", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const fromID = async (id: string) => {
|
||||
try {
|
||||
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
where: {
|
||||
id
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
const res = await db.query(query)
|
||||
|
||||
const profiles = res.profiles
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
throw new Error("No profiles were found");
|
||||
}
|
||||
|
||||
const profile = pipe(
|
||||
profiles,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
username: group[0].username,
|
||||
createdAt: group[0].createdAt,
|
||||
updatedAt: group[0].updatedAt,
|
||||
avatarUrl: group[0].avatarUrl,
|
||||
discriminator: group[0].discriminator,
|
||||
status: group[0].status as userStatus
|
||||
}))
|
||||
)
|
||||
|
||||
return profile[0]
|
||||
} catch (error) {
|
||||
console.log("user fromID", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const fromIDToOwner = async (id: string) => {
|
||||
try {
|
||||
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
where: {
|
||||
id
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
const res = await db.query(query)
|
||||
|
||||
const profiles = res.profiles as any
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
throw new Error("No profiles were found");
|
||||
}
|
||||
|
||||
return profiles[0]!.owner as string
|
||||
} catch (error) {
|
||||
console.log("user fromID", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
export const getCurrentProfile = async () => {
|
||||
const user = useCurrentUser()
|
||||
const currentProfile = await fromOwnerID(user.id);
|
||||
|
||||
return currentProfile
|
||||
}
|
||||
|
||||
export const setStatus = fn(userStatus, async (status) => {
|
||||
try {
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient()
|
||||
|
||||
const now = new Date().toISOString()
|
||||
|
||||
await db.transact(
|
||||
db.tx.profiles[user.id]!.update({
|
||||
status,
|
||||
updatedAt: now
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.log("user setStatus error", error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
export const list = async () => {
|
||||
try {
|
||||
const db = databaseClient()
|
||||
// const ago = new Date(Date.now() - (60 * 1000 * 30)).toISOString()
|
||||
const ago = new Date(Date.now() - (24 * 60 * 60 * 1000)).toISOString()
|
||||
|
||||
const query = {
|
||||
profiles: {
|
||||
$: {
|
||||
limit: 10,
|
||||
where: {
|
||||
updatedAt: { $gt: ago },
|
||||
},
|
||||
order: {
|
||||
updatedAt: "desc" as const,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const profiles = res.profiles
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
throw new Error("No profiles were found");
|
||||
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
profiles,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
username: group[0].username,
|
||||
createdAt: group[0].createdAt,
|
||||
updatedAt: group[0].updatedAt,
|
||||
avatarUrl: group[0].avatarUrl,
|
||||
discriminator: group[0].discriminator,
|
||||
status: group[0].status as userStatus
|
||||
}))
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
console.log("user list error", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import databaseClient from "../database"
|
||||
import { useCurrentUser } from "../actor";
|
||||
import { groupBy, map, pipe, values } from "remeda"
|
||||
import { id as createID } from "@instantdb/admin";
|
||||
|
||||
export module Sessions {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Session.id,
|
||||
}),
|
||||
public: z.boolean().openapi({
|
||||
description: "If true, the session is publicly viewable by all users. If false, only authorized users can access it",
|
||||
example: Examples.Session.public,
|
||||
}),
|
||||
endedAt: z.string().or(z.number()).or(z.undefined()).openapi({
|
||||
description: "The timestamp indicating when this session was completed or terminated. Null if session is still active.",
|
||||
example: Examples.Session.endedAt,
|
||||
}),
|
||||
startedAt: z.string().or(z.number()).openapi({
|
||||
description: "The timestamp indicating when this session started.",
|
||||
example: Examples.Session.startedAt,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Session",
|
||||
description: "Represents a single game play session, tracking its lifetime and accessibility settings.",
|
||||
example: Examples.Session,
|
||||
});
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
|
||||
export const create = fn(z.object({ public: z.boolean() }), async (input) => {
|
||||
try {
|
||||
const id = createID()
|
||||
const db = databaseClient()
|
||||
const user = useCurrentUser()
|
||||
const now = new Date().toISOString()
|
||||
|
||||
await db.transact(
|
||||
db.tx.sessions[id]!.update({
|
||||
public: input.public,
|
||||
startedAt: now,
|
||||
}).link({ owner: user.id })
|
||||
)
|
||||
|
||||
return id
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
export const getActive = async () => {
|
||||
try {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
sessions: {
|
||||
$: {
|
||||
where: {
|
||||
endedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const sessions = res.sessions
|
||||
if (!sessions || sessions.length === 0) {
|
||||
throw new Error("No active sessions found")
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
sessions,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
endedAt: group[0].endedAt,
|
||||
startedAt: group[0].startedAt,
|
||||
public: group[0].public,
|
||||
}))
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const fromID = fn(z.string(), async (id) => {
|
||||
try {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
sessions: {
|
||||
$: {
|
||||
where: {
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
const sessions = res.sessions
|
||||
|
||||
if (!sessions || sessions.length === 0) {
|
||||
throw new Error("No sessions were found");
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
sessions,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
endedAt: group[0].endedAt,
|
||||
startedAt: group[0].startedAt,
|
||||
public: group[0].public,
|
||||
}))
|
||||
)
|
||||
return result
|
||||
} catch (err) {
|
||||
console.log("sessions error", err)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
export const fromTaskID = fn(z.string(), async (taskID) => {
|
||||
try {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
sessions: {
|
||||
$: {
|
||||
where: {
|
||||
task: taskID,
|
||||
endedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
const sessions = res.sessions
|
||||
|
||||
if (!sessions || sessions.length === 0) {
|
||||
throw new Error("No sessions were found");
|
||||
}
|
||||
console.log("sessions", sessions)
|
||||
|
||||
const result = pipe(
|
||||
sessions,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
endedAt: group[0].endedAt,
|
||||
startedAt: group[0].startedAt,
|
||||
public: group[0].public,
|
||||
}))
|
||||
)
|
||||
return result[0]
|
||||
} catch (err) {
|
||||
console.log("sessions error", err)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
export const end = fn(z.string(), async (id) => {
|
||||
const user = useCurrentUser()
|
||||
try {
|
||||
const db = databaseClient()
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const query = {
|
||||
sessions: {
|
||||
$: {
|
||||
where: {
|
||||
owner: user.id,
|
||||
id,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
const sessions = res.sessions
|
||||
if (!sessions || sessions.length === 0) {
|
||||
throw new Error("No sessions were found");
|
||||
}
|
||||
|
||||
await db.transact(db.tx.sessions[sessions[0]!.id]!.update({ endedAt: now }))
|
||||
|
||||
return "ok"
|
||||
|
||||
} catch (error) {
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
export const fromOwnerID = fn(z.string(), async (id) => {
|
||||
try {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
sessions: {
|
||||
$: {
|
||||
where: {
|
||||
owner: id,
|
||||
endedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
const sessions = res.sessions
|
||||
|
||||
if (!sessions || sessions.length === 0) {
|
||||
throw new Error("No sessions were found");
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
sessions,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
endedAt: group[0].endedAt,
|
||||
startedAt: group[0].startedAt,
|
||||
public: group[0].public,
|
||||
}))
|
||||
)
|
||||
return result[0]
|
||||
} catch (err) {
|
||||
console.log("session owner error", err)
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import databaseClient from "../database"
|
||||
import { fn } from "../utils";
|
||||
import { groupBy, map, pipe, values } from "remeda"
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import { useCurrentUser } from "../actor";
|
||||
import { id as createID } from "@instantdb/admin";
|
||||
import { Email } from "../email";
|
||||
import { Profiles } from "../profile";
|
||||
|
||||
export const SubscriptionFrequency = z.enum([
|
||||
"fixed",
|
||||
"daily",
|
||||
"weekly",
|
||||
"monthly",
|
||||
"yearly",
|
||||
]);
|
||||
|
||||
export type SubscriptionFrequency = z.infer<typeof SubscriptionFrequency>;
|
||||
|
||||
export namespace Subscriptions {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Subscription.id,
|
||||
}),
|
||||
checkoutID: z.string().openapi({
|
||||
description: "The polar.sh checkout id",
|
||||
example: Examples.Subscription.checkoutID,
|
||||
}),
|
||||
// productID: z.string().openapi({
|
||||
// description: "ID of the product being subscribed to.",
|
||||
// example: Examples.Subscription.productID,
|
||||
// }),
|
||||
// quantity: z.number().int().openapi({
|
||||
// description: "Quantity of the subscription.",
|
||||
// example: Examples.Subscription.quantity,
|
||||
// }),
|
||||
// frequency: SubscriptionFrequency.openapi({
|
||||
// description: "Frequency of the subscription.",
|
||||
// example: Examples.Subscription.frequency,
|
||||
// }),
|
||||
// next: z.string().or(z.number()).openapi({
|
||||
// description: "Next billing date for the subscription.",
|
||||
// example: Examples.Subscription.next,
|
||||
// }),
|
||||
canceledAt: z.string().or(z.number()).optional().openapi({
|
||||
description: "Cancelled date for the subscription.",
|
||||
example: Examples.Subscription.canceledAt,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Subscription",
|
||||
description: "Subscription to a Nestri product.",
|
||||
example: Examples.Subscription,
|
||||
});
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
|
||||
export const list = fn(z.string().optional(), async (userID) => {
|
||||
const db = databaseClient()
|
||||
const user = userID ? userID : useCurrentUser().id
|
||||
|
||||
const query = {
|
||||
subscriptions: {
|
||||
$: {
|
||||
where: {
|
||||
owner: user,
|
||||
canceledAt: { $isNull: true }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const response = res.subscriptions
|
||||
if (!response || response.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
response,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
// next: group[0].next,
|
||||
// frequency: group[0].frequency as any,
|
||||
// quantity: group[0].quantity,
|
||||
// productID: group[0].productID,
|
||||
checkoutID: group[0].checkoutID,
|
||||
}))
|
||||
)
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export const create = fn(Info.omit({ id: true, canceledAt: true }), async (input) => {
|
||||
// const id = createID()
|
||||
const id = createID()
|
||||
const db = databaseClient()
|
||||
const user = useCurrentUser()
|
||||
|
||||
//Use the polar.sh ID
|
||||
await db.transact(db.tx.subscriptions[id]!.update({
|
||||
// next: input.next,
|
||||
// frequency: input.frequency,
|
||||
// quantity: input.quantity,
|
||||
checkoutID: input.checkoutID,
|
||||
}).link({ owner: user.id }))
|
||||
const res = await db.auth.getUser({ id: user.id })
|
||||
const profile = await Profiles.fromOwnerID(user.id)
|
||||
if (profile) {
|
||||
await Email.sendWelcome(res.email, profile.username)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
export const remove = fn(z.string(), async (id) => {
|
||||
const db = databaseClient()
|
||||
|
||||
await db.transact(db.tx.subscriptions[id]!.update({
|
||||
canceledAt: new Date().toISOString()
|
||||
}))
|
||||
})
|
||||
|
||||
export const fromID = fn(z.string(), async (id) => {
|
||||
const db = databaseClient()
|
||||
const user = useCurrentUser()
|
||||
const query = {
|
||||
subscriptions: {
|
||||
$: {
|
||||
where: {
|
||||
id,
|
||||
//Make sure they can only get subscriptions they own
|
||||
owner: user.id,
|
||||
canceledAt: { $isNull: true }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const response = res.subscriptions
|
||||
if (!response || response.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
response,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
checkoutID: group[0].checkoutID,
|
||||
// next: group[0].next,
|
||||
// frequency: group[0].frequency as any,
|
||||
// quantity: group[0].quantity,
|
||||
// productID: group[0].productID,
|
||||
}))
|
||||
)
|
||||
|
||||
return result[0]
|
||||
})
|
||||
|
||||
export const fromCheckoutID = fn(z.string(), async (id) => {
|
||||
const db = databaseClient()
|
||||
const user = useCurrentUser()
|
||||
const query = {
|
||||
subscriptions: {
|
||||
$: {
|
||||
where: {
|
||||
id,
|
||||
//Make sure they can only get subscriptions they own
|
||||
checkoutID: id,
|
||||
canceledAt: { $isNull: true }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const response = res.subscriptions
|
||||
if (!response || response.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
response,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
checkoutID: group[0].checkoutID,
|
||||
}))
|
||||
)
|
||||
|
||||
return result[0]
|
||||
})
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { fn } from "../utils";
|
||||
import { Resource } from "sst";
|
||||
import { Aws } from "../aws/client";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import databaseClient from "../database"
|
||||
import { useCurrentUser } from "../actor";
|
||||
import { id as createID } from "@instantdb/admin";
|
||||
import { groupBy, map, pipe, values } from "remeda"
|
||||
import { Sessions } from "../session";
|
||||
|
||||
export const lastStatus = z.enum([
|
||||
"RUNNING",
|
||||
"PENDING",
|
||||
"UNKNOWN",
|
||||
"STOPPED",
|
||||
]);
|
||||
|
||||
export const taskType = z.enum([
|
||||
"AWS",
|
||||
"ON_PREMISES",
|
||||
"UNKNOWN"
|
||||
]);
|
||||
|
||||
export const healthStatus = z.enum([
|
||||
"HEALTHY",
|
||||
"UNHEALTHY",
|
||||
"UNKNOWN",
|
||||
]);
|
||||
|
||||
export type taskType = z.infer<typeof taskType>;
|
||||
export type lastStatus = z.infer<typeof lastStatus>;
|
||||
export type healthStatus = z.infer<typeof healthStatus>;
|
||||
|
||||
export module Tasks {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Task.id,
|
||||
}),
|
||||
type: taskType.openapi({
|
||||
description: "Where this task is hosted on",
|
||||
example: Examples.Task.type,
|
||||
}),
|
||||
taskID: z.string().openapi({
|
||||
description: "The id of this task as seen on AWS",
|
||||
example: Examples.Task.taskID,
|
||||
}),
|
||||
startedAt: z.string().or(z.number()).openapi({
|
||||
description: "The time this task was started",
|
||||
example: Examples.Task.startedAt,
|
||||
}),
|
||||
lastUpdated: z.string().or(z.number()).openapi({
|
||||
description: "The time the information about this task was last updated",
|
||||
example: Examples.Task.lastUpdated,
|
||||
}),
|
||||
stoppedAt: z.string().or(z.number()).optional().openapi({
|
||||
description: "The time this task was stopped or quit",
|
||||
example: Examples.Task.lastUpdated,
|
||||
}),
|
||||
lastStatus: lastStatus.openapi({
|
||||
description: "The last registered status of this task",
|
||||
example: Examples.Task.lastStatus,
|
||||
}),
|
||||
healthStatus: healthStatus.openapi({
|
||||
description: "The health status of this task",
|
||||
example: Examples.Task.healthStatus,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Subscription",
|
||||
description: "Subscription to a Nestri product.",
|
||||
example: Examples.Task,
|
||||
});
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
|
||||
export const list = async () => {
|
||||
const db = databaseClient()
|
||||
const user = useCurrentUser()
|
||||
|
||||
try {
|
||||
const query = {
|
||||
tasks: {
|
||||
$: {
|
||||
where: {
|
||||
stoppedAt: { $isNull: true },
|
||||
owner: user.id
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await db.query(query)
|
||||
|
||||
const response = data.tasks
|
||||
if (!response || response.length === 0) {
|
||||
throw new Error("No task for this user were found");
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
response,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
taskID: group[0].taskID,
|
||||
type: group[0].type as taskType,
|
||||
lastStatus: group[0].lastStatus as lastStatus,
|
||||
healthStatus: group[0].healthStatus as healthStatus,
|
||||
startedAt: group[0].startedAt,
|
||||
stoppedAt: group[0].stoppedAt,
|
||||
lastUpdated: group[0].lastUpdated,
|
||||
}))
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const create = async () => {
|
||||
const user = useCurrentUser()
|
||||
|
||||
try {
|
||||
|
||||
//TODO: Use a simpler way to set the session ID
|
||||
// const sessionID = createID()
|
||||
|
||||
const sessionID = await Sessions.create({ public: true })
|
||||
if (!sessionID) throw new Error("No session id was given");
|
||||
|
||||
const run = await Aws.EcsRunTask({
|
||||
count: 1,
|
||||
cluster: Resource.NestriGPUCluster.value,
|
||||
taskDefinition: Resource.NestriGPUTask.value,
|
||||
launchType: "EC2",
|
||||
overrides: {
|
||||
containerOverrides: [
|
||||
{
|
||||
name: "nestri",
|
||||
environment: [
|
||||
{
|
||||
name: "NESTRI_ROOM",
|
||||
value: sessionID
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
if (!run.tasks || run.tasks.length === 0) {
|
||||
throw new Error(`No tasks were started`);
|
||||
}
|
||||
|
||||
// Extract task details
|
||||
const task = run.tasks[0];
|
||||
const taskArn = task?.taskArn!;
|
||||
const taskId = taskArn.split('/').pop()!; // Extract task ID from ARN
|
||||
const taskStatus = task?.lastStatus;
|
||||
const taskHealthStatus = task?.healthStatus;
|
||||
const startedAt = task?.startedAt!;
|
||||
|
||||
const id = createID()
|
||||
const db = databaseClient()
|
||||
const now = new Date().toISOString()
|
||||
await db.transact(db.tx.tasks[id]!.update({
|
||||
taskID: taskId,
|
||||
type: "AWS",
|
||||
healthStatus: taskHealthStatus ? taskHealthStatus.toString() : "UNKNOWN",
|
||||
startedAt: startedAt ? startedAt.toISOString() : now,
|
||||
lastStatus: taskStatus,
|
||||
lastUpdated: now,
|
||||
}).link({ owner: user.id, sessions: sessionID }))
|
||||
|
||||
return id
|
||||
} catch (e) {
|
||||
console.error("error", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const fromID = fn(z.string(), async (taskID) => {
|
||||
const db = databaseClient()
|
||||
try {
|
||||
const query = {
|
||||
tasks: {
|
||||
$: {
|
||||
where: {
|
||||
id: taskID,
|
||||
stoppedAt: { $isNull: true }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await db.query(query)
|
||||
|
||||
const response = data.tasks
|
||||
if (!response || response.length === 0) {
|
||||
throw new Error("No task with the given id was found");
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
response,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
taskID: group[0].taskID,
|
||||
type: group[0].type as taskType,
|
||||
lastStatus: group[0].lastStatus as lastStatus,
|
||||
healthStatus: group[0].healthStatus as healthStatus,
|
||||
startedAt: group[0].startedAt,
|
||||
stoppedAt: group[0].stoppedAt,
|
||||
lastUpdated: group[0].lastUpdated,
|
||||
}))
|
||||
)
|
||||
|
||||
return result[0]
|
||||
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
export const update = fn(z.string(), async (taskID) => {
|
||||
try {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
tasks: {
|
||||
$: {
|
||||
where: {
|
||||
id: taskID,
|
||||
stoppedAt: { $isNull: true }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await db.query(query)
|
||||
|
||||
const response = data.tasks
|
||||
if (!response || response.length === 0) {
|
||||
throw new Error("No task with the given taskID was found");
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const describeResponse = await Aws.EcsDescribeTasks({
|
||||
tasks: [response[0]!.taskID],
|
||||
cluster: Resource.NestriGPUCluster.value
|
||||
})
|
||||
|
||||
if (!describeResponse.tasks || describeResponse.tasks.length === 0) {
|
||||
throw new Error("No tasks were found");
|
||||
}
|
||||
|
||||
const task = describeResponse.tasks[0]!
|
||||
|
||||
const updatedDb = {
|
||||
healthStatus: task.healthStatus ? task.healthStatus : "UNKNOWN",
|
||||
lastStatus: task.lastStatus ? task.lastStatus : "UNKNOWN",
|
||||
lastUpdated: now,
|
||||
}
|
||||
|
||||
await db.transact(db.tx.tasks[response[0]!.id]!.update({
|
||||
...updatedDb
|
||||
}))
|
||||
|
||||
const updatedRes = [{ ...response[0]!, ...updatedDb }]
|
||||
|
||||
const result = pipe(
|
||||
updatedRes,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
taskID: group[0].taskID,
|
||||
type: group[0].type as taskType,
|
||||
lastStatus: group[0].lastStatus as lastStatus,
|
||||
healthStatus: group[0].healthStatus as healthStatus,
|
||||
startedAt: group[0].startedAt,
|
||||
stoppedAt: group[0].stoppedAt,
|
||||
lastUpdated: group[0].lastUpdated,
|
||||
}))
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
console.error("update error", error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
export const stop = fn(z.object({ taskID: z.string(), id: z.string() }), async (input) => {
|
||||
const db = databaseClient()
|
||||
const now = new Date().toISOString()
|
||||
try {
|
||||
//TODO:Check whether they own this task first
|
||||
|
||||
const stopResponse = await Aws.EcsStopTask({
|
||||
task: input.taskID,
|
||||
cluster: Resource.NestriGPUCluster.value,
|
||||
reason: "Client requested a shutdown"
|
||||
})
|
||||
|
||||
if (!stopResponse.task) {
|
||||
throw new Error(`No task was stopped`);
|
||||
}
|
||||
|
||||
await db.transact(db.tx.tasks[input.id]!.update({
|
||||
stoppedAt: now,
|
||||
lastUpdated: now,
|
||||
lastStatus: "STOPPED",
|
||||
healthStatus: "UNKNOWN"
|
||||
}))
|
||||
|
||||
return "ok"
|
||||
|
||||
} catch (error) {
|
||||
console.error("stop error", error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import databaseClient from "../database"
|
||||
import { fn } from "../utils";
|
||||
import { groupBy, map, pipe, values } from "remeda"
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import { useCurrentUser } from "../actor";
|
||||
import { id as createID } from "@instantdb/admin";
|
||||
|
||||
export namespace Teams {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Team.id,
|
||||
}),
|
||||
name: z.string().openapi({
|
||||
description: "Name of the team",
|
||||
example: Examples.Team.name,
|
||||
}),
|
||||
createdAt: z.string().or(z.number()).openapi({
|
||||
description: "The time when this team was first created",
|
||||
example: Examples.Team.createdAt,
|
||||
}),
|
||||
updatedAt: z.string().or(z.number()).openapi({
|
||||
description: "The time when this team was last edited",
|
||||
example: Examples.Team.updatedAt,
|
||||
}),
|
||||
// owner: z.boolean().openapi({
|
||||
// description: "Whether this team is owned by this user",
|
||||
// example: Examples.Team.owner,
|
||||
// }),
|
||||
slug: z.string().openapi({
|
||||
description: "This is the unique name identifier for the team",
|
||||
example: Examples.Team.slug
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Team",
|
||||
description: "A group of users sharing the same machines for gaming.",
|
||||
example: Examples.Team,
|
||||
});
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
|
||||
export const list = async () => {
|
||||
const db = databaseClient()
|
||||
const user = useCurrentUser()
|
||||
|
||||
const query = {
|
||||
teams: {
|
||||
$: {
|
||||
where: {
|
||||
members: user.id,
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const teams = res.teams
|
||||
if (!teams || teams.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
teams,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
name: group[0].name,
|
||||
createdAt: group[0].createdAt,
|
||||
updatedAt: group[0].updatedAt,
|
||||
slug: group[0].slug,
|
||||
//@ts-expect-error
|
||||
owner: group[0].owner === user.id
|
||||
}))
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
export const fromSlug = fn(z.string(), async (slug) => {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
teams: {
|
||||
$: {
|
||||
where: {
|
||||
slug,
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
const teams = res.teams
|
||||
if (!teams || teams.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
teams,
|
||||
groupBy(x => x.id),
|
||||
values(),
|
||||
map((group): Info => ({
|
||||
id: group[0].id,
|
||||
name: group[0].name,
|
||||
slug: group[0].slug,
|
||||
createdAt: group[0].createdAt,
|
||||
updatedAt: group[0].updatedAt,
|
||||
// owner: group[0].owner === user.id
|
||||
}))
|
||||
)
|
||||
|
||||
return result[0]
|
||||
})
|
||||
|
||||
export const create = fn(Info.pick({ name: true, slug: true }), async (input) => {
|
||||
const id = createID()
|
||||
const db = databaseClient()
|
||||
const user = useCurrentUser()
|
||||
const now = new Date().toISOString()
|
||||
|
||||
await db.transact(db.tx.teams[id]!.update({
|
||||
name: input.name,
|
||||
slug: input.slug,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).link({ owner: user.id, members: user.id }))
|
||||
|
||||
return id
|
||||
})
|
||||
|
||||
export const remove = fn(z.string(), async (id) => {
|
||||
const db = databaseClient()
|
||||
const now = new Date().toISOString()
|
||||
|
||||
await db.transact(db.tx.teams[id]!.update({
|
||||
deletedAt: now
|
||||
}))
|
||||
|
||||
return "ok"
|
||||
})
|
||||
|
||||
export const invite = fn(z.object({email:z.string(), id: z.string()}), async (input) => {
|
||||
//TODO:
|
||||
// const db = databaseClient()
|
||||
// const now = new Date().toISOString()
|
||||
|
||||
// await db.transact(db.tx.teams[id]!.update({
|
||||
// deletedAt: now
|
||||
// }))
|
||||
|
||||
return "ok"
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export interface CloudflareCF {
|
||||
colo: string;
|
||||
continent: string;
|
||||
country: string,
|
||||
city: string;
|
||||
region: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
metroCode: string;
|
||||
postalCode: string;
|
||||
timezone: string;
|
||||
regionCode: number;
|
||||
}
|
||||
|
||||
export interface CFRequest extends Request {
|
||||
cf: CloudflareCF
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import databaseClient from "../database"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
|
||||
export module Users {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.User.id,
|
||||
}),
|
||||
email: z.string().nullable().openapi({
|
||||
description: "Email address of the user.",
|
||||
example: Examples.User.email,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "User",
|
||||
description: "A Nestri console user.",
|
||||
example: Examples.User,
|
||||
});
|
||||
|
||||
export const fromEmail = fn(z.string(), async (email) => {
|
||||
const db = databaseClient()
|
||||
const res = await db.auth.getUser({ email })
|
||||
return res
|
||||
})
|
||||
|
||||
export const create = fn(z.string(), async (email) => {
|
||||
const db = databaseClient()
|
||||
const token = await db.auth.createToken(email)
|
||||
|
||||
return token
|
||||
})
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ZodSchema, z } from "zod";
|
||||
|
||||
export function fn<
|
||||
Arg1 extends ZodSchema,
|
||||
Callback extends (arg1: z.output<Arg1>) => any,
|
||||
>(arg1: Arg1, cb: Callback) {
|
||||
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
|
||||
const parsed = arg1.parse(input);
|
||||
return cb.apply(cb, [parsed as any]);
|
||||
};
|
||||
result.schema = arg1;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function doubleFn<
|
||||
Arg1 extends ZodSchema,
|
||||
Arg2 extends ZodSchema,
|
||||
Callback extends (arg1: z.output<Arg1>, arg2: z.output<Arg2>) => any,
|
||||
>(arg1: Arg1, arg2: Arg2, cb: Callback) {
|
||||
const result = function (input: z.input<typeof arg1>, input2: z.input<typeof arg2>): ReturnType<Callback> {
|
||||
const parsed = arg1.parse(input);
|
||||
const parsed2 = arg2.parse(input2);
|
||||
return cb.apply(cb, [parsed as any, parsed2 as any]);
|
||||
};
|
||||
result.schema = arg1;
|
||||
return result;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export const prefixes = {
|
||||
user: "usr",
|
||||
} as const;
|
||||
|
||||
export function createID(prefix: keyof typeof prefixes): string {
|
||||
return [prefixes[prefix], ulid()].join("_");
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./fn"
|
||||
export * from "./id"
|
||||
1049
packages/server/Cargo.lock
generated
1049
packages/server/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nestri-server"
|
||||
version = "0.1.0-alpha.2"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "nestri-server"
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct RateControlCQP {
|
||||
/// Constant Quantization Parameter (CQP) quality level
|
||||
pub quality: u32,
|
||||
}
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct RateControlVBR {
|
||||
/// Target bitrate in kbps
|
||||
pub target_bitrate: i32,
|
||||
/// Maximum bitrate in kbps
|
||||
pub max_bitrate: i32,
|
||||
}
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct RateControlCBR {
|
||||
/// Target bitrate in kbps
|
||||
pub target_bitrate: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum RateControl {
|
||||
/// Constant Quantization Parameter
|
||||
CQP(RateControlCQP),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::gpu::{self, get_gpu_by_card_path, get_gpus_by_vendor, GPUInfo};
|
||||
use crate::args::encoding_args::RateControl;
|
||||
use crate::gpu::{self, GPUInfo, get_gpu_by_card_path, get_gpus_by_vendor};
|
||||
use gst::prelude::*;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
@@ -245,7 +246,10 @@ pub fn encoder_gop_params(encoder: &VideoEncoderInfo, gop_size: u32) -> VideoEnc
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encoder_low_latency_params(encoder: &VideoEncoderInfo) -> VideoEncoderInfo {
|
||||
pub fn encoder_low_latency_params(
|
||||
encoder: &VideoEncoderInfo,
|
||||
rate_control: &RateControl,
|
||||
) -> VideoEncoderInfo {
|
||||
let mut encoder_optz = encoder_gop_params(encoder, 30);
|
||||
|
||||
match encoder_optz.encoder_api {
|
||||
@@ -283,13 +287,8 @@ pub fn encoder_low_latency_params(encoder: &VideoEncoderInfo) -> VideoEncoderInf
|
||||
encoder_optz.set_parameter("tune", "zerolatency");
|
||||
}
|
||||
"svtav1enc" => {
|
||||
encoder_optz.set_parameter("preset", "12");
|
||||
let suffix = if encoder_optz.get_parameters_string().contains("cbr") {
|
||||
":pred-struct=1"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
encoder_optz.set_parameter("parameters-string", &format!("lookahead=0{}", suffix));
|
||||
encoder_optz.set_parameter("preset", "11");
|
||||
encoder_optz.set_parameter("parameters-string", "lookahead=0");
|
||||
}
|
||||
"av1enc" => {
|
||||
encoder_optz.set_parameter("usage-profile", "realtime");
|
||||
|
||||
@@ -4,13 +4,13 @@ mod gpu;
|
||||
mod latency;
|
||||
mod messages;
|
||||
mod nestrisink;
|
||||
mod websocket;
|
||||
mod proto;
|
||||
mod websocket;
|
||||
|
||||
use crate::args::encoding_args;
|
||||
use crate::gpu::GPUVendor;
|
||||
use crate::nestrisink::NestriSignaller;
|
||||
use crate::websocket::NestriWebSocket;
|
||||
use crate::gpu::GPUVendor;
|
||||
use futures_util::StreamExt;
|
||||
use gst::prelude::*;
|
||||
use gstrswebrtc::signaller::Signallable;
|
||||
@@ -55,12 +55,19 @@ fn handle_gpus(args: &args::Args) -> Option<gpu::GPUInfo> {
|
||||
gpu = filtered_gpus.get(args.device.gpu_index as usize).cloned();
|
||||
} else {
|
||||
// get first GPU
|
||||
gpu = filtered_gpus.into_iter().find(|g| *g.vendor() != GPUVendor::UNKNOWN);
|
||||
gpu = filtered_gpus
|
||||
.into_iter()
|
||||
.find(|g| *g.vendor() != GPUVendor::UNKNOWN);
|
||||
}
|
||||
}
|
||||
if gpu.is_none() {
|
||||
println!("No GPU found with the specified parameters: vendor='{}', name='{}', index='{}', card_path='{}'",
|
||||
args.device.gpu_vendor, args.device.gpu_name, args.device.gpu_index, args.device.gpu_card_path);
|
||||
println!(
|
||||
"No GPU found with the specified parameters: vendor='{}', name='{}', index='{}', card_path='{}'",
|
||||
args.device.gpu_vendor,
|
||||
args.device.gpu_name,
|
||||
args.device.gpu_index,
|
||||
args.device.gpu_card_path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let gpu = gpu.unwrap();
|
||||
@@ -83,7 +90,11 @@ fn handle_encoder_video(args: &args::Args) -> Option<enc_helper::VideoEncoderInf
|
||||
encoder.codec.to_str(),
|
||||
encoder.encoder_api.to_str(),
|
||||
encoder.encoder_type.to_str(),
|
||||
if let Some(gpu) = &encoder.gpu_info { gpu.device_name() } else { "CPU" },
|
||||
if let Some(gpu) = &encoder.gpu_info {
|
||||
gpu.device_name()
|
||||
} else {
|
||||
"CPU"
|
||||
},
|
||||
);
|
||||
}
|
||||
// Pick most suitable video encoder based on given arguments
|
||||
@@ -99,8 +110,12 @@ fn handle_encoder_video(args: &args::Args) -> Option<enc_helper::VideoEncoderInf
|
||||
);
|
||||
}
|
||||
if video_encoder.is_none() {
|
||||
println!("No video encoder found with the specified parameters: name='{}', vcodec='{}', type='{}'",
|
||||
args.encoding.video.encoder, args.encoding.video.codec, args.encoding.video.encoder_type);
|
||||
println!(
|
||||
"No video encoder found with the specified parameters: name='{}', vcodec='{}', type='{}'",
|
||||
args.encoding.video.encoder,
|
||||
args.encoding.video.codec,
|
||||
args.encoding.video.encoder_type
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let video_encoder = video_encoder.unwrap();
|
||||
@@ -113,7 +128,8 @@ fn handle_encoder_video_settings(
|
||||
args: &args::Args,
|
||||
video_encoder: &enc_helper::VideoEncoderInfo,
|
||||
) -> enc_helper::VideoEncoderInfo {
|
||||
let mut optimized_encoder = enc_helper::encoder_low_latency_params(&video_encoder);
|
||||
let mut optimized_encoder =
|
||||
enc_helper::encoder_low_latency_params(&video_encoder, &args.encoding.video.rate_control);
|
||||
// Handle rate-control method
|
||||
match &args.encoding.video.rate_control {
|
||||
encoding_args::RateControl::CQP(cqp) => {
|
||||
@@ -240,14 +256,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
&match &args.encoding.audio.rate_control {
|
||||
encoding_args::RateControl::CBR(cbr) => cbr.target_bitrate * 1000i32,
|
||||
encoding_args::RateControl::VBR(vbr) => vbr.target_bitrate * 1000i32,
|
||||
_ => 128i32,
|
||||
_ => 128000i32,
|
||||
},
|
||||
);
|
||||
|
||||
/* Video */
|
||||
// Video Source Element
|
||||
let video_source = gst::ElementFactory::make("waylanddisplaysrc").build()?;
|
||||
video_source.set_property("render-node", &gpu.render_path());
|
||||
video_source.set_property_from_str("render-node", gpu.render_path());
|
||||
|
||||
// Caps Filter Element (resolution, fps)
|
||||
let caps_filter = gst::ElementFactory::make("capsfilter").build()?;
|
||||
@@ -289,6 +305,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
|
||||
webrtcsink.set_property_from_str("stun-server", "stun://stun.l.google.com:19302");
|
||||
webrtcsink.set_property_from_str("congestion-control", "disabled");
|
||||
webrtcsink.set_property("do-retransmission", false);
|
||||
|
||||
// Add elements to the pipeline
|
||||
pipeline.add_many(&[
|
||||
@@ -343,7 +360,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
|
||||
// Optimize latency of pipeline
|
||||
video_source.sync_state_with_parent().expect("failed to sync with parent");
|
||||
video_source
|
||||
.sync_state_with_parent()
|
||||
.expect("failed to sync with parent");
|
||||
video_source.set_property("do-timestamp", &true);
|
||||
audio_source.set_property("do-timestamp", &true);
|
||||
pipeline.set_property("latency", &0u64);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::messages::{
|
||||
decode_message_as, encode_message, AnswerType, JoinerType, MessageAnswer, MessageBase,
|
||||
MessageICE, MessageJoin, MessageSDP,
|
||||
AnswerType, JoinerType, MessageAnswer, MessageBase, MessageICE, MessageJoin, MessageSDP,
|
||||
decode_message_as, encode_message,
|
||||
};
|
||||
use crate::proto::proto::proto_input::InputType::{
|
||||
KeyDown, KeyUp, MouseKeyDown, MouseKeyUp, MouseMove, MouseMoveAbs, MouseWheel,
|
||||
@@ -10,7 +10,7 @@ use crate::websocket::NestriWebSocket;
|
||||
use glib::subclass::prelude::*;
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst_webrtc::{gst_sdp, WebRTCSDPType, WebRTCSessionDescription};
|
||||
use gst_webrtc::{WebRTCSDPType, WebRTCSessionDescription, gst_sdp};
|
||||
use gstrswebrtc::signaller::{Signallable, SignallableImpl};
|
||||
use prost::Message;
|
||||
use std::collections::HashSet;
|
||||
@@ -144,8 +144,9 @@ impl Signaller {
|
||||
&[
|
||||
&"nestri-data-channel",
|
||||
&gst::Structure::builder("config")
|
||||
.field("ordered", &false)
|
||||
.field("ordered", &true)
|
||||
.field("max-retransmits", &0u32)
|
||||
.field("priority", "high")
|
||||
.build(),
|
||||
],
|
||||
),
|
||||
@@ -337,12 +338,14 @@ impl ObjectSubclass for Signaller {
|
||||
impl ObjectImpl for Signaller {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPS: LazyLock<Vec<glib::ParamSpec>> = LazyLock::new(|| {
|
||||
vec![glib::ParamSpecBoolean::builder("manual-sdp-munging")
|
||||
.nick("Manual SDP munging")
|
||||
.blurb("Whether the signaller manages SDP munging itself")
|
||||
.default_value(false)
|
||||
.read_only()
|
||||
.build()]
|
||||
vec![
|
||||
glib::ParamSpecBoolean::builder("manual-sdp-munging")
|
||||
.nick("Manual SDP munging")
|
||||
.blurb("Whether the signaller manages SDP munging itself")
|
||||
.default_value(false)
|
||||
.read_only()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPS.as_ref()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
use crate::websocket::NestriWebSocket;
|
||||
use gst::glib;
|
||||
use gst::subclass::prelude::*;
|
||||
use gstrswebrtc::signaller::Signallable;
|
||||
use crate::websocket::NestriWebSocket;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod imp;
|
||||
|
||||
@@ -22,4 +22,4 @@ impl Default for NestriSignaller {
|
||||
fn default() -> Self {
|
||||
panic!("Cannot create NestriSignaller without NestriWebSocket");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod proto;
|
||||
pub mod proto;
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoTimestampEntry {
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub stage: ::prost::alloc::string::String,
|
||||
#[prost(message, optional, tag="2")]
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub time: ::core::option::Option<::prost_types::Timestamp>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoLatencyTracker {
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub sequence_id: ::prost::alloc::string::String,
|
||||
#[prost(message, repeated, tag="2")]
|
||||
#[prost(message, repeated, tag = "2")]
|
||||
pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>,
|
||||
}
|
||||
/// MouseMove message
|
||||
@@ -21,11 +21,11 @@ pub struct ProtoLatencyTracker {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMove {
|
||||
/// Fixed value "MouseMove"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag = "3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseMoveAbs message
|
||||
@@ -33,11 +33,11 @@ pub struct ProtoMouseMove {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMoveAbs {
|
||||
/// Fixed value "MouseMoveAbs"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag = "3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseWheel message
|
||||
@@ -45,11 +45,11 @@ pub struct ProtoMouseMoveAbs {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseWheel {
|
||||
/// Fixed value "MouseWheel"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag = "3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseKeyDown message
|
||||
@@ -57,9 +57,9 @@ pub struct ProtoMouseWheel {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyDown {
|
||||
/// Fixed value "MouseKeyDown"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// MouseKeyUp message
|
||||
@@ -67,9 +67,9 @@ pub struct ProtoMouseKeyDown {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyUp {
|
||||
/// Fixed value "MouseKeyUp"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// KeyDown message
|
||||
@@ -77,9 +77,9 @@ pub struct ProtoMouseKeyUp {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyDown {
|
||||
/// Fixed value "KeyDown"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// KeyUp message
|
||||
@@ -87,53 +87,53 @@ pub struct ProtoKeyDown {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyUp {
|
||||
/// Fixed value "KeyUp"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// Union of all Input types
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoInput {
|
||||
#[prost(oneof="proto_input::InputType", tags="1, 2, 3, 4, 5, 6, 7")]
|
||||
#[prost(oneof = "proto_input::InputType", tags = "1, 2, 3, 4, 5, 6, 7")]
|
||||
pub input_type: ::core::option::Option<proto_input::InputType>,
|
||||
}
|
||||
/// Nested message and enum types in `ProtoInput`.
|
||||
pub mod proto_input {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum InputType {
|
||||
#[prost(message, tag="1")]
|
||||
#[prost(message, tag = "1")]
|
||||
MouseMove(super::ProtoMouseMove),
|
||||
#[prost(message, tag="2")]
|
||||
#[prost(message, tag = "2")]
|
||||
MouseMoveAbs(super::ProtoMouseMoveAbs),
|
||||
#[prost(message, tag="3")]
|
||||
#[prost(message, tag = "3")]
|
||||
MouseWheel(super::ProtoMouseWheel),
|
||||
#[prost(message, tag="4")]
|
||||
#[prost(message, tag = "4")]
|
||||
MouseKeyDown(super::ProtoMouseKeyDown),
|
||||
#[prost(message, tag="5")]
|
||||
#[prost(message, tag = "5")]
|
||||
MouseKeyUp(super::ProtoMouseKeyUp),
|
||||
#[prost(message, tag="6")]
|
||||
#[prost(message, tag = "6")]
|
||||
KeyDown(super::ProtoKeyDown),
|
||||
#[prost(message, tag="7")]
|
||||
#[prost(message, tag = "7")]
|
||||
KeyUp(super::ProtoKeyUp),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMessageBase {
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub payload_type: ::prost::alloc::string::String,
|
||||
#[prost(message, optional, tag="2")]
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub latency: ::core::option::Option<ProtoLatencyTracker>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMessageInput {
|
||||
#[prost(message, optional, tag="1")]
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub message_base: ::core::option::Option<ProtoMessageBase>,
|
||||
#[prost(message, optional, tag="2")]
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub data: ::core::option::Option<ProtoInput>,
|
||||
}
|
||||
// @@protoc_insertion_point(module)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use crate::messages::{decode_message, encode_message, MessageBase, MessageLog};
|
||||
use crate::messages::{MessageBase, MessageLog, decode_message, encode_message};
|
||||
use futures_util::StreamExt;
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::stream::{SplitSink, SplitStream};
|
||||
use futures_util::StreamExt;
|
||||
use log::{Level, Log, Metadata, Record};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{mpsc, Mutex, Notify};
|
||||
use tokio::sync::{Mutex, Notify, mpsc};
|
||||
use tokio::time::sleep;
|
||||
use tokio_tungstenite::tungstenite::{Message, Utf8Bytes};
|
||||
use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream};
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async};
|
||||
|
||||
type Callback = Box<dyn Fn(String) + Send + Sync>;
|
||||
type WSRead = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
|
||||
@@ -95,7 +95,9 @@ impl NestriWebSocket {
|
||||
while let Some(message_result) = ws_read.next().await {
|
||||
match message_result {
|
||||
Ok(message) => {
|
||||
let data = message.into_text().expect("failed to turn message into text");
|
||||
let data = message
|
||||
.into_text()
|
||||
.expect("failed to turn message into text");
|
||||
let base_message = match decode_message(data.to_string()) {
|
||||
Ok(base_message) => base_message,
|
||||
Err(e) => {
|
||||
|
||||
@@ -10,10 +10,6 @@ import "@fontsource/geist-mono/400.css"
|
||||
import "@fontsource/geist-mono/700.css"
|
||||
//font-mona
|
||||
import "@fontsource-variable/mona-sans"
|
||||
// import "@fontsource/mona-sans/500.css"
|
||||
// import "@fontsource/mona-sans/600.css"
|
||||
// import "@fontsource/mona-sans/700.css"
|
||||
// import "@fontsource/mona-sans/800.css"
|
||||
//font-bricolage
|
||||
import '@fontsource-variable/bricolage-grotesque';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user