Skip to content

Commit

Permalink
Fixes a client-side bug with users' avatars missing resources.
Browse files Browse the repository at this point in the history
Sometimes, a user-patched message is getting sent to the client
where the nested avatar object does not have modelResource or
thumbnailResource. This was causing errors due to assuming
modelResource would always exist.

Workaround is to not assume that, and to attempt to fetch the
avatar manually if it is missing modelResource. It will then
spawn the avatar from this, and only if this failed to get the
necessary information is an error thrown about the avatar
missing its model.
  • Loading branch information
barankyle committed May 4, 2023
1 parent 1bab790 commit b3bab6d
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 14 deletions.
99 changes: 99 additions & 0 deletions .github/workflows/test-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: test-deploy

on:
push:
branches:
[missing-avatar-resources-fix]
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 $AWS_ACCESS_KEY $AWS_SECRET $AWS_REGION $CLUSTER_NAME
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET: ${{ secrets.AWS_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: Build Docker Image
run: bash scripts/build_docker_builder.sh dev $DOCKER_LABEL $PRIVATE_ECR $AWS_REGION
env:
DOCKER_LABEL: ${{ secrets.DOCKER_LABEL }}
REPO_NAME: ${{ secrets.DEV_REPO_NAME }}
AWS_REGION: ${{ secrets.AWS_REGION }}
ECR_URL: ${{ secrets.ECR_URL }}
PRIVATE_ECR: ${{ secrets.PRIVATE_ECR }}
- name: delete package.json
run: rm package.json
- name: npm-install 'cli' and 'aws-sdk'
run: npm install cli aws-sdk
- name: Publish to Elastic Container Registry
run: bash scripts/publish_ecr_builder.sh dev $GITHUB_SHA $DOCKER_LABEL $PRIVATE_ECR $AWS_REGION
env:
DOCKER_LABEL: ${{ secrets.DOCKER_LABEL }}
REPO_NAME: ${{ secrets.DEV_REPO_NAME }}
AWS_REGION: ${{ secrets.AWS_REGION }}
ECR_URL: ${{ secrets.ECR_URL }}
PRIVATE_ECR: ${{ secrets.PRIVATE_ECR }}
- name: Deploy to EKS
run: bash scripts/deploy_builder.sh dev $GITHUB_SHA
- name: Publish to Docker Hub
run: bash scripts/publish_dockerhub_builder.sh $GITHUB_SHA $DOCKER_LABEL
env:
DOCKER_LABEL: ${{ secrets.DOCKER_LABEL }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
PUBLISH_DOCKERHUB: ${{ secrets.PUBLISH_DOCKERHUB }}
- 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
28 changes: 21 additions & 7 deletions packages/client-core/src/components/World/EngineHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { NotificationService } from '../../common/services/NotificationService'
import { useRouter } from '../../common/services/RouterService'
import { useLocationState } from '../../social/services/LocationService'
import { SocketWebRTCClientNetwork } from '../../transports/SocketWebRTCClientFunctions'
import { AvatarService } from '../../user/services/AvatarService'
import { startClientSystems } from '../../world/startClientSystems'

const logger = multiLogger.child({ component: 'client-core:world' })
Expand All @@ -57,6 +58,24 @@ export const useLoadEngine = () => {
}, [])
}

const fetchMissingAvatar = async (user, avatarSpawnPose) => {
const avatar = await AvatarService.getAvatar(user.avatar.id.value)
if (avatar && (avatar.modelResource?.LOD0_url || (avatar.modelResource as any)?.src))
spawnLocalAvatarInWorld({
avatarSpawnPose,
avatarDetail: {
avatarURL: avatar.modelResource?.LOD0_url || (avatar.modelResource as any)?.src,
thumbnailURL: avatar.thumbnailResource?.LOD0_url || (avatar.thumbnailResource as any)?.src
},
name: user.name.value
})
else
NotificationService.dispatchNotify(
'Your avatar is missing its model. Please change your avatar from the user menu.',
{ variant: 'error' }
)
}

export const useLocationSpawnAvatar = (spectate = false) => {
const sceneLoaded = useHookstate(getMutableState(EngineState).sceneLoaded)
const authState = useAuthState()
Expand Down Expand Up @@ -89,7 +108,7 @@ export const useLocationSpawnAvatar = (spectate = false) => {
? getSpawnPoint(spawnPoint, Engine.instance.userId)
: getRandomSpawnPoint(Engine.instance.userId)

if (avatarDetails.modelResource?.LOD0_url || (avatarDetails.modelResource as any).src)
if (avatarDetails.modelResource?.LOD0_url || (avatarDetails.modelResource as any)?.src)
spawnLocalAvatarInWorld({
avatarSpawnPose,
avatarDetail: {
Expand All @@ -98,12 +117,7 @@ export const useLocationSpawnAvatar = (spectate = false) => {
},
name: user.name.value
})
else {
NotificationService.dispatchNotify(
'Your avatar is missing its model. Please change your avatar from the user menu.',
{ variant: 'error' }
)
}
else fetchMissingAvatar(user, avatarSpawnPose)
}, [sceneLoaded, authState.user, authState.user?.avatar, spectateParam])
}

Expand Down
9 changes: 9 additions & 0 deletions packages/client-core/src/user/services/AvatarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AvatarInterface } from '@etherealengine/common/src/interfaces/AvatarInt
import { StaticResourceInterface } from '@etherealengine/common/src/interfaces/StaticResourceInterface'
import { UserId } from '@etherealengine/common/src/interfaces/UserId'
import { matches, Validator } from '@etherealengine/engine/src/common/functions/MatchesUtils'
import { Engine } from '@etherealengine/engine/src/ecs/classes/Engine'
import { WorldNetworkAction } from '@etherealengine/engine/src/networking/functions/WorldNetworkAction'
import { defineAction, defineState, dispatchAction, getMutableState, useState } from '@etherealengine/hyperflux'

Expand Down Expand Up @@ -191,6 +192,14 @@ export const AvatarService = {
isPublic
}
}).promise as Promise<StaticResourceInterface[]>
},

async getAvatar(id: string) {
try {
return Engine.instance.api.service('avatar').get(id)
} catch (err) {
return null
}
}
}

Expand Down
147 changes: 140 additions & 7 deletions packages/server-core/src/user/user/user.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import addAssociations from '@etherealengine/server-core/src/hooks/add-associati
import addScopeToUser from '../../hooks/add-scope-to-user'
import authenticate from '../../hooks/authenticate'
import verifyScope from '../../hooks/verify-scope'
import logger from '../../ServerLogger'

const restrictUserPatch = (context: HookContext) => {
if (context.params.isInternal) return context
Expand Down Expand Up @@ -89,29 +90,161 @@ const addAvatarResources = () => {
return async (context: HookContext): Promise<HookContext> => {
const { app, result } = context

if (result.avatar) {
// if (!result.data) console.log('result.avatar', result, result.avatar, result.avatar?.dataValues, result.dataValues, result.dataValues?.avatar?.dataValues,)

if (result.dataValues?.avatar) {
if (result.dataValues?.avatar.modelResourceId)
try {
result.dataValues.avatar.modelResource = await app
.service('static-resource')
.get(result.dataValues.avatar.modelResourceId)
} catch (err) {
logger.error('error getting avatar model %o', err)
}
if (result.dataValues?.avatar.dataValues?.modelResourceId)
try {
result.dataValues.avatar.dataValues.modelResource = await app
.service('static-resource')
.get(result.dataValues.avatar.dataValues.modelResourceId)
} catch (err) {
logger.error('error getting avatar model %o', err)
}
if (result.dataValues?.avatar.thumbnailResourceId)
try {
result.dataValues.avatar.thumbnailResource = await app
.service('static-resource')
.get(result.dataValues.avatar.thumbnailResourceId)
} catch (err) {
logger.error('error getting avatar model %o', err)
}
if (result.dataValues?.avatar.dataValues?.thumbnailResourceId)
try {
result.dataValues.avatar.dataValues.thumbnailResource = await app
.service('static-resource')
.get(result.dataValues.avatar.dataValues.thumbnailResourceId)
} catch (err) {
logger.error('error getting avatar model %o', err)
}
} else if (result.avatar) {
if (result.avatar.modelResourceId)
try {
result.avatar.modelResource = await app.service('static-resource').get(result.avatar.modelResourceId)
} catch (err) {}
if (result.avatar.dataValues.modelResourceId)
} catch (err) {
logger.error('error getting avatar model %o', err)
}
if (result.avatar.dataValues?.modelResourceId)
try {
result.avatar.dataValues.modelResource = await app
.service('static-resource')
.get(result.avatar.dataValues.modelResourceId)
} catch (err) {}
} catch (err) {
logger.error('error getting avatar model %o', err)
}
if (result.avatar.thumbnailResourceId)
try {
result.avatar.thumbnailResource = await app.service('static-resource').get(result.avatar.thumbnailResourceId)
} catch (err) {}
if (result.avatar.dataValues.thumbnailResourceId)
} catch (err) {
logger.error('error getting avatar model %o', err)
}
if (result.avatar.dataValues?.thumbnailResourceId)
try {
result.avatar.dataValues.thumbnailResource = await app
.service('static-resource')
.get(result.avatar.dataValues.thumbnailResourceId)
} catch (err) {}
} catch (err) {
logger.error('error getting avatar model %o', err)
}
}

// if (result.data) {
// const mappedUsers = result.data.map(user => {
// return new Promise(async (resolve, reject) => {
// if (user.avatar) {
// if (user.avatar.modelResourceId)
// try {
// user.avatar.modelResource = await app.service('static-resource').get(user.avatar.modelResourceId)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// if (user.avatar.dataValues?.modelResourceId)
// try {
// user.avatar.dataValues.modelResource = await app
// .service('static-resource')
// .get(user.avatar.dataValues.modelResourceId)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// if (user.avatar.thumbnailResourceId)
// try {
// user.avatar.thumbnailResource = await app.service('static-resource').get(user.avatar.thumbnailResourceId)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// if (user.avatar.dataValues?.thumbnailResourceId)
// try {
// user.avatar.dataValues.thumbnailResource = await app
// .service('static-resource')
// .get(user.avatar.dataValues.thumbnailResourceId)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// }
// if (user.dataValues.avatar) {
// if (user.dataValues.avatar.modelResourceId)
// try {
// user.dataValues.avatar.modelResource = await app.service('static-resource').get(user.dataValues.avatar.modelResourceId)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// try {
// console.log('POPULATING MUTLI DATAVALUES MODELRESOURCE')
// user.dataValues.avatar.dataValues.modelResource = await app
// .service('static-resource')
// .get(user.dataValues.avatar.dataValues.modelResourceId)
// console.log('DONE POPULATED MUTLI DATAVALUES MODELRESOURCE', user, user.dataValues.avatar.dataValues)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// if (user.dataValues.avatar.thumbnailResourceId)
// try {
// user.dataValues.avatar.thumbnailResource = await app.service('static-resource').get(user.dataValues.avatar.thumbnailResourceId)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// if (user.dataValues.avatar.dataValues?.thumbnailResourceId)
// try {
// user.dataValues.avatar.dataValues.thumbnailResource = await app
// .service('static-resource')
// .get(user.dataValues.avatar.dataValues.thumbnailResourceId)
// resolve(user)
// } catch (err) {
// logger.error('error getting avatar model %o', err)
// reject(err)
// }
// }
// })
// })
// console.log('promises', mappedUsers)
// await Promise.all(mappedUsers)
// result.data = mappedUsers
// }

// if (!result.data) console.log('Returned result.avatar', result, result.avatar, result.avatar?.dataValues, result.dataValues?.avatar?.dataValues)
// if (result.data && result.total > 0) console.log('nested user', result.data[0].avatar.dataValues)
return context
}
}
Expand Down

0 comments on commit b3bab6d

Please sign in to comment.