diff --git a/.env.local.default b/.env.local.default index 4d9454f2cd..d187727b1a 100644 --- a/.env.local.default +++ b/.env.local.default @@ -17,7 +17,7 @@ KEY=certs/key.pem # Client variables --------------- APP_TITLE="IR Engine" -APP_LOGO=https://etherealengine-static.s3-us-east-1.amazonaws.com/logo.png +APP_LOGO=https://preview.ir.world/static/ir.svg APP_URL=https://localhost:3000 APP_HOST=localhost:3000 APP_PORT=3000 diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml deleted file mode 100755 index 41c5901310..0000000000 --- a/.github/workflows/dev-deploy.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: dev-deploy - -on: - push: - branches: [dev] -jobs: - secrets-gate-run: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-run.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-run - run: | - if [ ${{ secrets.DEPLOYMENTS_ENABLED }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - secrets-gate-webhook: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-webhook.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-webhook - run: | - if [ ${{ secrets.SEND_FINISHED_WEBHOOK }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - dev-deploy: - needs: - - secrets-gate-run - if: ${{ needs.secrets-gate-run.outputs.ok == 'enabled' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Setup Helm - run: scripts/setup_helm_builder.sh - - name: Setup AWS - run: scripts/setup_aws_builder.sh $EKS_AWS_ACCESS_KEY_ID $EKS_AWS_ACCESS_KEY_SECRET $AWS_REGION $CLUSTER_NAME - env: - EKS_AWS_ACCESS_KEY_ID: ${{ secrets.EKS_AWS_ACCESS_KEY_ID }} - EKS_AWS_ACCESS_KEY_SECRET: ${{ secrets.EKS_AWS_ACCESS_KEY_SECRET }} - AWS_REGION: ${{ secrets.AWS_REGION }} - CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }} - - name: Space debug - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - name: move package.json - run: mv package.json package.jsonmoved - - name: npm-install 'cli', @aws-sdk/client-ecr(-public), and @kubernetes/client-node - run: npm install cli @aws-sdk/client-ecr @aws-sdk/client-ecr-public @kubernetes/client-node - - name: restore package.json - run: mv package.jsonmoved package.json - - name: Expose GitHub Runtime - uses: crazy-max/ghaction-github-runtime@v2 - - name: Build and Push Docker Image - run: bash scripts/build_docker_builder.sh dev $GITHUB_SHA $AWS_REGION $PRIVATE_REPO - env: - STORAGE_AWS_ACCESS_KEY_ID: ${{ secrets.STORAGE_AWS_ACCESS_KEY_ID }} - STORAGE_AWS_ACCESS_KEY_SECRET: ${{ secrets.STORAGE_AWS_ACCESS_KEY_SECRET }} - REPO_NAME: ${{ secrets.DEV_REPO_NAME }} - AWS_REGION: ${{ secrets.AWS_REGION }} - REPO_URL: ${{ secrets.DEV_REPO_URL }} - REPO_PROVIDER: ${{ secrets.REPO_PROVIDER }} - PRIVATE_REPO: ${{ secrets.PRIVATE_REPO }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Deploy to EKS - run: bash scripts/deploy_builder.sh dev $GITHUB_SHA - - name: Job succeeded - if: ${{ needs.secrets-gate-webhook.outputs.ok == 'enabled' }} - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - env: - JOB_STATUS: ${{ job.status }} - WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} - HOOK_OS_NAME: ${{ runner.os }} - WORKFLOW_NAME: ${{ github.workflow }} - run: | - git clone https://github.com/DiscordHooks/github-actions-discord-webhook.git webhook - bash webhook/send.sh $JOB_STATUS $WEBHOOK_URL - shell: bash diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml deleted file mode 100755 index 1539bc63f5..0000000000 --- a/.github/workflows/prod-deploy.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: prod-deploy -on: - push: - branches: - [main] -jobs: - secrets-gate-run: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-run.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-run - run: | - if [ ${{ secrets.DEPLOYMENTS_ENABLED }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - secrets-gate-webhook: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-webhook.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-webhook - run: | - if [ ${{ secrets.SEND_FINISHED_WEBHOOK }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - prod-deploy: - needs: - - secrets-gate-run - if: ${{ needs.secrets-gate-run.outputs.ok == 'enabled' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Setup Helm - run: scripts/setup_helm_builder.sh - - name: Setup AWS - run: scripts/setup_aws_builder.sh $EKS_AWS_ACCESS_KEY_ID $EKS_AWS_ACCESS_KEY_SECRET $AWS_REGION $CLUSTER_NAME - env: - EKS_AWS_ACCESS_KEY_ID: ${{ secrets.EKS_AWS_ACCESS_KEY_ID }} - EKS_AWS_ACCESS_KEY_SECRET: ${{ secrets.EKS_AWS_ACCESS_KEY_SECRET }} - AWS_REGION: ${{ secrets.AWS_REGION }} - CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }} - - name: Space debug - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - name: move package.json - run: mv package.json package.jsonmoved - - name: npm-install 'cli', @aws-sdk/client-ecr(-public), and @kubernetes/client-node - run: npm install cli @aws-sdk/client-ecr @aws-sdk/client-ecr-public @kubernetes/client-node - - name: restore package.json - run: mv package.jsonmoved package.json - - name: Expose GitHub Runtime - uses: crazy-max/ghaction-github-runtime@v2 - - name: Build and Push Docker Image - run: bash scripts/build_docker_builder.sh prod $GITHUB_SHA $AWS_REGION $PRIVATE_REPO - env: - STORAGE_AWS_ACCESS_KEY_ID: ${{ secrets.STORAGE_AWS_ACCESS_KEY_ID }} - STORAGE_AWS_ACCESS_KEY_SECRET: ${{ secrets.STORAGE_AWS_ACCESS_KEY_SECRET }} - REPO_NAME: ${{ secrets.PROD_REPO_NAME }} - AWS_REGION: ${{ secrets.AWS_REGION }} - REPO_URL: ${{ secrets.PROD_REPO_URL }} - REPO_PROVIDER: ${{ secrets.REPO_PROVIDER }} - PRIVATE_REPO: ${{ secrets.PRIVATE_REPO }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Deploy to EKS - run: bash scripts/deploy_builder.sh prod $GITHUB_SHA - - name: Job succeeded - if: ${{ needs.secrets-gate-webhook.outputs.ok == 'enabled' }} - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - env: - JOB_STATUS: ${{ job.status }} - WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} - HOOK_OS_NAME: ${{ runner.os }} - WORKFLOW_NAME: ${{ github.workflow }} - run: | - git clone https://github.com/DiscordHooks/github-actions-discord-webhook.git webhook - bash webhook/send.sh $JOB_STATUS $WEBHOOK_URL - shell: bash diff --git a/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts b/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts index 1bb3f11853..67300cb262 100755 --- a/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts +++ b/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts @@ -54,7 +54,7 @@ import { getSearchParamFromURL } from '@etherealengine/common/src/utils/getSearc import { Engine } from '@etherealengine/ecs/src/Engine' import { defineSystem, destroySystem } from '@etherealengine/ecs/src/SystemFunctions' import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups' -import { AuthTask } from '@etherealengine/engine/src/avatar/functions/receiveJoinWorld' +import { AuthTask, ReadyTask } from '@etherealengine/engine/src/avatar/functions/receiveJoinWorld' import { Identifiable, PeerID, State, dispatchAction, getMutableState, getState, none } from '@etherealengine/hyperflux' import { Action, @@ -98,6 +98,8 @@ import { stopFaceTracking, stopLipsyncTracking } from '../media/webcam/WebcamInput' +import { ChannelState } from '../social/services/ChannelService' +import { LocationState } from '../social/services/LocationService' import { AuthState } from '../user/services/AuthService' import { MediaStreamState, MediaStreamService as _MediaStreamService } from './MediaStreams' import { clearPeerMediaChannels } from './PeerMediaChannelState' @@ -221,7 +223,7 @@ export const connectToInstance = ( if (instanceStillProvisioned(instanceID, locationID, channelID)) _connect() }, 3000) - const onConnect = () => { + const onConnect = async () => { if (aborted || !primus) return connecting = false primus.off('incoming::open', onConnect) @@ -230,31 +232,49 @@ export const connectToInstance = ( clearTimeout(connectionFailTimeout) const topic = locationID ? NetworkTopics.world : NetworkTopics.media - authenticatePrimus(primus, instanceID, topic) - - /** Server closed the connection. */ - const onDisconnect = () => { - if (aborted) return - if (primus) { - primus.off('incoming::end', onDisconnect) - primus.off('end', onDisconnect) + const instanceserverReady = await checkInstanceserverReady(primus, instanceID, topic) + if (instanceserverReady) { + await authenticatePrimus(primus, instanceID, topic) + + /** Server closed the connection. */ + const onDisconnect = () => { + if (aborted) return + if (primus) { + primus.off('incoming::end', onDisconnect) + primus.off('end', onDisconnect) + } + const network = getState(NetworkState).networks[instanceID] as SocketWebRTCClientNetwork + if (!network) return logger.error('Disconnected from unconnected instance ' + instanceID) + + logger.info('Disconnected from network %o', { topic: network.topic, id: network.id }) + /** + * If we are disconnected (server closes our socket) rather than leave the network, + * we just need to destroy and recreate the transport + */ + closeNetwork(network) + /** If we still have the instance provisioned, we should try again */ + if (instanceStillProvisioned(instanceID, locationID, channelID)) _connect() + } + // incoming::end is emitted when the server closes the connection + primus.on('incoming::end', onDisconnect) + // end is emitted when the client closes the connection + primus.on('end', onDisconnect) + } else { + if (locationID) { + const currentLocation = getMutableState(LocationState).currentLocation.location + const currentLocationId = currentLocation.id.value + currentLocation.id.set(undefined as unknown as LocationID) + currentLocation.id.set(currentLocationId) + } else { + const channelState = getMutableState(ChannelState) + const targetChannelId = channelState.targetChannelId.value + channelState.targetChannelId.set(undefined as unknown as ChannelID) + channelState.targetChannelId.set(targetChannelId) } - const network = getState(NetworkState).networks[instanceID] as SocketWebRTCClientNetwork - if (!network) return logger.error('Disconnected from unconnected instance ' + instanceID) - - logger.info('Disonnected from network %o', { topic: network.topic, id: network.id }) - /** - * If we are disconnected (server closes our socket) rather than leave the network, - * we just need to destroy and recreate the transport - */ - closeNetwork(network) - /** If we still have the instance provisioned, we should try again */ - if (instanceStillProvisioned(instanceID, locationID, channelID)) _connect() + primus.removeAllListeners() + primus.end() + console.log('PRIMUS GONE') } - // incoming::end is emitted when the server closes the connection - primus.on('incoming::end', onDisconnect) - // end is emitted when the client closes the connection - primus.on('end', onDisconnect) } primus!.on('incoming::open', onConnect) } @@ -283,6 +303,45 @@ export const getChannelIdFromTransport = (network: SocketWebRTCClientNetwork) => return isWorldConnection ? null : currentChannelInstanceConnection?.channelId } +export async function checkInstanceserverReady(primus: Primus, instanceID: InstanceID, topic: Topic) { + logger.info('Checking that instanceserver is ready') + const { instanceReady } = await new Promise((resolve) => { + const onStatus = (response: ReadyTask) => { + // eslint-disable-next-line no-prototype-builtins + if (response.hasOwnProperty('instanceReady')) { + clearInterval(interval) + resolve(response) + primus.off('data', onStatus) + primus.removeListener('incoming::end', onDisconnect) + } + } + + primus.on('data', onStatus) + + let disconnected = false + const interval = setInterval(() => { + if (disconnected) { + clearInterval(interval) + resolve({ instanceReady: false }) + primus.removeAllListeners() + primus.end() + return + } + }, 100) + + const onDisconnect = () => { + disconnected = true + } + primus.addListener('incoming::end', onDisconnect) + }) + + if (!instanceReady) { + unprovisionInstance(topic, instanceID) + } + + return instanceReady +} + export async function authenticatePrimus(primus: Primus, instanceID: InstanceID, topic: Topic) { logger.info('Authenticating instance ' + instanceID) @@ -325,10 +384,10 @@ export async function authenticatePrimus(primus: Primus, instanceID: InstanceID, /** We failed to connect to be authenticated, we do not want to try again */ // TODO: do we want to unprovision here? unprovisionInstance(topic, instanceID) - return logger.error(new Error('Unable to connect with credentials' + error)) + return logger.error(new Error('Unable to connect with credentials ' + error)) } - connectToNetwork(primus, instanceID, topic, hostPeerID!, routerRtpCapabilities!, cachedActions!) + await connectToNetwork(primus, instanceID, topic, hostPeerID!, routerRtpCapabilities!, cachedActions!) } export const connectToNetwork = async ( diff --git a/packages/engine/src/avatar/functions/receiveJoinWorld.ts b/packages/engine/src/avatar/functions/receiveJoinWorld.ts index 9c1af3c67e..68ce4f87e6 100644 --- a/packages/engine/src/avatar/functions/receiveJoinWorld.ts +++ b/packages/engine/src/avatar/functions/receiveJoinWorld.ts @@ -51,6 +51,10 @@ export type AuthTask = { error?: AuthError } +export type ReadyTask = { + instanceReady: boolean +} + export type JoinWorldRequestData = { inviteCode?: InviteCode } diff --git a/packages/instanceserver/src/SocketFunctions.ts b/packages/instanceserver/src/SocketFunctions.ts index 7b8489fee4..9ade3e2b18 100644 --- a/packages/instanceserver/src/SocketFunctions.ts +++ b/packages/instanceserver/src/SocketFunctions.ts @@ -41,6 +41,8 @@ import { getServerNetwork } from './SocketWebRTCServerFunctions' const logger = multiLogger.child({ component: 'instanceserver:spark' }) +const NON_READY_INTERVALS = 100 //100 tenths of a second, i.e. 10 seconds + export const setupSocketFunctions = async (app: Application, spark: any) => { let authTask: AuthTask | undefined @@ -50,15 +52,27 @@ export const setupSocketFunctions = async (app: Application, spark: any) => { * * Authorize user and make sure everything is valid before allowing them to join the world **/ - await new Promise((resolve) => { + const ready = await new Promise((resolve) => { + let counter = 0 const interval = setInterval(() => { + counter++ if (getState(InstanceServerState).ready) { clearInterval(interval) - resolve() + resolve(true) + } + if (counter > NON_READY_INTERVALS) { + clearInterval(interval) + resolve(false) } }, 100) }) + if (!ready) { + app.primus.write({ instanceReady: false }) + return + } + + app.primus.write({ instanceReady: true }) const network = getServerNetwork(app) const onAuthenticationRequest = async (data) => { diff --git a/packages/instanceserver/src/channels.ts b/packages/instanceserver/src/channels.ts index 38f141ed79..ec99989535 100755 --- a/packages/instanceserver/src/channels.ts +++ b/packages/instanceserver/src/channels.ts @@ -409,9 +409,13 @@ const updateInstance = async ({ if (isNeedingNewServer && !instanceStarted) { instanceStarted = true const initialized = await initializeInstance({ app, status, headers, userId }) - if (initialized) await loadEngine({ app, sceneId, headers }) - else instanceStarted = false - return true + if (initialized) { + await loadEngine({ app, sceneId, headers }) + return true + } else { + instanceStarted = false + return false + } } else { try { if (!getState(InstanceServerState).ready) diff --git a/packages/server-core/email-templates/account/layout.pug b/packages/server-core/email-templates/account/layout.pug new file mode 100755 index 0000000000..36fd7cfdd6 --- /dev/null +++ b/packages/server-core/email-templates/account/layout.pug @@ -0,0 +1,80 @@ +//- layout.pug +doctype html +html + head + style. + body{ + font-family: Roboto, Arial; + height: 100%; + width: auto; + background: #fff; + margin: 0; + padding-top: 2.5rem; + } + .container { + width: 75%; + max-width: 75%; + height: auto; + margin: 0 auto; + background: #383650; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + padding: 2rem; + padding-bottom: 4rem; + border-radius: 10px; + } + .main { + margin: 0 auto; + max-width: 90%; + } + .header { + padding-bottom: 2rem; + } + .logo{ + display: block; + margin: 0 auto; + heigh: auto; + max-width: 3em; + max-height: 4em; + } + .title-container{ + background: #5045AB; + padding: 2rem; + } + .message-container{ + background: #fff; + padding: 2rem; + } + h1 { + color: #fff; + font-size: 1.8rem; + text-wrap: wrap; + text-overflow: ellipsis; + } + h2 { + color: #918EAD; + } + p { + color: #8f9299; + font-weight: bold; + } + a { + word-wrap: break-word; + } + .link-btn { + width: 96%; + display: inline-block; + background: #5045AB; + text-align: center; + padding: 0.6rem; + border-radius: 5px; + text-decoration: none; + color: #fff !important; + font-weight: bold; + font-size: 1.2rem; + } +body + div(class='container') + .header + img(src=logo, class='logo') + .main + block content \ No newline at end of file diff --git a/packages/server-core/email-templates/account/magiclink-email.pug b/packages/server-core/email-templates/account/magiclink-email.pug index 7c11b93029..b248fc665b 100755 --- a/packages/server-core/email-templates/account/magiclink-email.pug +++ b/packages/server-core/email-templates/account/magiclink-email.pug @@ -1,16 +1,11 @@ -doctype html -body(style='margin: 0;') - div(style='margin: 0 auto; width:75%;background: #fff;') - div(style='margin-top:40px;height: auto;background: #383650;box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;padding: 4rem; border-radius: 10px;') - .hd(style='padding-bottom:30px') - img(src=logo, style='display: block; margin: 0 auto; max-width: 6em;') - .main - div(style='background: #5045AB;padding: 3rem 4rem 1.5rem 4rem;') - h1(style='font-family: Roboto, Arial; color: #fff;font-size: 1.8rem;') You've Got The Magic Link! - div(style='background: #fff;padding: 2rem;') - h2(style='font-family: Roboto, Arial;color: #918EAD;') Hi 👋! - p(style='font-family: Roboto, Arial;color: #8f9299;font-weight: bold;') You asked us to send you a magic link for quickly signing into your account. - a(href=hashLink, style='font-family: Roboto, Arial;width: 96%;display: inline-block;background: #5045AB;text-align: center;padding: 0.6rem;border-radius: 5px;text-decoration: none;color: #fff;font-weight: bold;font-size: 1.2rem;font-family: Roboto, Arial;') Sign In Now! - div - p(style='color: #8f9299;font-weight: bold;font-family: Roboto, Arial;') Or copy and paste this link into your browser: - a(style='word-wrap: break-word;', href=hashLink) #{hashLink} \ No newline at end of file +extends layout.pug +block content + .title-container + h1 You've Got The Magic Link! + .message-container + h2 Hi 👋! + p You asked us to send you a magic link for quickly signing into your account. + a(href=hashLink class='link-btn') Sign In Now! + div + p Or copy and paste this link into your browser: + a(href=hashLink) #{hashLink} \ No newline at end of file diff --git a/packages/spatial/src/physics/components/RigidBodyComponent.ts b/packages/spatial/src/physics/components/RigidBodyComponent.ts index 2c0706dd38..9384e3bcdc 100644 --- a/packages/spatial/src/physics/components/RigidBodyComponent.ts +++ b/packages/spatial/src/physics/components/RigidBodyComponent.ts @@ -28,6 +28,7 @@ import { Types } from 'bitecs' import { useEntityContext } from '@etherealengine/ecs' import { defineComponent, + hasComponent, removeComponent, setComponent, useComponent @@ -123,6 +124,7 @@ export const RigidBodyComponent = defineComponent({ component.initialized.set(true) return () => { Physics.removeRigidbody(physicsWorld, entity) + if (!hasComponent(entity, RigidBodyComponent)) return component.initialized.set(false) } }, [physicsWorld])